UNPKG

@zag-js/switch

Version:

Core logic for the switch widget implemented as a state machine

295 lines (290 loc) • 8.79 kB
import { createAnatomy } from '@zag-js/anatomy'; import { dispatchInputCheckedEvent, setElementChecked, trackFormControl, trackPress, dataAttr, visuallyHiddenStyle, getEventTarget, isSafari } from '@zag-js/dom-query'; import { trackFocusVisible, isFocusVisible } from '@zag-js/focus-visible'; import { createGuards, createMachine } from '@zag-js/core'; import { createProps } from '@zag-js/types'; import { createSplitProps } from '@zag-js/utils'; // src/switch.anatomy.ts var anatomy = createAnatomy("switch").parts("root", "label", "control", "thumb"); var parts = anatomy.build(); // src/switch.dom.ts var getRootId = (ctx) => ctx.ids?.root ?? `switch:${ctx.id}`; var getLabelId = (ctx) => ctx.ids?.label ?? `switch:${ctx.id}:label`; var getThumbId = (ctx) => ctx.ids?.thumb ?? `switch:${ctx.id}:thumb`; var getControlId = (ctx) => ctx.ids?.control ?? `switch:${ctx.id}:control`; var getHiddenInputId = (ctx) => ctx.ids?.hiddenInput ?? `switch:${ctx.id}:input`; var getRootEl = (ctx) => ctx.getById(getRootId(ctx)); var getHiddenInputEl = (ctx) => ctx.getById(getHiddenInputId(ctx)); // src/switch.connect.ts function connect(service, normalize) { const { context, send, prop, scope } = service; const disabled = !!prop("disabled"); const readOnly = !!prop("readOnly"); const required = !!prop("required"); const checked = !!context.get("checked"); const focused = !disabled && context.get("focused"); const focusVisible = !disabled && context.get("focusVisible"); const active = !disabled && context.get("active"); const dataAttrs = { "data-active": dataAttr(active), "data-focus": dataAttr(focused), "data-focus-visible": dataAttr(focusVisible), "data-readonly": dataAttr(readOnly), "data-hover": dataAttr(context.get("hovered")), "data-disabled": dataAttr(disabled), "data-state": checked ? "checked" : "unchecked", "data-invalid": dataAttr(prop("invalid")), "data-required": dataAttr(required) }; return { checked, disabled, focused, setChecked(checked2) { send({ type: "CHECKED.SET", checked: checked2, isTrusted: false }); }, toggleChecked() { send({ type: "CHECKED.TOGGLE", checked, isTrusted: false }); }, getRootProps() { return normalize.label({ ...parts.root.attrs, ...dataAttrs, dir: prop("dir"), id: getRootId(scope), htmlFor: getHiddenInputId(scope), onPointerMove() { if (disabled) return; send({ type: "CONTEXT.SET", context: { hovered: true } }); }, onPointerLeave() { if (disabled) return; send({ type: "CONTEXT.SET", context: { hovered: false } }); }, onClick(event) { if (disabled) return; const target = getEventTarget(event); if (target === getHiddenInputEl(scope)) { event.stopPropagation(); } if (isSafari()) { getHiddenInputEl(scope)?.focus(); } } }); }, getLabelProps() { return normalize.element({ ...parts.label.attrs, ...dataAttrs, dir: prop("dir"), id: getLabelId(scope) }); }, getThumbProps() { return normalize.element({ ...parts.thumb.attrs, ...dataAttrs, dir: prop("dir"), id: getThumbId(scope), "aria-hidden": true }); }, getControlProps() { return normalize.element({ ...parts.control.attrs, ...dataAttrs, dir: prop("dir"), id: getControlId(scope), "aria-hidden": true }); }, getHiddenInputProps() { return normalize.input({ id: getHiddenInputId(scope), type: "checkbox", required: prop("required"), defaultChecked: checked, disabled, "aria-labelledby": getLabelId(scope), "aria-invalid": prop("invalid"), name: prop("name"), form: prop("form"), value: prop("value"), style: visuallyHiddenStyle, onFocus() { const focusVisible2 = isFocusVisible(); send({ type: "CONTEXT.SET", context: { focused: true, focusVisible: focusVisible2 } }); }, onBlur() { send({ type: "CONTEXT.SET", context: { focused: false, focusVisible: false } }); }, onClick(event) { if (readOnly) { event.preventDefault(); return; } const checked2 = event.currentTarget.checked; send({ type: "CHECKED.SET", checked: checked2, isTrusted: true }); } }); } }; } var { not } = createGuards(); var machine = createMachine({ props({ props: props2 }) { return { defaultChecked: false, label: "switch", value: "on", ...props2 }; }, initialState() { return "ready"; }, context({ prop, bindable }) { return { checked: bindable(() => ({ defaultValue: prop("defaultChecked"), value: prop("checked"), onChange(value) { prop("onCheckedChange")?.({ checked: value }); } })), fieldsetDisabled: bindable(() => ({ defaultValue: false })), focusVisible: bindable(() => ({ defaultValue: false })), active: bindable(() => ({ defaultValue: false })), focused: bindable(() => ({ defaultValue: false })), hovered: bindable(() => ({ defaultValue: false })) }; }, computed: { isDisabled: ({ context, prop }) => prop("disabled") || context.get("fieldsetDisabled") }, watch({ track, prop, context, action }) { track([() => prop("disabled")], () => { action(["removeFocusIfNeeded"]); }); track([() => context.get("checked")], () => { action(["syncInputElement"]); }); }, effects: ["trackFormControlState", "trackPressEvent", "trackFocusVisible"], on: { "CHECKED.TOGGLE": [ { guard: not("isTrusted"), actions: ["toggleChecked", "dispatchChangeEvent"] }, { actions: ["toggleChecked"] } ], "CHECKED.SET": [ { guard: not("isTrusted"), actions: ["setChecked", "dispatchChangeEvent"] }, { actions: ["setChecked"] } ], "CONTEXT.SET": { actions: ["setContext"] } }, states: { ready: {} }, implementations: { guards: { isTrusted: ({ event }) => !!event.isTrusted }, effects: { trackPressEvent({ computed, scope, context }) { if (computed("isDisabled")) return; return trackPress({ pointerNode: getRootEl(scope), keyboardNode: getHiddenInputEl(scope), isValidKey: (event) => event.key === " ", onPress: () => context.set("active", false), onPressStart: () => context.set("active", true), onPressEnd: () => context.set("active", false) }); }, trackFocusVisible({ computed, scope }) { if (computed("isDisabled")) return; return trackFocusVisible({ root: scope.getRootNode() }); }, trackFormControlState({ context, send, scope }) { return trackFormControl(getHiddenInputEl(scope), { onFieldsetDisabledChange(disabled) { context.set("fieldsetDisabled", disabled); }, onFormReset() { const checked = context.initial("checked"); send({ type: "CHECKED.SET", checked: !!checked, src: "form-reset" }); } }); } }, actions: { setContext({ context, event }) { for (const key in event.context) { context.set(key, event.context[key]); } }, syncInputElement({ context, scope }) { const inputEl = getHiddenInputEl(scope); if (!inputEl) return; setElementChecked(inputEl, !!context.get("checked")); }, removeFocusIfNeeded({ context, prop }) { if (prop("disabled")) { context.set("focused", false); } }, setChecked({ context, event }) { context.set("checked", event.checked); }, toggleChecked({ context }) { context.set("checked", !context.get("checked")); }, dispatchChangeEvent({ context, scope }) { const inputEl = getHiddenInputEl(scope); dispatchInputCheckedEvent(inputEl, { checked: context.get("checked") }); } } } }); var props = createProps()([ "checked", "defaultChecked", "dir", "disabled", "form", "getRootNode", "id", "ids", "invalid", "label", "name", "onCheckedChange", "readOnly", "required", "value" ]); var splitProps = createSplitProps(props); export { anatomy, connect, machine, props, splitProps };