@zag-js/tabs
Version:
Core logic for the tabs widget implemented as a state machine
300 lines (298 loc) • 9.73 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/tabs.machine.ts
var tabs_machine_exports = {};
__export(tabs_machine_exports, {
machine: () => machine
});
module.exports = __toCommonJS(tabs_machine_exports);
var import_core = require("@zag-js/core");
var import_dom_query = require("@zag-js/dom-query");
var import_utils = require("@zag-js/utils");
var dom = __toESM(require("./tabs.dom.js"));
var { createMachine } = (0, import_core.setup)();
var machine = createMachine({
props({ props }) {
return {
dir: "ltr",
orientation: "horizontal",
activationMode: "automatic",
loopFocus: true,
composite: true,
navigate(details) {
(0, import_dom_query.clickIfLink)(details.node);
},
defaultValue: null,
...props
};
},
initialState() {
return "idle";
},
context({ prop, bindable }) {
return {
value: bindable(() => ({
defaultValue: prop("defaultValue"),
value: prop("value"),
onChange(value) {
prop("onValueChange")?.({ value });
}
})),
focusedValue: bindable(() => ({
defaultValue: prop("value") || prop("defaultValue"),
sync: true,
onChange(value) {
prop("onFocusChange")?.({ focusedValue: value });
}
})),
ssr: bindable(() => ({ defaultValue: true })),
indicatorRect: bindable(() => ({
defaultValue: null
}))
};
},
watch({ context, prop, track, action }) {
track([() => context.get("value")], () => {
action(["syncIndicatorRect", "syncTabIndex", "navigateIfNeeded"]);
});
track([() => prop("dir"), () => prop("orientation")], () => {
action(["syncIndicatorRect"]);
});
},
on: {
SET_VALUE: {
actions: ["setValue"]
},
CLEAR_VALUE: {
actions: ["clearValue"]
},
SET_INDICATOR_RECT: {
actions: ["setIndicatorRect"]
},
SYNC_TAB_INDEX: {
actions: ["syncTabIndex"]
}
},
entry: ["syncIndicatorRect", "syncTabIndex", "syncSsr"],
exit: ["cleanupObserver"],
states: {
idle: {
on: {
TAB_FOCUS: {
target: "focused",
actions: ["setFocusedValue"]
},
TAB_CLICK: {
target: "focused",
actions: ["setFocusedValue", "setValue"]
}
}
},
focused: {
on: {
TAB_CLICK: {
actions: ["setFocusedValue", "setValue"]
},
ARROW_PREV: [
{
guard: "selectOnFocus",
actions: ["focusPrevTab", "selectFocusedTab"]
},
{
actions: ["focusPrevTab"]
}
],
ARROW_NEXT: [
{
guard: "selectOnFocus",
actions: ["focusNextTab", "selectFocusedTab"]
},
{
actions: ["focusNextTab"]
}
],
HOME: [
{
guard: "selectOnFocus",
actions: ["focusFirstTab", "selectFocusedTab"]
},
{
actions: ["focusFirstTab"]
}
],
END: [
{
guard: "selectOnFocus",
actions: ["focusLastTab", "selectFocusedTab"]
},
{
actions: ["focusLastTab"]
}
],
TAB_FOCUS: {
actions: ["setFocusedValue"]
},
TAB_BLUR: {
target: "idle",
actions: ["clearFocusedValue"]
}
}
}
},
implementations: {
guards: {
selectOnFocus: ({ prop }) => prop("activationMode") === "automatic"
},
actions: {
selectFocusedTab({ context, prop }) {
(0, import_dom_query.raf)(() => {
const focusedValue = context.get("focusedValue");
if (!focusedValue) return;
const nullable = prop("deselectable") && context.get("value") === focusedValue;
const value = nullable ? null : focusedValue;
context.set("value", value);
});
},
setFocusedValue({ context, event, flush }) {
if (event.value == null) return;
flush(() => {
context.set("focusedValue", event.value);
});
},
clearFocusedValue({ context }) {
context.set("focusedValue", null);
},
setValue({ context, event, prop }) {
const nullable = prop("deselectable") && context.get("value") === context.get("focusedValue");
context.set("value", nullable ? null : event.value);
},
clearValue({ context }) {
context.set("value", null);
},
focusFirstTab({ scope }) {
(0, import_dom_query.raf)(() => {
dom.getFirstTriggerEl(scope)?.focus();
});
},
focusLastTab({ scope }) {
(0, import_dom_query.raf)(() => {
dom.getLastTriggerEl(scope)?.focus();
});
},
focusNextTab({ context, prop, scope, event }) {
const focusedValue = event.value ?? context.get("focusedValue");
if (!focusedValue) return;
const triggerEl = dom.getNextTriggerEl(scope, {
value: focusedValue,
loopFocus: prop("loopFocus")
});
(0, import_dom_query.raf)(() => {
if (prop("composite")) {
triggerEl?.focus();
} else if (triggerEl?.dataset.value != null) {
context.set("focusedValue", triggerEl.dataset.value);
}
});
},
focusPrevTab({ context, prop, scope, event }) {
const focusedValue = event.value ?? context.get("focusedValue");
if (!focusedValue) return;
const triggerEl = dom.getPrevTriggerEl(scope, {
value: focusedValue,
loopFocus: prop("loopFocus")
});
(0, import_dom_query.raf)(() => {
if (prop("composite")) {
triggerEl?.focus();
} else if (triggerEl?.dataset.value != null) {
context.set("focusedValue", triggerEl.dataset.value);
}
});
},
syncTabIndex({ context, scope }) {
(0, import_dom_query.raf)(() => {
const value = context.get("value");
if (!value) return;
const contentEl = dom.getContentEl(scope, value);
if (!contentEl) return;
const focusables = (0, import_dom_query.getFocusables)(contentEl);
if (focusables.length > 0) {
contentEl.removeAttribute("tabindex");
} else {
contentEl.setAttribute("tabindex", "0");
}
});
},
cleanupObserver({ refs }) {
const cleanup = refs.get("indicatorCleanup");
if (cleanup) cleanup();
},
setIndicatorRect({ context, event, scope }) {
const value = event.id ?? context.get("value");
const indicatorEl = dom.getIndicatorEl(scope);
if (!indicatorEl) return;
if (!value) return;
const triggerEl = dom.getTriggerEl(scope, value);
if (!triggerEl) return;
context.set("indicatorRect", dom.getRectByValue(scope, value));
},
syncSsr({ context }) {
context.set("ssr", false);
},
syncIndicatorRect({ context, refs, scope }) {
const cleanup = refs.get("indicatorCleanup");
if (cleanup) cleanup();
const indicatorEl = dom.getIndicatorEl(scope);
if (!indicatorEl) return;
const exec = () => {
const triggerEl = dom.getTriggerEl(scope, context.get("value"));
if (!triggerEl) return;
const rect = dom.getOffsetRect(triggerEl);
context.set("indicatorRect", (prev) => (0, import_utils.isEqual)(prev, rect) ? prev : rect);
};
exec();
const triggerEls = dom.getElements(scope);
const indicatorCleanup = (0, import_utils.callAll)(...triggerEls.map((el) => import_dom_query.resizeObserverBorderBox.observe(el, exec)));
refs.set("indicatorCleanup", indicatorCleanup);
},
navigateIfNeeded({ context, prop, scope }) {
const value = context.get("value");
if (!value) return;
const triggerEl = dom.getTriggerEl(scope, value);
if ((0, import_dom_query.isAnchorElement)(triggerEl)) {
prop("navigate")?.({ value, node: triggerEl, href: triggerEl.href });
}
}
}
}
});
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
machine
});