@zag-js/select
Version:
Core logic for the select widget implemented as a state machine
1,158 lines (1,153 loc) • 39.3 kB
JavaScript
import { createAnatomy } from '@zag-js/anatomy';
import { ListCollection } from '@zag-js/collection';
import { raf, getInitialFocus, observeAttributes, trackFormControl, getByTypeahead, scrollIntoView, isSelfTarget, isValidTabEvent, getEventKey, getEventTarget, isEditableElement, visuallyHiddenStyle, dataAttr, ariaAttr } from '@zag-js/dom-query';
import { getPlacement, getPlacementStyles } from '@zag-js/popper';
import { addOrRemove, isEqual, createSplitProps, ensure } from '@zag-js/utils';
import { createGuards, createMachine } from '@zag-js/core';
import { trackDismissableElement } from '@zag-js/dismissable';
import { createProps } from '@zag-js/types';
// src/select.anatomy.ts
var anatomy = createAnatomy("select").parts(
"label",
"positioner",
"trigger",
"indicator",
"clearTrigger",
"item",
"itemText",
"itemIndicator",
"itemGroup",
"itemGroupLabel",
"list",
"content",
"root",
"control",
"valueText"
);
var parts = anatomy.build();
var collection = (options) => {
return new ListCollection(options);
};
collection.empty = () => {
return new ListCollection({ items: [] });
};
// src/select.dom.ts
var getRootId = (ctx) => ctx.ids?.root ?? `select:${ctx.id}`;
var getContentId = (ctx) => ctx.ids?.content ?? `select:${ctx.id}:content`;
var getTriggerId = (ctx) => ctx.ids?.trigger ?? `select:${ctx.id}:trigger`;
var getClearTriggerId = (ctx) => ctx.ids?.clearTrigger ?? `select:${ctx.id}:clear-trigger`;
var getLabelId = (ctx) => ctx.ids?.label ?? `select:${ctx.id}:label`;
var getControlId = (ctx) => ctx.ids?.control ?? `select:${ctx.id}:control`;
var getItemId = (ctx, id) => ctx.ids?.item?.(id) ?? `select:${ctx.id}:option:${id}`;
var getHiddenSelectId = (ctx) => ctx.ids?.hiddenSelect ?? `select:${ctx.id}:select`;
var getPositionerId = (ctx) => ctx.ids?.positioner ?? `select:${ctx.id}:positioner`;
var getItemGroupId = (ctx, id) => ctx.ids?.itemGroup?.(id) ?? `select:${ctx.id}:optgroup:${id}`;
var getItemGroupLabelId = (ctx, id) => ctx.ids?.itemGroupLabel?.(id) ?? `select:${ctx.id}:optgroup-label:${id}`;
var getHiddenSelectEl = (ctx) => ctx.getById(getHiddenSelectId(ctx));
var getContentEl = (ctx) => ctx.getById(getContentId(ctx));
var getTriggerEl = (ctx) => ctx.getById(getTriggerId(ctx));
var getClearTriggerEl = (ctx) => ctx.getById(getClearTriggerId(ctx));
var getPositionerEl = (ctx) => ctx.getById(getPositionerId(ctx));
var getItemEl = (ctx, id) => ctx.getById(getItemId(ctx, id));
// src/select.connect.ts
function connect(service, normalize) {
const { context, prop, scope, state, computed, send } = service;
const disabled = prop("disabled") || context.get("fieldsetDisabled");
const invalid = prop("invalid");
const readOnly = prop("readOnly");
const composite = prop("composite");
const collection2 = prop("collection");
const open = state.hasTag("open");
const focused = state.matches("focused");
const highlightedValue = context.get("highlightedValue");
const highlightedItem = context.get("highlightedItem");
const selectedItems = context.get("selectedItems");
const currentPlacement = context.get("currentPlacement");
const isTypingAhead = computed("isTypingAhead");
const interactive = computed("isInteractive");
const ariaActiveDescendant = highlightedValue ? getItemId(scope, highlightedValue) : void 0;
function getItemState(props2) {
const _disabled = collection2.getItemDisabled(props2.item);
const value = collection2.getItemValue(props2.item);
ensure(value, () => `[zag-js] No value found for item ${JSON.stringify(props2.item)}`);
return {
value,
disabled: Boolean(disabled || _disabled),
highlighted: highlightedValue === value,
selected: context.get("value").includes(value)
};
}
const popperStyles = getPlacementStyles({
...prop("positioning"),
placement: currentPlacement
});
return {
open,
focused,
empty: context.get("value").length === 0,
highlightedItem,
highlightedValue,
selectedItems,
hasSelectedItems: computed("hasSelectedItems"),
value: context.get("value"),
valueAsString: computed("valueAsString"),
collection: collection2,
multiple: !!prop("multiple"),
disabled: !!disabled,
reposition(options = {}) {
send({ type: "POSITIONING.SET", options });
},
focus() {
getTriggerEl(scope)?.focus({ preventScroll: true });
},
setOpen(nextOpen) {
const open2 = state.hasTag("open");
if (open2 === nextOpen) return;
send({ type: nextOpen ? "OPEN" : "CLOSE" });
},
selectValue(value) {
send({ type: "ITEM.SELECT", value });
},
setValue(value) {
send({ type: "VALUE.SET", value });
},
selectAll() {
send({ type: "VALUE.SET", value: collection2.getValues() });
},
setHighlightValue(value) {
send({ type: "HIGHLIGHTED_VALUE.SET", value });
},
clearHighlightValue() {
send({ type: "HIGHLIGHTED_VALUE.CLEAR" });
},
clearValue(value) {
if (value) {
send({ type: "ITEM.CLEAR", value });
} else {
send({ type: "VALUE.CLEAR" });
}
},
getItemState,
getRootProps() {
return normalize.element({
...parts.root.attrs,
dir: prop("dir"),
id: getRootId(scope),
"data-invalid": dataAttr(invalid),
"data-readonly": dataAttr(readOnly)
});
},
getLabelProps() {
return normalize.label({
dir: prop("dir"),
id: getLabelId(scope),
...parts.label.attrs,
"data-disabled": dataAttr(disabled),
"data-invalid": dataAttr(invalid),
"data-readonly": dataAttr(readOnly),
htmlFor: getHiddenSelectId(scope),
onClick(event) {
if (event.defaultPrevented) return;
if (disabled) return;
getTriggerEl(scope)?.focus({ preventScroll: true });
}
});
},
getControlProps() {
return normalize.element({
...parts.control.attrs,
dir: prop("dir"),
id: getControlId(scope),
"data-state": open ? "open" : "closed",
"data-focus": dataAttr(focused),
"data-disabled": dataAttr(disabled),
"data-invalid": dataAttr(invalid)
});
},
getValueTextProps() {
return normalize.element({
...parts.valueText.attrs,
dir: prop("dir"),
"data-disabled": dataAttr(disabled),
"data-invalid": dataAttr(invalid),
"data-focus": dataAttr(focused)
});
},
getTriggerProps() {
return normalize.button({
id: getTriggerId(scope),
disabled,
dir: prop("dir"),
type: "button",
role: "combobox",
"aria-controls": getContentId(scope),
"aria-expanded": open,
"aria-haspopup": "listbox",
"data-state": open ? "open" : "closed",
"aria-invalid": invalid,
"aria-labelledby": getLabelId(scope),
...parts.trigger.attrs,
"data-disabled": dataAttr(disabled),
"data-invalid": dataAttr(invalid),
"data-readonly": dataAttr(readOnly),
"data-placement": currentPlacement,
"data-placeholder-shown": dataAttr(!computed("hasSelectedItems")),
onClick(event) {
if (!interactive) return;
if (event.defaultPrevented) return;
send({ type: "TRIGGER.CLICK" });
},
onFocus() {
send({ type: "TRIGGER.FOCUS" });
},
onBlur() {
send({ type: "TRIGGER.BLUR" });
},
onKeyDown(event) {
if (event.defaultPrevented) return;
if (!interactive) return;
const keyMap = {
ArrowUp() {
send({ type: "TRIGGER.ARROW_UP" });
},
ArrowDown(event2) {
send({ type: event2.altKey ? "OPEN" : "TRIGGER.ARROW_DOWN" });
},
ArrowLeft() {
send({ type: "TRIGGER.ARROW_LEFT" });
},
ArrowRight() {
send({ type: "TRIGGER.ARROW_RIGHT" });
},
Home() {
send({ type: "TRIGGER.HOME" });
},
End() {
send({ type: "TRIGGER.END" });
},
Enter() {
send({ type: "TRIGGER.ENTER" });
},
Space(event2) {
if (isTypingAhead) {
send({ type: "TRIGGER.TYPEAHEAD", key: event2.key });
} else {
send({ type: "TRIGGER.ENTER" });
}
}
};
const exec = keyMap[getEventKey(event, {
dir: prop("dir"),
orientation: "vertical"
})];
if (exec) {
exec(event);
event.preventDefault();
return;
}
if (getByTypeahead.isValidEvent(event)) {
send({ type: "TRIGGER.TYPEAHEAD", key: event.key });
event.preventDefault();
}
}
});
},
getIndicatorProps() {
return normalize.element({
...parts.indicator.attrs,
dir: prop("dir"),
"aria-hidden": true,
"data-state": open ? "open" : "closed",
"data-disabled": dataAttr(disabled),
"data-invalid": dataAttr(invalid),
"data-readonly": dataAttr(readOnly)
});
},
getItemProps(props2) {
const itemState = getItemState(props2);
return normalize.element({
id: getItemId(scope, itemState.value),
role: "option",
...parts.item.attrs,
dir: prop("dir"),
"data-value": itemState.value,
"aria-selected": itemState.selected,
"data-state": itemState.selected ? "checked" : "unchecked",
"data-highlighted": dataAttr(itemState.highlighted),
"data-disabled": dataAttr(itemState.disabled),
"aria-disabled": ariaAttr(itemState.disabled),
onPointerMove(event) {
if (itemState.disabled || event.pointerType !== "mouse") return;
if (itemState.value === highlightedValue) return;
send({ type: "ITEM.POINTER_MOVE", value: itemState.value });
},
onClick(event) {
if (event.defaultPrevented) return;
if (itemState.disabled) return;
send({ type: "ITEM.CLICK", src: "pointerup", value: itemState.value });
},
onPointerLeave(event) {
if (itemState.disabled) return;
if (props2.persistFocus) return;
if (event.pointerType !== "mouse") return;
const pointerMoved = service.event.previous()?.type.includes("POINTER");
if (!pointerMoved) return;
send({ type: "ITEM.POINTER_LEAVE" });
}
});
},
getItemTextProps(props2) {
const itemState = getItemState(props2);
return normalize.element({
...parts.itemText.attrs,
"data-state": itemState.selected ? "checked" : "unchecked",
"data-disabled": dataAttr(itemState.disabled),
"data-highlighted": dataAttr(itemState.highlighted)
});
},
getItemIndicatorProps(props2) {
const itemState = getItemState(props2);
return normalize.element({
"aria-hidden": true,
...parts.itemIndicator.attrs,
"data-state": itemState.selected ? "checked" : "unchecked",
hidden: !itemState.selected
});
},
getItemGroupLabelProps(props2) {
const { htmlFor } = props2;
return normalize.element({
...parts.itemGroupLabel.attrs,
id: getItemGroupLabelId(scope, htmlFor),
dir: prop("dir"),
role: "presentation"
});
},
getItemGroupProps(props2) {
const { id } = props2;
return normalize.element({
...parts.itemGroup.attrs,
"data-disabled": dataAttr(disabled),
id: getItemGroupId(scope, id),
"aria-labelledby": getItemGroupLabelId(scope, id),
role: "group",
dir: prop("dir")
});
},
getClearTriggerProps() {
return normalize.button({
...parts.clearTrigger.attrs,
id: getClearTriggerId(scope),
type: "button",
"aria-label": "Clear value",
"data-invalid": dataAttr(invalid),
disabled,
hidden: !computed("hasSelectedItems"),
dir: prop("dir"),
onClick(event) {
if (event.defaultPrevented) return;
send({ type: "CLEAR.CLICK" });
}
});
},
getHiddenSelectProps() {
const value = context.get("value");
const defaultValue = prop("multiple") ? value : value?.[0];
return normalize.select({
name: prop("name"),
form: prop("form"),
disabled,
multiple: prop("multiple"),
required: prop("required"),
"aria-hidden": true,
id: getHiddenSelectId(scope),
defaultValue,
style: visuallyHiddenStyle,
tabIndex: -1,
// Some browser extensions will focus the hidden select.
// Let's forward the focus to the trigger.
onFocus() {
getTriggerEl(scope)?.focus({ preventScroll: true });
},
"aria-labelledby": getLabelId(scope)
});
},
getPositionerProps() {
return normalize.element({
...parts.positioner.attrs,
dir: prop("dir"),
id: getPositionerId(scope),
style: popperStyles.floating
});
},
getContentProps() {
return normalize.element({
hidden: !open,
dir: prop("dir"),
id: getContentId(scope),
role: composite ? "listbox" : "dialog",
...parts.content.attrs,
"data-state": open ? "open" : "closed",
"data-placement": currentPlacement,
"data-activedescendant": ariaActiveDescendant,
"aria-activedescendant": composite ? ariaActiveDescendant : void 0,
"aria-multiselectable": prop("multiple") && composite ? true : void 0,
"aria-labelledby": getLabelId(scope),
tabIndex: 0,
onKeyDown(event) {
if (!interactive) return;
if (!isSelfTarget(event)) return;
if (event.key === "Tab") {
const valid = isValidTabEvent(event);
if (!valid) {
event.preventDefault();
return;
}
}
const keyMap = {
ArrowUp() {
send({ type: "CONTENT.ARROW_UP" });
},
ArrowDown() {
send({ type: "CONTENT.ARROW_DOWN" });
},
Home() {
send({ type: "CONTENT.HOME" });
},
End() {
send({ type: "CONTENT.END" });
},
Enter() {
send({ type: "ITEM.CLICK", src: "keydown.enter" });
},
Space(event2) {
if (isTypingAhead) {
send({ type: "CONTENT.TYPEAHEAD", key: event2.key });
} else {
keyMap.Enter?.(event2);
}
}
};
const exec = keyMap[getEventKey(event)];
if (exec) {
exec(event);
event.preventDefault();
return;
}
const target = getEventTarget(event);
if (isEditableElement(target)) {
return;
}
if (getByTypeahead.isValidEvent(event)) {
send({ type: "CONTENT.TYPEAHEAD", key: event.key });
event.preventDefault();
}
}
});
},
getListProps() {
return normalize.element({
...parts.list.attrs,
tabIndex: 0,
role: !composite ? "listbox" : void 0,
"aria-labelledby": getTriggerId(scope),
"aria-activedescendant": !composite ? ariaActiveDescendant : void 0,
"aria-multiselectable": !composite && prop("multiple") ? true : void 0
});
}
};
}
var { and, not, or } = createGuards();
var machine = createMachine({
props({ props: props2 }) {
return {
loopFocus: false,
closeOnSelect: !props2.multiple,
composite: true,
defaultValue: [],
...props2,
collection: props2.collection ?? collection.empty(),
positioning: {
placement: "bottom-start",
gutter: 8,
...props2.positioning
}
};
},
context({ prop, bindable }) {
return {
value: bindable(() => ({
defaultValue: prop("defaultValue"),
value: prop("value"),
isEqual,
onChange(value) {
const items = prop("collection").findMany(value);
return prop("onValueChange")?.({ value, items });
}
})),
highlightedValue: bindable(() => ({
defaultValue: prop("defaultHighlightedValue") || null,
value: prop("highlightedValue"),
onChange(value) {
prop("onHighlightChange")?.({
highlightedValue: value,
highlightedItem: prop("collection").find(value),
highlightedIndex: prop("collection").indexOf(value)
});
}
})),
currentPlacement: bindable(() => ({
defaultValue: void 0
})),
fieldsetDisabled: bindable(() => ({
defaultValue: false
})),
highlightedItem: bindable(() => ({
defaultValue: null
})),
selectedItems: bindable(() => {
const value = prop("value") ?? prop("defaultValue") ?? [];
const items = prop("collection").findMany(value);
return { defaultValue: items };
})
};
},
refs() {
return {
typeahead: { ...getByTypeahead.defaultOptions }
};
},
computed: {
hasSelectedItems: ({ context }) => context.get("value").length > 0,
isTypingAhead: ({ refs }) => refs.get("typeahead").keysSoFar !== "",
isDisabled: ({ prop, context }) => !!prop("disabled") || !!context.get("fieldsetDisabled"),
isInteractive: ({ prop }) => !(prop("disabled") || prop("readOnly")),
valueAsString: ({ context, prop }) => prop("collection").stringifyItems(context.get("selectedItems"))
},
initialState({ prop }) {
const open = prop("open") || prop("defaultOpen");
return open ? "open" : "idle";
},
entry: ["syncSelectElement"],
watch({ context, prop, track, action }) {
track([() => context.get("value").toString()], () => {
action(["syncSelectedItems", "syncSelectElement", "dispatchChangeEvent"]);
});
track([() => prop("open")], () => {
action(["toggleVisibility"]);
});
track([() => context.get("highlightedValue")], () => {
action(["syncHighlightedItem"]);
});
track([() => prop("collection").toString()], () => {
action(["syncCollection"]);
});
},
on: {
"HIGHLIGHTED_VALUE.SET": {
actions: ["setHighlightedItem"]
},
"HIGHLIGHTED_VALUE.CLEAR": {
actions: ["clearHighlightedItem"]
},
"ITEM.SELECT": {
actions: ["selectItem"]
},
"ITEM.CLEAR": {
actions: ["clearItem"]
},
"VALUE.SET": {
actions: ["setSelectedItems"]
},
"VALUE.CLEAR": {
actions: ["clearSelectedItems"]
},
"CLEAR.CLICK": {
actions: ["clearSelectedItems", "focusTriggerEl"]
}
},
effects: ["trackFormControlState"],
states: {
idle: {
tags: ["closed"],
on: {
"CONTROLLED.OPEN": [
{
guard: "isTriggerClickEvent",
target: "open",
actions: ["setInitialFocus", "highlightFirstSelectedItem"]
},
{
target: "open",
actions: ["setInitialFocus"]
}
],
"TRIGGER.CLICK": [
{
guard: "isOpenControlled",
actions: ["invokeOnOpen"]
},
{
target: "open",
actions: ["invokeOnOpen", "setInitialFocus", "highlightFirstSelectedItem"]
}
],
"TRIGGER.FOCUS": {
target: "focused"
},
OPEN: [
{
guard: "isOpenControlled",
actions: ["invokeOnOpen"]
},
{
target: "open",
actions: ["setInitialFocus", "invokeOnOpen"]
}
]
}
},
focused: {
tags: ["closed"],
on: {
"CONTROLLED.OPEN": [
{
guard: "isTriggerClickEvent",
target: "open",
actions: ["setInitialFocus", "highlightFirstSelectedItem"]
},
{
guard: "isTriggerArrowUpEvent",
target: "open",
actions: ["setInitialFocus", "highlightComputedLastItem"]
},
{
guard: or("isTriggerArrowDownEvent", "isTriggerEnterEvent"),
target: "open",
actions: ["setInitialFocus", "highlightComputedFirstItem"]
},
{
target: "open",
actions: ["setInitialFocus"]
}
],
OPEN: [
{
guard: "isOpenControlled",
actions: ["invokeOnOpen"]
},
{
target: "open",
actions: ["setInitialFocus", "invokeOnOpen"]
}
],
"TRIGGER.BLUR": {
target: "idle"
},
"TRIGGER.CLICK": [
{
guard: "isOpenControlled",
actions: ["invokeOnOpen"]
},
{
target: "open",
actions: ["setInitialFocus", "invokeOnOpen", "highlightFirstSelectedItem"]
}
],
"TRIGGER.ENTER": [
{
guard: "isOpenControlled",
actions: ["invokeOnOpen"]
},
{
target: "open",
actions: ["setInitialFocus", "invokeOnOpen", "highlightComputedFirstItem"]
}
],
"TRIGGER.ARROW_UP": [
{
guard: "isOpenControlled",
actions: ["invokeOnOpen"]
},
{
target: "open",
actions: ["setInitialFocus", "invokeOnOpen", "highlightComputedLastItem"]
}
],
"TRIGGER.ARROW_DOWN": [
{
guard: "isOpenControlled",
actions: ["invokeOnOpen"]
},
{
target: "open",
actions: ["setInitialFocus", "invokeOnOpen", "highlightComputedFirstItem"]
}
],
"TRIGGER.ARROW_LEFT": [
{
guard: and(not("multiple"), "hasSelectedItems"),
actions: ["selectPreviousItem"]
},
{
guard: not("multiple"),
actions: ["selectLastItem"]
}
],
"TRIGGER.ARROW_RIGHT": [
{
guard: and(not("multiple"), "hasSelectedItems"),
actions: ["selectNextItem"]
},
{
guard: not("multiple"),
actions: ["selectFirstItem"]
}
],
"TRIGGER.HOME": {
guard: not("multiple"),
actions: ["selectFirstItem"]
},
"TRIGGER.END": {
guard: not("multiple"),
actions: ["selectLastItem"]
},
"TRIGGER.TYPEAHEAD": {
guard: not("multiple"),
actions: ["selectMatchingItem"]
}
}
},
open: {
tags: ["open"],
exit: ["scrollContentToTop"],
effects: ["trackDismissableElement", "computePlacement", "scrollToHighlightedItem"],
on: {
"CONTROLLED.CLOSE": [
{
guard: "restoreFocus",
target: "focused",
actions: ["focusTriggerEl", "clearHighlightedItem"]
},
{
target: "idle",
actions: ["clearHighlightedItem"]
}
],
CLOSE: [
{
guard: "isOpenControlled",
actions: ["invokeOnClose"]
},
{
guard: "restoreFocus",
target: "focused",
actions: ["invokeOnClose", "focusTriggerEl", "clearHighlightedItem"]
},
{
target: "idle",
actions: ["invokeOnClose", "clearHighlightedItem"]
}
],
"TRIGGER.CLICK": [
{
guard: "isOpenControlled",
actions: ["invokeOnClose"]
},
{
target: "focused",
actions: ["invokeOnClose", "clearHighlightedItem"]
}
],
"ITEM.CLICK": [
{
guard: and("closeOnSelect", "isOpenControlled"),
actions: ["selectHighlightedItem", "invokeOnClose"]
},
{
guard: "closeOnSelect",
target: "focused",
actions: ["selectHighlightedItem", "invokeOnClose", "focusTriggerEl", "clearHighlightedItem"]
},
{
actions: ["selectHighlightedItem"]
}
],
"CONTENT.HOME": {
actions: ["highlightFirstItem"]
},
"CONTENT.END": {
actions: ["highlightLastItem"]
},
"CONTENT.ARROW_DOWN": [
{
guard: and("hasHighlightedItem", "loop", "isLastItemHighlighted"),
actions: ["highlightFirstItem"]
},
{
guard: "hasHighlightedItem",
actions: ["highlightNextItem"]
},
{
actions: ["highlightFirstItem"]
}
],
"CONTENT.ARROW_UP": [
{
guard: and("hasHighlightedItem", "loop", "isFirstItemHighlighted"),
actions: ["highlightLastItem"]
},
{
guard: "hasHighlightedItem",
actions: ["highlightPreviousItem"]
},
{
actions: ["highlightLastItem"]
}
],
"CONTENT.TYPEAHEAD": {
actions: ["highlightMatchingItem"]
},
"ITEM.POINTER_MOVE": {
actions: ["highlightItem"]
},
"ITEM.POINTER_LEAVE": {
actions: ["clearHighlightedItem"]
},
"POSITIONING.SET": {
actions: ["reposition"]
}
}
}
},
implementations: {
guards: {
loop: ({ prop }) => !!prop("loopFocus"),
multiple: ({ prop }) => !!prop("multiple"),
hasSelectedItems: ({ computed }) => !!computed("hasSelectedItems"),
hasHighlightedItem: ({ context }) => context.get("highlightedValue") != null,
isFirstItemHighlighted: ({ context, prop }) => context.get("highlightedValue") === prop("collection").firstValue,
isLastItemHighlighted: ({ context, prop }) => context.get("highlightedValue") === prop("collection").lastValue,
closeOnSelect: ({ prop, event }) => !!(event.closeOnSelect ?? prop("closeOnSelect")),
restoreFocus: ({ event }) => restoreFocusFn(event),
// guard assertions (for controlled mode)
isOpenControlled: ({ prop }) => prop("open") !== void 0,
isTriggerClickEvent: ({ event }) => event.previousEvent?.type === "TRIGGER.CLICK",
isTriggerEnterEvent: ({ event }) => event.previousEvent?.type === "TRIGGER.ENTER",
isTriggerArrowUpEvent: ({ event }) => event.previousEvent?.type === "TRIGGER.ARROW_UP",
isTriggerArrowDownEvent: ({ event }) => event.previousEvent?.type === "TRIGGER.ARROW_DOWN"
},
effects: {
trackFormControlState({ context, scope }) {
return trackFormControl(getHiddenSelectEl(scope), {
onFieldsetDisabledChange(disabled) {
context.set("fieldsetDisabled", disabled);
},
onFormReset() {
const value = context.initial("value");
context.set("value", value);
}
});
},
trackDismissableElement({ scope, send, prop }) {
const contentEl = () => getContentEl(scope);
let restoreFocus = true;
return trackDismissableElement(contentEl, {
defer: true,
exclude: [getTriggerEl(scope), getClearTriggerEl(scope)],
onFocusOutside: prop("onFocusOutside"),
onPointerDownOutside: prop("onPointerDownOutside"),
onInteractOutside(event) {
prop("onInteractOutside")?.(event);
restoreFocus = !(event.detail.focusable || event.detail.contextmenu);
},
onDismiss() {
send({ type: "CLOSE", src: "interact-outside", restoreFocus });
}
});
},
computePlacement({ context, prop, scope }) {
const positioning = prop("positioning");
context.set("currentPlacement", positioning.placement);
const triggerEl = () => getTriggerEl(scope);
const positionerEl = () => getPositionerEl(scope);
return getPlacement(triggerEl, positionerEl, {
defer: true,
...positioning,
onComplete(data) {
context.set("currentPlacement", data.placement);
}
});
},
scrollToHighlightedItem({ context, prop, scope, event }) {
const exec = (immediate) => {
const highlightedValue = context.get("highlightedValue");
if (highlightedValue == null) return;
if (event.current().type.includes("POINTER")) return;
const optionEl = getItemEl(scope, highlightedValue);
const contentEl2 = getContentEl(scope);
const scrollToIndexFn = prop("scrollToIndexFn");
if (scrollToIndexFn) {
const highlightedIndex = prop("collection").indexOf(highlightedValue);
scrollToIndexFn?.({ index: highlightedIndex, immediate });
return;
}
scrollIntoView(optionEl, { rootEl: contentEl2, block: "nearest" });
};
raf(() => exec(true));
const contentEl = () => getContentEl(scope);
return observeAttributes(contentEl, {
defer: true,
attributes: ["data-activedescendant"],
callback() {
exec(false);
}
});
}
},
actions: {
reposition({ context, prop, scope, event }) {
const positionerEl = () => getPositionerEl(scope);
getPlacement(getTriggerEl(scope), positionerEl, {
...prop("positioning"),
...event.options,
defer: true,
listeners: false,
onComplete(data) {
context.set("currentPlacement", data.placement);
}
});
},
toggleVisibility({ send, prop, event }) {
send({ type: prop("open") ? "CONTROLLED.OPEN" : "CONTROLLED.CLOSE", previousEvent: event });
},
highlightPreviousItem({ context, prop }) {
const highlightedValue = context.get("highlightedValue");
if (highlightedValue == null) return;
const value = prop("collection").getPreviousValue(highlightedValue, 1, prop("loopFocus"));
if (value == null) return;
context.set("highlightedValue", value);
},
highlightNextItem({ context, prop }) {
const highlightedValue = context.get("highlightedValue");
if (highlightedValue == null) return;
const value = prop("collection").getNextValue(highlightedValue, 1, prop("loopFocus"));
if (value == null) return;
context.set("highlightedValue", value);
},
highlightFirstItem({ context, prop }) {
const value = prop("collection").firstValue;
context.set("highlightedValue", value);
},
highlightLastItem({ context, prop }) {
const value = prop("collection").lastValue;
context.set("highlightedValue", value);
},
setInitialFocus({ scope }) {
raf(() => {
const element = getInitialFocus({
root: getContentEl(scope)
});
element?.focus({ preventScroll: true });
});
},
focusTriggerEl({ event, scope }) {
if (!restoreFocusFn(event)) return;
raf(() => {
const element = getTriggerEl(scope);
element?.focus({ preventScroll: true });
});
},
selectHighlightedItem({ context, prop, event }) {
let value = event.value ?? context.get("highlightedValue");
if (value == null || !prop("collection").has(value)) return;
prop("onSelect")?.({ value });
const nullable = prop("deselectable") && !prop("multiple") && context.get("value").includes(value);
value = nullable ? null : value;
context.set("value", (prev) => {
if (value == null) return [];
if (prop("multiple")) return addOrRemove(prev, value);
return [value];
});
},
highlightComputedFirstItem({ context, prop, computed }) {
const collection2 = prop("collection");
const value = computed("hasSelectedItems") ? collection2.sort(context.get("value"))[0] : collection2.firstValue;
context.set("highlightedValue", value);
},
highlightComputedLastItem({ context, prop, computed }) {
const collection2 = prop("collection");
const value = computed("hasSelectedItems") ? collection2.sort(context.get("value"))[0] : collection2.lastValue;
context.set("highlightedValue", value);
},
highlightFirstSelectedItem({ context, prop, computed }) {
if (!computed("hasSelectedItems")) return;
const value = prop("collection").sort(context.get("value"))[0];
context.set("highlightedValue", value);
},
highlightItem({ context, event }) {
context.set("highlightedValue", event.value);
},
highlightMatchingItem({ context, prop, event, refs }) {
const value = prop("collection").search(event.key, {
state: refs.get("typeahead"),
currentValue: context.get("highlightedValue")
});
if (value == null) return;
context.set("highlightedValue", value);
},
setHighlightedItem({ context, event }) {
context.set("highlightedValue", event.value);
},
clearHighlightedItem({ context }) {
context.set("highlightedValue", null);
},
selectItem({ context, prop, event }) {
prop("onSelect")?.({ value: event.value });
const nullable = prop("deselectable") && !prop("multiple") && context.get("value").includes(event.value);
const value = nullable ? null : event.value;
context.set("value", (prev) => {
if (value == null) return [];
if (prop("multiple")) return addOrRemove(prev, value);
return [value];
});
},
clearItem({ context, event }) {
context.set("value", (prev) => prev.filter((v) => v !== event.value));
},
setSelectedItems({ context, event }) {
context.set("value", event.value);
},
clearSelectedItems({ context }) {
context.set("value", []);
},
selectPreviousItem({ context, prop }) {
const [firstItem] = context.get("value");
const value = prop("collection").getPreviousValue(firstItem);
if (value) context.set("value", [value]);
},
selectNextItem({ context, prop }) {
const [firstItem] = context.get("value");
const value = prop("collection").getNextValue(firstItem);
if (value) context.set("value", [value]);
},
selectFirstItem({ context, prop }) {
const value = prop("collection").firstValue;
if (value) context.set("value", [value]);
},
selectLastItem({ context, prop }) {
const value = prop("collection").lastValue;
if (value) context.set("value", [value]);
},
selectMatchingItem({ context, prop, event, refs }) {
const value = prop("collection").search(event.key, {
state: refs.get("typeahead"),
currentValue: context.get("value")[0]
});
if (value == null) return;
context.set("value", [value]);
},
scrollContentToTop({ prop, scope }) {
if (prop("scrollToIndexFn")) {
prop("scrollToIndexFn")?.({ index: 0, immediate: true });
} else {
getContentEl(scope)?.scrollTo(0, 0);
}
},
invokeOnOpen({ prop }) {
prop("onOpenChange")?.({ open: true });
},
invokeOnClose({ prop }) {
prop("onOpenChange")?.({ open: false });
},
syncSelectElement({ context, prop, scope }) {
const selectEl = getHiddenSelectEl(scope);
if (!selectEl) return;
if (context.get("value").length === 0 && !prop("multiple")) {
selectEl.selectedIndex = -1;
return;
}
for (const option of selectEl.options) {
option.selected = context.get("value").includes(option.value);
}
},
syncCollection({ context, prop }) {
const collection2 = prop("collection");
const highlightedItem = collection2.find(context.get("highlightedValue"));
if (highlightedItem) context.set("highlightedItem", highlightedItem);
const selectedItems = collection2.findMany(context.get("value"));
context.set("selectedItems", selectedItems);
},
syncSelectedItems({ context, prop }) {
const collection2 = prop("collection");
const prevSelectedItems = context.get("selectedItems");
const value = context.get("value");
const selectedItems = value.map((value2) => {
const item = prevSelectedItems.find((item2) => collection2.getItemValue(item2) === value2);
return item || collection2.find(value2);
});
context.set("selectedItems", selectedItems);
},
syncHighlightedItem({ context, prop }) {
const collection2 = prop("collection");
const highlightedValue = context.get("highlightedValue");
const highlightedItem = highlightedValue ? collection2.find(highlightedValue) : null;
context.set("highlightedItem", highlightedItem);
},
dispatchChangeEvent({ scope }) {
queueMicrotask(() => {
const node = getHiddenSelectEl(scope);
if (!node) return;
const win = scope.getWin();
const changeEvent = new win.Event("change", { bubbles: true, composed: true });
node.dispatchEvent(changeEvent);
});
}
}
}
});
function restoreFocusFn(event) {
const v = event.restoreFocus ?? event.previousEvent?.restoreFocus;
return v == null || !!v;
}
var props = createProps()([
"closeOnSelect",
"collection",
"composite",
"defaultHighlightedValue",
"defaultOpen",
"defaultValue",
"deselectable",
"dir",
"disabled",
"form",
"getRootNode",
"highlightedValue",
"id",
"ids",
"invalid",
"loopFocus",
"multiple",
"name",
"onFocusOutside",
"onHighlightChange",
"onInteractOutside",
"onOpenChange",
"onPointerDownOutside",
"onSelect",
"onValueChange",
"open",
"positioning",
"readOnly",
"required",
"scrollToIndexFn",
"value"
]);
var splitProps = createSplitProps(props);
var itemProps = createProps()(["item", "persistFocus"]);
var splitItemProps = createSplitProps(itemProps);
var itemGroupProps = createProps()(["id"]);
var splitItemGroupProps = createSplitProps(itemGroupProps);
var itemGroupLabelProps = createProps()(["htmlFor"]);
var splitItemGroupLabelProps = createSplitProps(itemGroupLabelProps);
export { anatomy, collection, connect, itemGroupLabelProps, itemGroupProps, itemProps, machine, props, splitItemGroupLabelProps, splitItemGroupProps, splitItemProps, splitProps };