Source: tabs/Tabs.js

import { ReactiveDict } from 'meteor/reactive-dict'
import { Random } from 'meteor/random'
import './Tabs.html'

/**
 * Components to build tabs that users can switch between to reveal different content
 * @module
 * @see https://blazeui.meteorapp.com/components?c=Tabs
 */

/**
 * Root component, providing shared state to its children.
 * Note that currently only orientation='horizontal' and dir='ltr' are
 * supported states.
 * @type object
 */
export const Tabs = {
  name: 'Tabs',
  main: true,
  class: '',
  state: ({ instance }) => {
    instance.state = new ReactiveDict({
      orientation: 'horizontal',
      dir: 'ltr',
      current: instance.data?.defaultValue ?? null,
      mode: instance.data?.mode ?? 'manual',
      uid: Random.id(6)
    })
    return instance.state
  },
  attributes ({ props, state, api }) {
    const { merge } = api.styles()
    return {
      'data-orientation': state.orientation,
      dir: state.dir,
      class: merge(Tabs.class, props.class)
    }
  }
}

/**
 * @typedef TabsState
 * @property [orientation='horizontal'] {('horizontal'|'vertical')}
 * @property [dir='ltr'] {('ltr'|'rtl')}
 * @property [current=null] {string}
 * @property [mode='manual'] {('manual'|'auto')} if 'auto', tabs can be switched via keyboard by simply focusing them,
 *   otherwise, tabs are siwtched by clicking or pressing Space when in focus.
 * @property uid {string} a random unique id for this instance to prefix id attriubutes
 */

/** @private */
const useFromContext = () => ({ instance, api }) => {
  const resolve = api.state().useFromContext()
  return resolve({ instance })
}

/**
 * Place the content for a given tab with the respective value.
 * @type object
 * @property value {string} the value that links this content to the tab with the exact same value
 */
export const TabsContent = {
  name: 'TabsContent',
  class: "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
  state: useFromContext(),
  attributes ({ props, state, api }) {
    const { merge } = api.styles()
    const { value, disabled, class: className, context, ...rest } = props
    const uid = state.uid
    const isSelected = value === state.current
    return {
      'data-state': isSelected ? 'active' : 'inactive',
      'data-orientation': state.orientation,
      role: 'tabpanel',
      'aria-labelledby': `${uid}-trigger-${value}`,
      hidden: !isSelected,
      id: `${uid}-content-${value}`,
      tabIndex: 0,
      class: merge(TabsContent.class, className),
      ...rest
    }
  }
}

/**
 * Wraps the multiple tabs into a horizontal list
 * @type object
 */
export const TabsList = {
  name: 'TabsList',
  class: 'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
  state: useFromContext(),
  attributes ({ props, state, api }) {
    const { merge } = api.styles()
    const { class: className, context, ...rest } = props
    return {
      role: 'tablist',
      'aria-orientation': state.orientation,
      class: merge(TabsList.class, className),
      ...rest
    }
  },
  onRendered ({ instance }) {
    if (instance.state.get('current') === null) {
      const $triggers = instance.$('[role="tab"]')
      console.debug($triggers)
    }
  }
}

/**
 * Trigger for a tab is a styled button.
 * @type object
 * @property value {string} the value that links this trigger to the content with the exact same value
 * @property [disabled=false] {boolean} disables the button
 */
export const TabsTrigger = {
  name: 'TabsTrigger',
  class: "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
  state: useFromContext(),
  attributes ({ props, state, api }) {
    const { merge } = api.styles()
    const { value, disabled, class: className, context, ...rest } = props
    const uid = state.uid
    const isSelected = value === state.current
    return {
      type: 'button',
      role: 'tab',
      'aria-selected': isSelected,
      'aria-controls': `${uid}-content-${value}`,
      'data-state': isSelected ? 'active' : 'inactive',
      'data-disabled': disabled ? '' : undefined,
      'data-value': value,
      disabled: disabled,
      id: `${uid}-trigger-${value}`,
      class: merge(TabsTrigger.class, className),
      ...rest
    }
  },
  events: {
    'mousedown button' (e, t) {
      const disabled = t.state.get('disabled')
      const value = e.currentTarget.getAttribute('data-value')

      // only call handler if it's the left button (mousedown gets triggered by all mouse buttons)
      // but not when the control key is pressed (avoiding MacOS right click)
      if (!disabled && e.button === 0 && e.ctrlKey === false) {
        t.state.set({ current: value })
      }
      else {
        // prevent focus to avoid accidental activation
        e.preventDefault();
      }
    },
    'keydown button' (e, t) {
      const value = e.currentTarget.getAttribute('data-value')
      if ([' ', 'Enter'].includes(e.key)) t.state.set({ current: value })
    },
    'focus button' (e, t) {
      // handle "automatic" activation if necessary
      // ie. activate tab following focus
      const isAutomaticActivation = t.state.get('mode') !== 'manual';
      const value = e.currentTarget.getAttribute('data-value')
      const isSelected = t.state.get('current') === value
      const disabled = t.state.get('disabled')
      if (!isSelected && !disabled && isAutomaticActivation) {
        t.state.set({ current: value })
      }
    }
  }
}