UNPKG

@zag-js/toggle-group

Version:

Core logic for the toggle widget implemented as a state machine

349 lines (344 loc) • 11 kB
'use strict'; 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/toggle-group.anatomy.ts var anatomy = anatomy$1.createAnatomy("toggle-group").parts("root", "item"); var parts = anatomy.build(); var getRootId = (ctx) => ctx.ids?.root ?? `toggle-group:${ctx.id}`; var getItemId = (ctx, value) => ctx.ids?.item?.(value) ?? `toggle-group:${ctx.id}:${value}`; var getRootEl = (ctx) => ctx.getById(getRootId(ctx)); var getElements = (ctx) => { const ownerId = CSS.escape(getRootId(ctx)); const selector = `[data-ownedby='${ownerId}']:not([data-disabled])`; return domQuery.queryAll(getRootEl(ctx), selector); }; var getFirstEl = (ctx) => utils.first(getElements(ctx)); var getLastEl = (ctx) => utils.last(getElements(ctx)); var getNextEl = (ctx, id, loopFocus) => domQuery.nextById(getElements(ctx), id, loopFocus); var getPrevEl = (ctx, id, loopFocus) => domQuery.prevById(getElements(ctx), id, loopFocus); // src/toggle-group.connect.ts function connect(service, normalize) { const { context, send, prop, scope } = service; const value = context.get("value"); const disabled = prop("disabled"); const isSingle = !prop("multiple"); const rovingFocus = prop("rovingFocus"); const isHorizontal = prop("orientation") === "horizontal"; function getItemState(props2) { const id = getItemId(scope, props2.value); return { id, disabled: Boolean(props2.disabled || disabled), pressed: !!value.includes(props2.value), focused: context.get("focusedId") === id }; } return { value, setValue(value2) { send({ type: "VALUE.SET", value: value2 }); }, getRootProps() { return normalize.element({ ...parts.root.attrs, id: getRootId(scope), dir: prop("dir"), role: isSingle ? "radiogroup" : "group", tabIndex: context.get("isTabbingBackward") ? -1 : 0, "data-disabled": domQuery.dataAttr(disabled), "data-orientation": prop("orientation"), "data-focus": domQuery.dataAttr(context.get("focusedId") != null), style: { outline: "none" }, onMouseDown() { if (disabled) return; send({ type: "ROOT.MOUSE_DOWN" }); }, onFocus(event) { if (disabled) return; if (event.currentTarget !== domQuery.getEventTarget(event)) return; if (context.get("isClickFocus")) return; if (context.get("isTabbingBackward")) return; send({ type: "ROOT.FOCUS" }); }, onBlur(event) { const target = event.relatedTarget; if (domQuery.contains(event.currentTarget, target)) return; if (disabled) return; send({ type: "ROOT.BLUR" }); } }); }, getItemState, getItemProps(props2) { const itemState = getItemState(props2); const rovingTabIndex = itemState.focused ? 0 : -1; return normalize.button({ ...parts.item.attrs, id: itemState.id, type: "button", "data-ownedby": getRootId(scope), "data-focus": domQuery.dataAttr(itemState.focused), disabled: itemState.disabled, tabIndex: rovingFocus ? rovingTabIndex : void 0, // radio role: isSingle ? "radio" : void 0, "aria-checked": isSingle ? itemState.pressed : void 0, "aria-pressed": isSingle ? void 0 : itemState.pressed, // "data-disabled": domQuery.dataAttr(itemState.disabled), "data-orientation": prop("orientation"), dir: prop("dir"), "data-state": itemState.pressed ? "on" : "off", onFocus() { if (itemState.disabled) return; send({ type: "TOGGLE.FOCUS", id: itemState.id }); }, onClick(event) { if (itemState.disabled) return; send({ type: "TOGGLE.CLICK", id: itemState.id, value: props2.value }); if (domQuery.isSafari()) { event.currentTarget.focus({ preventScroll: true }); } }, onKeyDown(event) { if (event.defaultPrevented) return; if (!domQuery.contains(event.currentTarget, domQuery.getEventTarget(event))) return; if (itemState.disabled) return; const keyMap = { Tab(event2) { const isShiftTab = event2.shiftKey; send({ type: "TOGGLE.SHIFT_TAB", isShiftTab }); }, ArrowLeft() { if (!rovingFocus || !isHorizontal) return; send({ type: "TOGGLE.FOCUS_PREV" }); }, ArrowRight() { if (!rovingFocus || !isHorizontal) return; send({ type: "TOGGLE.FOCUS_NEXT" }); }, ArrowUp() { if (!rovingFocus || isHorizontal) return; send({ type: "TOGGLE.FOCUS_PREV" }); }, ArrowDown() { if (!rovingFocus || isHorizontal) return; send({ type: "TOGGLE.FOCUS_NEXT" }); }, Home() { if (!rovingFocus) return; send({ type: "TOGGLE.FOCUS_FIRST" }); }, End() { if (!rovingFocus) return; send({ type: "TOGGLE.FOCUS_LAST" }); } }; const exec = keyMap[domQuery.getEventKey(event)]; if (exec) { exec(event); if (event.key !== "Tab") event.preventDefault(); } } }); } }; } var { not, and } = core.createGuards(); var machine = core.createMachine({ props({ props: props2 }) { return { defaultValue: [], orientation: "horizontal", rovingFocus: true, loopFocus: true, deselectable: true, ...props2 }; }, initialState() { return "idle"; }, context({ prop, bindable }) { return { value: bindable(() => ({ defaultValue: prop("defaultValue"), value: prop("value"), onChange(value) { prop("onValueChange")?.({ value }); } })), focusedId: bindable(() => ({ defaultValue: null })), isTabbingBackward: bindable(() => ({ defaultValue: false })), isClickFocus: bindable(() => ({ defaultValue: false })), isWithinToolbar: bindable(() => ({ defaultValue: false })) }; }, computed: { currentLoopFocus: ({ context, prop }) => prop("loopFocus") && !context.get("isWithinToolbar") }, entry: ["checkIfWithinToolbar"], on: { "VALUE.SET": { actions: ["setValue"] }, "TOGGLE.CLICK": { actions: ["setValue"] }, "ROOT.MOUSE_DOWN": { actions: ["setClickFocus"] } }, states: { idle: { on: { "ROOT.FOCUS": { target: "focused", guard: not(and("isClickFocus", "isTabbingBackward")), actions: ["focusFirstToggle", "clearClickFocus"] }, "TOGGLE.FOCUS": { target: "focused", actions: ["setFocusedId"] } } }, focused: { on: { "ROOT.BLUR": { target: "idle", actions: ["clearIsTabbingBackward", "clearFocusedId", "clearClickFocus"] }, "TOGGLE.FOCUS": { actions: ["setFocusedId"] }, "TOGGLE.FOCUS_NEXT": { actions: ["focusNextToggle"] }, "TOGGLE.FOCUS_PREV": { actions: ["focusPrevToggle"] }, "TOGGLE.FOCUS_FIRST": { actions: ["focusFirstToggle"] }, "TOGGLE.FOCUS_LAST": { actions: ["focusLastToggle"] }, "TOGGLE.SHIFT_TAB": [ { guard: not("isFirstToggleFocused"), target: "idle", actions: ["setIsTabbingBackward"] }, { actions: ["setIsTabbingBackward"] } ] } } }, implementations: { guards: { isClickFocus: ({ context }) => context.get("isClickFocus"), isTabbingBackward: ({ context }) => context.get("isTabbingBackward"), isFirstToggleFocused: ({ context, scope }) => context.get("focusedId") === getFirstEl(scope)?.id }, actions: { setIsTabbingBackward({ context }) { context.set("isTabbingBackward", true); }, clearIsTabbingBackward({ context }) { context.set("isTabbingBackward", false); }, setClickFocus({ context }) { context.set("isClickFocus", true); }, clearClickFocus({ context }) { context.set("isClickFocus", false); }, checkIfWithinToolbar({ context, scope }) { const closestToolbar = getRootEl(scope)?.closest("[role=toolbar]"); context.set("isWithinToolbar", !!closestToolbar); }, setFocusedId({ context, event }) { context.set("focusedId", event.id); }, clearFocusedId({ context }) { context.set("focusedId", null); }, setValue({ context, event, prop }) { utils.ensureProps(event, ["value"]); let next = context.get("value"); if (utils.isArray(event.value)) { next = event.value; } else if (prop("multiple")) { next = utils.addOrRemove(next, event.value); } else { const isSelected = utils.isEqual(next, [event.value]); next = isSelected && prop("deselectable") ? [] : [event.value]; } context.set("value", next); }, focusNextToggle({ context, scope, prop }) { domQuery.raf(() => { const focusedId = context.get("focusedId"); if (!focusedId) return; getNextEl(scope, focusedId, prop("loopFocus"))?.focus({ preventScroll: true }); }); }, focusPrevToggle({ context, scope, prop }) { domQuery.raf(() => { const focusedId = context.get("focusedId"); if (!focusedId) return; getPrevEl(scope, focusedId, prop("loopFocus"))?.focus({ preventScroll: true }); }); }, focusFirstToggle({ scope }) { domQuery.raf(() => { getFirstEl(scope)?.focus({ preventScroll: true }); }); }, focusLastToggle({ scope }) { domQuery.raf(() => { getLastEl(scope)?.focus({ preventScroll: true }); }); } } } }); var props = types.createProps()([ "dir", "disabled", "getRootNode", "id", "ids", "loopFocus", "multiple", "onValueChange", "orientation", "rovingFocus", "value", "defaultValue", "deselectable" ]); var splitProps = utils.createSplitProps(props); var itemProps = types.createProps()(["value", "disabled"]); var splitItemProps = utils.createSplitProps(itemProps); exports.anatomy = anatomy; exports.connect = connect; exports.itemProps = itemProps; exports.machine = machine; exports.props = props; exports.splitItemProps = splitItemProps; exports.splitProps = splitProps;