@zag-js/steps
Version:
Core logic for the steps widget implemented as a state machine
302 lines (296 loc) • 8.72 kB
JavaScript
var anatomy$1 = require('@zag-js/anatomy');
var domQuery = require('@zag-js/dom-query');
var utils = require('@zag-js/utils');
var core = require('@zag-js/core');
var types = require('@zag-js/types');
// src/steps.anatomy.ts
var anatomy = anatomy$1.createAnatomy("steps").parts(
"root",
"list",
"item",
"trigger",
"indicator",
"separator",
"content",
"nextTrigger",
"prevTrigger",
"progress"
);
var parts = anatomy.build();
// src/steps.dom.ts
var getRootId = (ctx) => ctx.ids?.root ?? `steps:${ctx.id}`;
var getListId = (ctx) => ctx.ids?.list ?? `steps:${ctx.id}:list`;
var getTriggerId = (ctx, index) => ctx.ids?.triggerId?.(index) ?? `steps:${ctx.id}:trigger:${index}`;
var getContentId = (ctx, index) => ctx.ids?.contentId?.(index) ?? `steps:${ctx.id}:content:${index}`;
// src/steps.connect.ts
function connect(service, normalize) {
const { context, send, computed, prop, scope } = service;
const step = context.get("step");
const count = prop("count");
const percent = computed("percent");
const hasNextStep = computed("hasNextStep");
const hasPrevStep = computed("hasPrevStep");
const getItemState = (props2) => ({
triggerId: getTriggerId(scope, props2.index),
contentId: getContentId(scope, props2.index),
current: props2.index === step,
completed: props2.index < step,
incomplete: props2.index > step,
index: props2.index,
first: props2.index === 0,
last: props2.index === count - 1
});
const goToNextStep = () => {
send({ type: "STEP.NEXT", src: "next.trigger.click" });
};
const goToPrevStep = () => {
send({ type: "STEP.PREV", src: "prev.trigger.click" });
};
const resetStep = () => {
send({ type: "STEP.RESET", src: "reset.trigger.click" });
};
const setStep = (value) => {
send({ type: "STEP.SET", value, src: "api.setValue" });
};
return {
value: step,
count,
percent,
hasNextStep,
hasPrevStep,
isCompleted: computed("completed"),
goToNextStep,
goToPrevStep,
resetStep,
getItemState,
setStep,
getRootProps() {
return normalize.element({
...parts.root.attrs,
id: getRootId(scope),
dir: prop("dir"),
"data-orientation": prop("orientation"),
style: {
"--percent": `${percent}%`
}
});
},
getListProps() {
const arr = utils.fromLength(count);
const triggerIds = arr.map((_, index) => getTriggerId(scope, index));
return normalize.element({
...parts.list.attrs,
dir: prop("dir"),
id: getListId(scope),
role: "tablist",
"aria-owns": triggerIds.join(" "),
"aria-orientation": prop("orientation"),
"data-orientation": prop("orientation")
});
},
getItemProps(props2) {
const itemState = getItemState(props2);
return normalize.element({
...parts.item.attrs,
dir: prop("dir"),
"aria-current": itemState.current ? "step" : void 0,
"data-orientation": prop("orientation")
});
},
getTriggerProps(props2) {
const itemState = getItemState(props2);
return normalize.button({
...parts.trigger.attrs,
id: itemState.triggerId,
role: "tab",
dir: prop("dir"),
tabIndex: !prop("linear") || itemState.current ? 0 : -1,
"aria-selected": itemState.current,
"aria-controls": itemState.contentId,
"data-state": itemState.current ? "open" : "closed",
"data-orientation": prop("orientation"),
"data-complete": domQuery.dataAttr(itemState.completed),
"data-current": domQuery.dataAttr(itemState.current),
"data-incomplete": domQuery.dataAttr(itemState.incomplete),
onClick(event) {
if (event.defaultPrevented) return;
if (prop("linear")) return;
send({ type: "STEP.SET", value: props2.index, src: "trigger.click" });
}
});
},
getContentProps(props2) {
const itemState = getItemState(props2);
return normalize.element({
...parts.content.attrs,
dir: prop("dir"),
id: itemState.contentId,
role: "tabpanel",
tabIndex: 0,
hidden: !itemState.current,
"data-state": itemState.current ? "open" : "closed",
"data-orientation": prop("orientation"),
"aria-labelledby": itemState.triggerId
});
},
getIndicatorProps(props2) {
const itemState = getItemState(props2);
return normalize.element({
...parts.indicator.attrs,
dir: prop("dir"),
"aria-hidden": true,
"data-complete": domQuery.dataAttr(itemState.completed),
"data-current": domQuery.dataAttr(itemState.current),
"data-incomplete": domQuery.dataAttr(itemState.incomplete)
});
},
getSeparatorProps(props2) {
const itemState = getItemState(props2);
return normalize.element({
...parts.separator.attrs,
dir: prop("dir"),
"data-orientation": prop("orientation"),
"data-complete": domQuery.dataAttr(itemState.completed),
"data-current": domQuery.dataAttr(itemState.current),
"data-incomplete": domQuery.dataAttr(itemState.incomplete)
});
},
getNextTriggerProps() {
return normalize.button({
...parts.nextTrigger.attrs,
dir: prop("dir"),
type: "button",
disabled: !hasNextStep,
onClick(event) {
if (event.defaultPrevented) return;
goToNextStep();
}
});
},
getPrevTriggerProps() {
return normalize.button({
dir: prop("dir"),
...parts.prevTrigger.attrs,
type: "button",
disabled: !hasPrevStep,
onClick(event) {
if (event.defaultPrevented) return;
goToPrevStep();
}
});
},
getProgressProps() {
return normalize.element({
dir: prop("dir"),
...parts.progress.attrs,
role: "progressbar",
"aria-valuenow": percent,
"aria-valuemin": 0,
"aria-valuemax": 100,
"aria-valuetext": `${percent}% complete`,
"data-complete": domQuery.dataAttr(percent === 100)
});
}
};
}
var machine = core.createMachine({
props({ props: props2 }) {
return {
defaultStep: 0,
count: 1,
linear: false,
orientation: "horizontal",
...props2
};
},
context({ prop, bindable }) {
return {
step: bindable(() => ({
defaultValue: prop("defaultStep"),
value: prop("step"),
onChange(value) {
prop("onStepChange")?.({ step: value });
const completed = value == prop("count");
if (completed) prop("onStepComplete")?.();
}
}))
};
},
computed: {
percent: core.memo(
({ context, prop }) => [context.get("step"), prop("count")],
([step, count]) => step / count * 100
),
hasNextStep: ({ context, prop }) => context.get("step") < prop("count"),
hasPrevStep: ({ context }) => context.get("step") > 0,
completed: ({ context, prop }) => context.get("step") === prop("count")
},
initialState() {
return "idle";
},
entry: ["validateStep"],
states: {
idle: {
on: {
"STEP.SET": {
actions: ["setStep"]
},
"STEP.NEXT": {
actions: ["goToNextStep"]
},
"STEP.PREV": {
actions: ["goToPrevStep"]
},
"STEP.RESET": {
actions: ["resetStep"]
}
}
}
},
implementations: {
actions: {
goToNextStep({ context, prop }) {
const value = Math.min(context.get("step") + 1, prop("count"));
context.set("step", value);
},
goToPrevStep({ context }) {
const value = Math.max(context.get("step") - 1, 0);
context.set("step", value);
},
resetStep({ context }) {
context.set("step", 0);
},
setStep({ context, event }) {
context.set("step", event.value);
},
validateStep({ context, prop }) {
validateStep(prop("count"), context.get("step"));
}
}
}
});
var validateStep = (count, step) => {
if (!utils.isValueWithinRange(step, 0, count)) {
throw new RangeError(`[zag-js/steps] step index ${step} is out of bounds`);
}
};
var props = types.createProps()([
"count",
"dir",
"getRootNode",
"id",
"ids",
"linear",
"onStepChange",
"onStepComplete",
"orientation",
"step",
"defaultStep"
]);
var splitProps = utils.createSplitProps(props);
exports.anatomy = anatomy;
exports.connect = connect;
exports.machine = machine;
exports.props = props;
exports.splitProps = splitProps;
;