UNPKG

@zag-js/radio-group

Version:

Core logic for the radio group widget implemented as a state machine

219 lines (218 loc) • 8.03 kB
// src/radio-group.connect.ts import { dataAttr, getEventTarget, isLeftClick, isSafari, visuallyHiddenStyle } from "@zag-js/dom-query"; import { isFocusVisible } from "@zag-js/focus-visible"; import { toPx } from "@zag-js/utils"; import { parts } from "./radio-group.anatomy.mjs"; import * as dom from "./radio-group.dom.mjs"; function connect(service, normalize) { const { context, send, computed, prop, scope } = service; const groupDisabled = computed("isDisabled"); const groupInvalid = prop("invalid"); const readOnly = prop("readOnly"); function getItemState(props) { return { value: props.value, invalid: !!props.invalid || !!groupInvalid, disabled: !!props.disabled || groupDisabled, checked: context.get("value") === props.value, focused: context.get("focusedValue") === props.value, focusVisible: context.get("focusVisibleValue") === props.value, hovered: context.get("hoveredValue") === props.value, active: context.get("activeValue") === props.value }; } function getItemDataAttrs(props) { const itemState = getItemState(props); return { "data-focus": dataAttr(itemState.focused), "data-focus-visible": dataAttr(itemState.focusVisible), "data-disabled": dataAttr(itemState.disabled), "data-readonly": dataAttr(readOnly), "data-state": itemState.checked ? "checked" : "unchecked", "data-hover": dataAttr(itemState.hovered), "data-invalid": dataAttr(itemState.invalid), "data-orientation": prop("orientation"), "data-ssr": dataAttr(context.get("ssr")) }; } const focus = () => { const nodeToFocus = dom.getFirstEnabledAndCheckedInputEl(scope) ?? dom.getFirstEnabledInputEl(scope); nodeToFocus?.focus(); }; return { focus, value: context.get("value"), setValue(value) { send({ type: "SET_VALUE", value, isTrusted: false }); }, clearValue() { send({ type: "SET_VALUE", value: null, isTrusted: false }); }, getRootProps() { return normalize.element({ ...parts.root.attrs, role: "radiogroup", id: dom.getRootId(scope), "aria-labelledby": dom.getLabelId(scope), "aria-required": prop("required") || void 0, "aria-disabled": groupDisabled || void 0, "aria-readonly": readOnly || void 0, "data-orientation": prop("orientation"), "data-disabled": dataAttr(groupDisabled), "data-invalid": dataAttr(groupInvalid), "data-required": dataAttr(prop("required")), "aria-orientation": prop("orientation"), dir: prop("dir"), style: { position: "relative" } }); }, getLabelProps() { return normalize.element({ ...parts.label.attrs, dir: prop("dir"), "data-orientation": prop("orientation"), "data-disabled": dataAttr(groupDisabled), "data-invalid": dataAttr(groupInvalid), "data-required": dataAttr(prop("required")), id: dom.getLabelId(scope), onClick: focus }); }, getItemState, getItemProps(props) { const itemState = getItemState(props); return normalize.label({ ...parts.item.attrs, dir: prop("dir"), id: dom.getItemId(scope, props.value), htmlFor: dom.getItemHiddenInputId(scope, props.value), ...getItemDataAttrs(props), onPointerMove() { if (itemState.disabled) return; if (itemState.hovered) return; send({ type: "SET_HOVERED", value: props.value, hovered: true }); }, onPointerLeave() { if (itemState.disabled) return; send({ type: "SET_HOVERED", value: null }); }, onPointerDown(event) { if (itemState.disabled) return; if (!isLeftClick(event)) return; if (itemState.focused && event.pointerType === "mouse") { event.preventDefault(); } send({ type: "SET_ACTIVE", value: props.value, active: true }); }, onPointerUp() { if (itemState.disabled) return; send({ type: "SET_ACTIVE", value: null }); }, onClick() { if (!itemState.disabled && isSafari()) { dom.getItemHiddenInputEl(scope, props.value)?.focus(); } } }); }, getItemTextProps(props) { return normalize.element({ ...parts.itemText.attrs, dir: prop("dir"), id: dom.getItemLabelId(scope, props.value), ...getItemDataAttrs(props) }); }, getItemControlProps(props) { const itemState = getItemState(props); return normalize.element({ ...parts.itemControl.attrs, dir: prop("dir"), id: dom.getItemControlId(scope, props.value), "data-active": dataAttr(itemState.active), "aria-hidden": true, ...getItemDataAttrs(props) }); }, getItemHiddenInputProps(props) { const itemState = getItemState(props); return normalize.input({ "data-ownedby": dom.getRootId(scope), id: dom.getItemHiddenInputId(scope, props.value), type: "radio", name: prop("name") || prop("id"), form: prop("form"), value: props.value, required: prop("required"), "aria-labelledby": dom.getItemLabelId(scope, props.value), "aria-invalid": itemState.invalid || void 0, onClick(event) { if (readOnly) { event.preventDefault(); return; } if (event.currentTarget.checked) { send({ type: "SET_VALUE", value: props.value, isTrusted: true }); } }, onBlur() { send({ type: "SET_FOCUSED", value: null, focused: false, focusVisible: false }); }, onFocus() { const focusVisible = isFocusVisible(); send({ type: "SET_FOCUSED", value: props.value, focused: true, focusVisible }); }, onKeyDown(event) { if (event.defaultPrevented) return; if (event.key === " ") { send({ type: "SET_ACTIVE", value: props.value, active: true }); } }, onKeyUp(event) { if (event.defaultPrevented) return; if (event.key === " ") { send({ type: "SET_ACTIVE", value: null }); } }, disabled: itemState.disabled || readOnly, defaultChecked: itemState.checked, style: visuallyHiddenStyle }); }, getIndicatorProps() { const rect = context.get("indicatorRect"); const animateIndicator = context.get("animateIndicator"); return normalize.element({ id: dom.getIndicatorId(scope), ...parts.indicator.attrs, dir: prop("dir"), hidden: context.get("value") == null || isRectEmpty(rect), "data-disabled": dataAttr(groupDisabled), "data-orientation": prop("orientation"), onTransitionEnd(event) { if (getEventTarget(event) !== event.currentTarget) return; send({ type: "INDICATOR_TRANSITION_END" }); }, style: { "--transition-property": "left, top, width, height", "--left": toPx(rect?.x), "--top": toPx(rect?.y), "--width": toPx(rect?.width), "--height": toPx(rect?.height), position: "absolute", willChange: animateIndicator ? "var(--transition-property)" : "auto", transitionProperty: animateIndicator ? "var(--transition-property)" : "none", transitionDuration: animateIndicator ? "var(--transition-duration, 150ms)" : "0ms", transitionTimingFunction: "var(--transition-timing-function)", [prop("orientation") === "horizontal" ? "left" : "top"]: prop("orientation") === "horizontal" ? "var(--left)" : "var(--top)" } }); } }; } var isRectEmpty = (rect) => rect == null || rect.width === 0 && rect.height === 0 && rect.x === 0 && rect.y === 0; export { connect };