@zag-js/accordion
Version:
Core logic for the accordion widget implemented as a state machine
317 lines (313 loc) • 10.1 kB
JavaScript
import { createAnatomy } from '@zag-js/anatomy';
import { prevById, nextById, queryAll, getEventKey, isSafari, dataAttr } from '@zag-js/dom-query';
import { warn, last, first, add, remove, createSplitProps } from '@zag-js/utils';
import { createGuards, createMachine } from '@zag-js/core';
import { createProps } from '@zag-js/types';
// src/accordion.anatomy.ts
var anatomy = createAnatomy("accordion").parts("root", "item", "itemTrigger", "itemContent", "itemIndicator");
var parts = anatomy.build();
var getRootId = (ctx) => ctx.ids?.root ?? `accordion:${ctx.id}`;
var getItemId = (ctx, value) => ctx.ids?.item?.(value) ?? `accordion:${ctx.id}:item:${value}`;
var getItemContentId = (ctx, value) => ctx.ids?.itemContent?.(value) ?? `accordion:${ctx.id}:content:${value}`;
var getItemTriggerId = (ctx, value) => ctx.ids?.itemTrigger?.(value) ?? `accordion:${ctx.id}:trigger:${value}`;
var getRootEl = (ctx) => ctx.getById(getRootId(ctx));
var getTriggerEls = (ctx) => {
const ownerId = CSS.escape(getRootId(ctx));
const selector = `[aria-controls][data-ownedby='${ownerId}']:not([disabled])`;
return queryAll(getRootEl(ctx), selector);
};
var getFirstTriggerEl = (ctx) => first(getTriggerEls(ctx));
var getLastTriggerEl = (ctx) => last(getTriggerEls(ctx));
var getNextTriggerEl = (ctx, id) => nextById(getTriggerEls(ctx), getItemTriggerId(ctx, id));
var getPrevTriggerEl = (ctx, id) => prevById(getTriggerEls(ctx), getItemTriggerId(ctx, id));
// src/accordion.connect.ts
function connect(service, normalize) {
const { send, context, prop, scope, computed } = service;
const focusedValue = context.get("focusedValue");
const value = context.get("value");
const multiple = prop("multiple");
function setValue(value2) {
let nextValue = value2;
if (!multiple && nextValue.length > 1) {
nextValue = [nextValue[0]];
}
send({ type: "VALUE.SET", value: nextValue });
}
function getItemState(props2) {
return {
expanded: value.includes(props2.value),
focused: focusedValue === props2.value,
disabled: Boolean(props2.disabled ?? prop("disabled"))
};
}
return {
focusedValue,
value,
setValue,
getItemState,
getRootProps() {
return normalize.element({
...parts.root.attrs,
dir: prop("dir"),
id: getRootId(scope),
"data-orientation": prop("orientation")
});
},
getItemProps(props2) {
const itemState = getItemState(props2);
return normalize.element({
...parts.item.attrs,
dir: prop("dir"),
id: getItemId(scope, props2.value),
"data-state": itemState.expanded ? "open" : "closed",
"data-focus": dataAttr(itemState.focused),
"data-disabled": dataAttr(itemState.disabled),
"data-orientation": prop("orientation")
});
},
getItemContentProps(props2) {
const itemState = getItemState(props2);
return normalize.element({
...parts.itemContent.attrs,
dir: prop("dir"),
role: "region",
id: getItemContentId(scope, props2.value),
"aria-labelledby": getItemTriggerId(scope, props2.value),
hidden: !itemState.expanded,
"data-state": itemState.expanded ? "open" : "closed",
"data-disabled": dataAttr(itemState.disabled),
"data-focus": dataAttr(itemState.focused),
"data-orientation": prop("orientation")
});
},
getItemIndicatorProps(props2) {
const itemState = getItemState(props2);
return normalize.element({
...parts.itemIndicator.attrs,
dir: prop("dir"),
"aria-hidden": true,
"data-state": itemState.expanded ? "open" : "closed",
"data-disabled": dataAttr(itemState.disabled),
"data-focus": dataAttr(itemState.focused),
"data-orientation": prop("orientation")
});
},
getItemTriggerProps(props2) {
const { value: value2 } = props2;
const itemState = getItemState(props2);
return normalize.button({
...parts.itemTrigger.attrs,
type: "button",
dir: prop("dir"),
id: getItemTriggerId(scope, value2),
"aria-controls": getItemContentId(scope, value2),
"aria-expanded": itemState.expanded,
disabled: itemState.disabled,
"data-orientation": prop("orientation"),
"aria-disabled": itemState.disabled,
"data-state": itemState.expanded ? "open" : "closed",
"data-ownedby": getRootId(scope),
onFocus() {
if (itemState.disabled) return;
send({ type: "TRIGGER.FOCUS", value: value2 });
},
onBlur() {
if (itemState.disabled) return;
send({ type: "TRIGGER.BLUR" });
},
onClick(event) {
if (itemState.disabled) return;
if (isSafari()) {
event.currentTarget.focus();
}
send({ type: "TRIGGER.CLICK", value: value2 });
},
onKeyDown(event) {
if (event.defaultPrevented) return;
if (itemState.disabled) return;
const keyMap = {
ArrowDown() {
if (computed("isHorizontal")) return;
send({ type: "GOTO.NEXT", value: value2 });
},
ArrowUp() {
if (computed("isHorizontal")) return;
send({ type: "GOTO.PREV", value: value2 });
},
ArrowRight() {
if (!computed("isHorizontal")) return;
send({ type: "GOTO.NEXT", value: value2 });
},
ArrowLeft() {
if (!computed("isHorizontal")) return;
send({ type: "GOTO.PREV", value: value2 });
},
Home() {
send({ type: "GOTO.FIRST", value: value2 });
},
End() {
send({ type: "GOTO.LAST", value: value2 });
}
};
const key = getEventKey(event, {
dir: prop("dir"),
orientation: prop("orientation")
});
const exec = keyMap[key];
if (exec) {
exec(event);
event.preventDefault();
}
}
});
}
};
}
var { and, not } = createGuards();
var machine = createMachine({
props({ props: props2 }) {
return {
collapsible: false,
multiple: false,
orientation: "vertical",
defaultValue: [],
...props2
};
},
initialState() {
return "idle";
},
context({ prop, bindable }) {
return {
focusedValue: bindable(() => ({
defaultValue: null,
sync: true,
onChange(value) {
prop("onFocusChange")?.({ value });
}
})),
value: bindable(() => ({
defaultValue: prop("defaultValue"),
value: prop("value"),
onChange(value) {
prop("onValueChange")?.({ value });
}
}))
};
},
computed: {
isHorizontal: ({ prop }) => prop("orientation") === "horizontal"
},
on: {
"VALUE.SET": {
actions: ["setValue"]
}
},
states: {
idle: {
on: {
"TRIGGER.FOCUS": {
target: "focused",
actions: ["setFocusedValue"]
}
}
},
focused: {
on: {
"GOTO.NEXT": {
actions: ["focusNextTrigger"]
},
"GOTO.PREV": {
actions: ["focusPrevTrigger"]
},
"TRIGGER.CLICK": [
{
guard: and("isExpanded", "canToggle"),
actions: ["collapse"]
},
{
guard: not("isExpanded"),
actions: ["expand"]
}
],
"GOTO.FIRST": {
actions: ["focusFirstTrigger"]
},
"GOTO.LAST": {
actions: ["focusLastTrigger"]
},
"TRIGGER.BLUR": {
target: "idle",
actions: ["clearFocusedValue"]
}
}
}
},
implementations: {
guards: {
canToggle: ({ prop }) => !!prop("collapsible") || !!prop("multiple"),
isExpanded: ({ context, event }) => context.get("value").includes(event.value)
},
actions: {
collapse({ context, prop, event }) {
const next = prop("multiple") ? remove(context.get("value"), event.value) : [];
context.set("value", next);
},
expand({ context, prop, event }) {
const next = prop("multiple") ? add(context.get("value"), event.value) : [event.value];
context.set("value", next);
},
focusFirstTrigger({ scope }) {
getFirstTriggerEl(scope)?.focus();
},
focusLastTrigger({ scope }) {
getLastTriggerEl(scope)?.focus();
},
focusNextTrigger({ context, scope }) {
const focusedValue = context.get("focusedValue");
if (!focusedValue) return;
const triggerEl = getNextTriggerEl(scope, focusedValue);
triggerEl?.focus();
},
focusPrevTrigger({ context, scope }) {
const focusedValue = context.get("focusedValue");
if (!focusedValue) return;
const triggerEl = getPrevTriggerEl(scope, focusedValue);
triggerEl?.focus();
},
setFocusedValue({ context, event }) {
context.set("focusedValue", event.value);
},
clearFocusedValue({ context }) {
context.set("focusedValue", null);
},
setValue({ context, event }) {
context.set("value", event.value);
},
coarseValue({ context, prop }) {
if (!prop("multiple") && context.get("value").length > 1) {
warn(`The value of accordion should be a single value when multiple is false.`);
context.set("value", [context.get("value")[0]]);
}
}
}
}
});
var props = createProps()([
"collapsible",
"dir",
"disabled",
"getRootNode",
"id",
"ids",
"multiple",
"onFocusChange",
"onValueChange",
"orientation",
"value",
"defaultValue"
]);
var splitProps = createSplitProps(props);
var itemProps = createProps()(["value", "disabled"]);
var splitItemProps = createSplitProps(itemProps);
export { anatomy, connect, itemProps, machine, props, splitItemProps, splitProps };