UNPKG

@melt-ui/svelte

Version:
167 lines (166 loc) 6.45 kB
import { addMeltEventListener, makeElement, createElHelpers, disabledAttr, executeCallbacks, getDirectionalKeys, getElemDirection, isBrowser, isHTMLElement, kbd, last, next, omit, overridable, prev, toWritableStores, } from '../../internal/helpers/index.js'; import { writable } from 'svelte/store'; const defaults = { orientation: 'horizontal', activateOnFocus: true, loop: true, autoSet: true, }; const { name, selector } = createElHelpers('tabs'); export function createTabs(props) { const withDefaults = { ...defaults, ...props }; const options = toWritableStores(omit(withDefaults, 'defaultValue', 'value', 'onValueChange', 'autoSet')); const { orientation, activateOnFocus, loop } = options; const valueWritable = withDefaults.value ?? writable(withDefaults.defaultValue); const value = overridable(valueWritable, withDefaults?.onValueChange); let ssrValue = withDefaults.defaultValue ?? value.get(); // Root const root = makeElement(name(), { stores: orientation, returned: ($orientation) => { return { 'data-orientation': $orientation, }; }, }); // List const list = makeElement(name('list'), { stores: orientation, returned: ($orientation) => { return { role: 'tablist', 'aria-orientation': $orientation, 'data-orientation': $orientation, }; }, }); const parseTriggerProps = (props) => { if (typeof props === 'string') { return { value: props }; } else { return props; } }; const trigger = makeElement(name('trigger'), { stores: [value, orientation], returned: ([$value, $orientation]) => { return (props) => { const { value: tabValue, disabled } = parseTriggerProps(props); if (!$value && !ssrValue && withDefaults.autoSet) { ssrValue = tabValue; $value = tabValue; value.set(tabValue); } const sourceOfTruth = isBrowser ? $value : ssrValue; const isActive = sourceOfTruth === tabValue; return { type: 'button', role: 'tab', 'data-state': isActive ? 'active' : 'inactive', tabindex: isActive ? 0 : -1, 'data-value': tabValue, 'data-orientation': $orientation, 'data-disabled': disabledAttr(disabled), disabled: disabledAttr(disabled), }; }; }, action: (node) => { const unsub = executeCallbacks(addMeltEventListener(node, 'focus', () => { const disabled = node.dataset.disabled === 'true'; const tabValue = node.dataset.value; if (activateOnFocus.get() && !disabled && tabValue !== undefined) { value.set(tabValue); } }), addMeltEventListener(node, 'click', (e) => { node.focus(); e.preventDefault(); const disabled = node.dataset.disabled === 'true'; if (disabled) return; const tabValue = node.dataset.value; node.focus(); if (tabValue !== undefined) { value.set(tabValue); } }), addMeltEventListener(node, 'keydown', (e) => { const tabValue = node.dataset.value; if (!tabValue) return; const el = e.currentTarget; if (!isHTMLElement(el)) return; const rootEl = el.closest(selector()); if (!isHTMLElement(rootEl)) return; const $loop = loop.get(); const triggers = Array.from(rootEl.querySelectorAll('[role="tab"]')).filter((trigger) => isHTMLElement(trigger)); const enabledTriggers = triggers.filter((el) => !el.hasAttribute('data-disabled')); const triggerIdx = enabledTriggers.findIndex((el) => el === e.target); const dir = getElemDirection(rootEl); const { nextKey, prevKey } = getDirectionalKeys(dir, orientation.get()); if (e.key === nextKey) { e.preventDefault(); const nextEl = next(enabledTriggers, triggerIdx, $loop); nextEl.focus(); } else if (e.key === prevKey) { e.preventDefault(); const prevEl = prev(enabledTriggers, triggerIdx, $loop); prevEl.focus(); } else if (e.key === kbd.ENTER || e.key === kbd.SPACE) { e.preventDefault(); value.set(tabValue); } else if (e.key === kbd.HOME) { e.preventDefault(); const firstTrigger = enabledTriggers[0]; firstTrigger.focus(); } else if (e.key === kbd.END) { e.preventDefault(); const lastTrigger = last(enabledTriggers); lastTrigger.focus(); } })); return { destroy: unsub, }; }, }); // Content const content = makeElement(name('content'), { stores: value, returned: ($value) => { return (tabValue) => { return { role: 'tabpanel', // TODO: improve 'aria-labelledby': tabValue, hidden: isBrowser ? $value === tabValue ? undefined : true : ssrValue === tabValue ? undefined : true, tabindex: 0, }; }; }, }); return { elements: { root, list, trigger, content, }, states: { value, }, options, }; }