@zag-js/toggle-group
Version:
Core logic for the toggle widget implemented as a state machine
349 lines (344 loc) • 11 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/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;