UNPKG

@grail-ui/svelte

Version:

[![NPM](https://img.shields.io/npm/v/@grail-ui/svelte)](https://www.npmjs.com/package/@grail-ui/svelte) [![minified](https://img.shields.io/bundlephobia/min/@grail-ui/svelte)](https://bundlephobia.com/package/@grail-ui/svelte) [![minified + zipped](https:

145 lines (144 loc) 5.46 kB
import { derived, get, writable } from 'svelte/store'; import { uniqueId } from '../util/id.js'; import { listKeyManager } from '../keyManager/listKeyManager.js'; import { selectionModel } from '../collections/selectionModel.js'; import { chain } from '../util/chain.js'; import { addEventListener } from '../eventListener/eventListener.js'; import { ENTER, SPACE } from '../util/keyboard.js'; const ACCORDION_ATTRIBUTES = { item: 'data-accordion-item', trigger: 'data-accordion-trigger', content: 'data-accordion-content', }; const getState = (open) => (open ? 'open' : 'closed'); const getTriggerId = (baseId, value) => `${baseId}-trigger-${value}`; const getContentId = (baseId, value) => `${baseId}-content-${value}`; const getTriggers = (node) => Array.from(node.querySelectorAll(`[${ACCORDION_ATTRIBUTES.item}]`)) .flatMap((item) => Array.from(item.querySelectorAll(`[${ACCORDION_ATTRIBUTES.trigger}]`))) .filter(Boolean); export function createAccordion(config) { const { multiple, value, onValueChange, disabled } = { disabled: false, multiple: false, ...config, }; const disabled$ = writable(disabled); const baseId = uniqueId('accordion'); const initiallyExpandedKeys = value !== undefined ? (Array.isArray(value) ? value : [value]) : []; const { changed, toggle, select, deselect, clear } = selectionModel({ multiple: multiple, initiallySelectedValues: initiallyExpandedKeys, }); const expanded$ = derived(changed, ($changed) => $changed.selection, new Set()); function toggleTrigger(trigger) { if (trigger && !trigger.dataset.disabled && trigger.dataset.accordionTrigger) { toggle(trigger.dataset.accordionTrigger); } } let rootNode; const useAccordion = (node) => { rootNode = node; const items = writable([]); const keyManager = listKeyManager({ items, homeAndEnd: true, wrap: true, skipPredicate: (item) => 'disabled' in item.dataset, onActivate: (item) => item.focus(), }); function getTrigger(event) { const path = event.composedPath(); const node = path.find((el) => el instanceof HTMLElement && el.hasAttribute(ACCORDION_ATTRIBUTES.trigger)); return node; } const handleKeyDown = (event) => { const trigger = getTrigger(event); if (!trigger) return; if ([SPACE, ENTER].includes(event.key)) { event.preventDefault(); toggleTrigger(trigger); return; } const triggerCollection = getTriggers(node); items.set(triggerCollection); keyManager.setActiveItem(triggerCollection.indexOf(trigger)); keyManager.onKeydown(event); }; const handleClick = (event) => { toggleTrigger(getTrigger(event)); }; const removeEvents = chain(addEventListener(node, 'keydown', handleKeyDown), addEventListener(node, 'click', handleClick)); const unsubscribe = expanded$.subscribe((expanded) => { const keys = [...expanded]; onValueChange?.(multiple ? keys : keys[0]); }); return { destroy() { rootNode = undefined; keyManager.destroy(); removeEvents(); unsubscribe(); }, }; }; const derivedState = function (fn) { return derived([expanded$, disabled$], () => { return function (key) { const _disabled = get(disabled$); const disabled = Array.isArray(_disabled) ? _disabled.includes(key) : typeof _disabled === 'boolean' ? _disabled : _disabled === key; const state = getState(get(expanded$).has(key)); return fn({ key, disabled, state }); }; }); }; const itemAttrs = derivedState(({ key }) => ({ [ACCORDION_ATTRIBUTES.item]: key, })); const triggerAttrs = derivedState(({ key, disabled, state }) => ({ [ACCORDION_ATTRIBUTES.trigger]: key, id: getTriggerId(baseId, key), 'aria-controls': getContentId(baseId, key), 'aria-expanded': `${state === 'open'}`, ...(disabled ? { disabled: 'true', 'data-disabled': 'true' } : {}), })); const contentAttrs = derivedState(({ key, state }) => ({ [ACCORDION_ATTRIBUTES.content]: key, role: 'region', id: getContentId(baseId, key), 'aria-labelledby': getTriggerId(baseId, key), ...(state === 'closed' ? { inert: 'true' } : {}), })); const expand = (...keys) => { select(...keys); }; const collapse = (...keys) => { deselect(...keys); }; const expandAll = () => { if (rootNode) { const keys = getTriggers(rootNode).map((trigger) => trigger.dataset.accordionTrigger); select(...keys); } }; const collapseAll = () => { clear(); }; return { expanded: expanded$, disabled: disabled$, toggle, expand, collapse, expandAll, collapseAll, useAccordion, itemAttrs, triggerAttrs, contentAttrs, }; }