UNPKG

melt

Version:

The next generation of Melt UI. Built for Svelte 5.

190 lines (189 loc) 5.97 kB
import { SelectionState } from "../utils/selection-state.svelte"; import { kbd } from "../utils/keyboard"; import { extract } from "../utils/extract"; import { dataAttr, disabledAttr } from "../utils/attribute"; import { createBuilderMetadata } from "../utils/identifiers"; import { isHtmlElement } from "../utils/is"; const { dataAttrs, dataSelectors, createIds } = createBuilderMetadata("accordion", [ "root", "item", "trigger", "heading", "content", ]); export class Accordion { // Props #props; multiple = $derived(extract(this.#props.multiple, false)); disabled = $derived(extract(this.#props.disabled, false)); // State #value; ids = $state(createIds()); constructor(props = {}) { this.#props = props; this.#value = new SelectionState({ value: props.value, onChange: props.onValueChange, multiple: props.multiple, }); } get value() { return this.#value.current; } set value(value) { this.#value.current = value; } /** * Spread attributes for the accordion root element. */ get root() { return { [dataAttrs.root]: "", id: this.ids.root, }; } /** * Returns an Item class with the necessary * spread attributes for an accordion item. * @param item */ getItem(item) { return new AccordionItem({ accordion: this, item, rootId: this.ids.root, }); } /** * Checks if an item is currently expanded. * @param id - ID of the item to check. */ isExpanded(id) { return this.#value.has(id); } /** * Expands a specific item. * @param id - ID of the item to expand. */ expand(id) { this.#value.add(id); } /** * Collapses a specific item. * @param id - ID of the item to collapse. */ collapse(id) { this.#value.delete(id); } /** * Toggles the expanded state of an item. * @param id - ID of the item to toggle. */ toggleExpanded(id) { if (this.#value.has(id)) { this.collapse(id); } else { if (this.multiple) { this.expand(id); } else { this.#value.clear(); this.expand(id); } } } } export class AccordionItem { #props; item = $derived(this.#props.item); #accordion = $derived(this.#props.accordion); #rootId = $derived(this.#props.rootId); /** Check if this item is disabled. */ isDisabled = $derived(this.#accordion.disabled || this.item.disabled); /** Check if this item is expanded. */ isExpanded = $derived(this.#accordion.isExpanded(this.item.id)); /** Expands this item. */ expand = () => this.#accordion.expand(this.item.id); /** Collapses this item. */ collapse = () => this.#accordion.collapse(this.item.id); /** Toggles the expanded state of this item. */ toggleExpanded = () => this.#accordion.toggleExpanded(this.item.id); constructor(props) { this.#props = props; } /** * Attributes for an accordion heading element. */ get heading() { return { [dataAttrs.heading]: "", role: "heading", "aria-level": this.item.headingLevel, "data-heading-level": this.item.headingLevel, }; } /** * Attributes for an accordion item trigger. */ get trigger() { return { [dataAttrs.trigger]: "", disabled: disabledAttr(this.isDisabled), "aria-disabled": this.isDisabled, "aria-expanded": this.isExpanded, "data-disabled": dataAttr(this.isDisabled), "data-value": this.item.id, "data-state": this.isExpanded ? "open" : "closed", onclick: () => this.toggleExpanded(), onkeydown: (e) => { const key = e.key; if (![kbd.ARROW_DOWN, kbd.ARROW_UP, kbd.HOME, kbd.END].includes(key)) { return; } e.preventDefault(); if (key === kbd.SPACE || key === kbd.ENTER) { if (this.isDisabled) return; this.toggleExpanded(); } const el = e.target; const rootEl = document.getElementById(this.#rootId); if (!rootEl || !isHtmlElement(el)) return; const items = Array.from(rootEl.querySelectorAll(dataSelectors.trigger)); const candidateItems = items.filter((item) => { if (!isHtmlElement(item)) return false; return !("disabled" in item.dataset); }); if (!candidateItems.length) return; const elIdx = candidateItems.indexOf(el); if (e.key === kbd.ARROW_DOWN) { candidateItems[(elIdx + 1) % candidateItems.length]?.focus(); } if (e.key === kbd.ARROW_UP) { candidateItems[(elIdx - 1 + candidateItems.length) % candidateItems.length]?.focus(); } if (e.key === kbd.HOME) { candidateItems[0]?.focus(); } if (e.key === kbd.END) { candidateItems[candidateItems.length - 1]?.focus(); } }, }; } /** * Attributes for an accordion content element. */ get content() { return { [dataAttrs.content]: "", "data-state": this.isExpanded ? "open" : "closed", "data-disabled": disabledAttr(this.isDisabled), "data-value": this.item.id, }; } }