| import { addMeltEventListener, makeElement, createElHelpers, disabledAttr, executeCallbacks, getElemDirection, handleRovingFocus, isHTMLElement, kbd, noop, omit, overridable, toWritableStores, } from '../../internal/helpers/index.js'; | |
| import { derived, writable } from 'svelte/store'; | |
| const defaults = { | |
| type: 'single', | |
| orientation: 'horizontal', | |
| loop: true, | |
| rovingFocus: true, | |
| disabled: false, | |
| defaultValue: '', | |
| }; | |
| const { name, selector } = createElHelpers('toggle-group'); | |
| export const createToggleGroup = (props) => { | |
| const withDefaults = { ...defaults, ...props }; | |
| const options = toWritableStores(omit(withDefaults, 'value')); | |
| const { type, orientation, loop, rovingFocus, disabled } = options; | |
| const defaultValue = withDefaults.defaultValue | |
| ? withDefaults.defaultValue | |
| : withDefaults.type === 'single' | |
| ? undefined | |
| : []; | |
| const valueWritable = withDefaults.value ?? writable(defaultValue); | |
| const value = overridable(valueWritable, withDefaults?.onValueChange); | |
| const root = makeElement(name(), { | |
| stores: orientation, | |
| returned: ($orientation) => { | |
| return { | |
| role: 'group', | |
| 'data-orientation': $orientation, | |
| }; | |
| }, | |
| }); | |
| const item = makeElement(name('item'), { | |
| stores: [value, disabled, orientation, type], | |
| returned: ([$value, $disabled, $orientation, $type]) => { | |
| return (props) => { | |
| const itemValue = typeof props === 'string' ? props : props.value; | |
| const argDisabled = typeof props === 'string' ? false : !!props.disabled; | |
| const disabled = $disabled || argDisabled; | |
| const pressed = Array.isArray($value) ? $value.includes(itemValue) : $value === itemValue; | |
| const isSingle = $type === 'single'; | |
| const isMultiple = $type === 'multiple' || $type === undefined; | |
| return { | |
| disabled: disabledAttr(disabled), | |
| pressed, | |
| 'data-orientation': $orientation, | |
| 'data-disabled': disabledAttr(disabled), | |
| 'data-state': pressed ? 'on' : 'off', | |
| 'data-value': itemValue, | |
| 'aria-pressed': isMultiple ? pressed : undefined, | |
| 'aria-checked': isSingle ? pressed : undefined, | |
| type: 'button', | |
| role: isSingle ? 'radio' : undefined, | |
| tabindex: pressed ? 0 : -1, | |
| }; | |
| }; | |
| }, | |
| action: (node) => { | |
| let unsub = noop; | |
| const parentGroup = node.closest(selector()); | |
| if (!isHTMLElement(parentGroup)) | |
| return {}; | |
| const items = Array.from(parentGroup.querySelectorAll(selector('item'))); | |
| const $value = value.get(); | |
| const anyPressed = Array.isArray($value) ? $value.length > 0 : $value ? true : false; | |
| if (!anyPressed && items[0] === node) { | |
| node.tabIndex = 0; | |
| } | |
| function getNodeProps() { | |
| const itemValue = node.dataset.value; | |
| const disabled = node.dataset.disabled === 'true'; | |
| return { value: itemValue, disabled }; | |
| } | |
| function handleValueUpdate() { | |
| const { value: itemValue, disabled } = getNodeProps(); | |
| if (itemValue === undefined || disabled) | |
| return; | |
| value.update(($value) => { | |
| if (Array.isArray($value)) { | |
| if ($value.includes(itemValue)) { | |
| return $value.filter((i) => i !== itemValue); | |
| } | |
| return [...$value, itemValue]; | |
| } | |
| return $value === itemValue ? undefined : itemValue; | |
| }); | |
| } | |
| unsub = executeCallbacks(addMeltEventListener(node, 'click', () => { | |
| handleValueUpdate(); | |
| }), addMeltEventListener(node, 'keydown', (e) => { | |
| if (e.key === kbd.SPACE || e.key === kbd.ENTER) { | |
| e.preventDefault(); | |
| handleValueUpdate(); | |
| return; | |
| } | |
| if (!rovingFocus.get()) | |
| return; | |
| const el = e.currentTarget; | |
| if (!isHTMLElement(el)) | |
| return; | |
| const root = el.closest(selector()); | |
| if (!isHTMLElement(root)) | |
| return; | |
| const items = Array.from(root.querySelectorAll(selector('item') + ':not([data-disabled])')).filter((item) => isHTMLElement(item)); | |
| const currentIndex = items.indexOf(el); | |
| const dir = getElemDirection(el); | |
| const $orientation = orientation.get(); | |
| const nextKey = { | |
| horizontal: dir === 'rtl' ? kbd.ARROW_LEFT : kbd.ARROW_RIGHT, | |
| vertical: kbd.ARROW_DOWN, | |
| }[$orientation ?? 'horizontal']; | |
| const prevKey = { | |
| horizontal: dir === 'rtl' ? kbd.ARROW_RIGHT : kbd.ARROW_LEFT, | |
| vertical: kbd.ARROW_UP, | |
| }[$orientation ?? 'horizontal']; | |
| const $loop = loop.get(); | |
| if (e.key === nextKey) { | |
| e.preventDefault(); | |
| const nextIndex = currentIndex + 1; | |
| if (nextIndex >= items.length && $loop) { | |
| handleRovingFocus(items[0]); | |
| } | |
| else { | |
| handleRovingFocus(items[nextIndex]); | |
| } | |
| } | |
| else if (e.key === prevKey) { | |
| e.preventDefault(); | |
| const prevIndex = currentIndex - 1; | |
| if (prevIndex < 0 && $loop) { | |
| handleRovingFocus(items[items.length - 1]); | |
| } | |
| else { | |
| handleRovingFocus(items[prevIndex]); | |
| } | |
| } | |
| else if (e.key === kbd.HOME) { | |
| e.preventDefault(); | |
| handleRovingFocus(items[0]); | |
| } | |
| else if (e.key === kbd.END) { | |
| e.preventDefault(); | |
| handleRovingFocus(items[items.length - 1]); | |
| } | |
| })); | |
| return { | |
| destroy: unsub, | |
| }; | |
| }, | |
| }); | |
| const isPressed = derived(value, ($value) => { | |
| return (itemValue) => { | |
| return Array.isArray($value) ? $value.includes(itemValue) : $value === itemValue; | |
| }; | |
| }); | |
| return { | |
| elements: { | |
| root, | |
| item, | |
| }, | |
| states: { | |
| value, | |
| }, | |
| helpers: { | |
| isPressed, | |
| }, | |
| options, | |
| }; | |
| }; | |