@grail-ui/svelte
Version:
[](https://www.npmjs.com/package/@grail-ui/svelte) [](https://bundlephobia.com/package/@grail-ui/svelte) [ • 5.46 kB
JavaScript
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,
};
}