melt
Version:
The next generation of Melt UI. Built for Svelte 5.
190 lines (189 loc) • 5.97 kB
JavaScript
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,
};
}
}