@zag-js/listbox
Version:
Core logic for the listbox widget implemented as a state machine
440 lines (438 loc) • 16.3 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/listbox.machine.ts
var listbox_machine_exports = {};
__export(listbox_machine_exports, {
machine: () => machine
});
module.exports = __toCommonJS(listbox_machine_exports);
var import_collection = require("@zag-js/collection");
var import_core = require("@zag-js/core");
var import_dom_query = require("@zag-js/dom-query");
var import_focus_visible = require("@zag-js/focus-visible");
var import_utils = require("@zag-js/utils");
var import_listbox = require("./listbox.collection.js");
var dom = __toESM(require("./listbox.dom.js"));
var { guards, createMachine } = (0, import_core.setup)();
var { or } = guards;
var machine = createMachine({
props({ props }) {
return {
loopFocus: false,
composite: true,
defaultValue: [],
multiple: false,
typeahead: true,
collection: import_listbox.collection.empty(),
orientation: "vertical",
selectionMode: "single",
...props
};
},
context({ prop, bindable, getContext }) {
const initialValue = prop("value") ?? prop("defaultValue") ?? [];
const initialSelectedItems = prop("collection").findMany(initialValue);
return {
value: bindable(() => ({
defaultValue: prop("defaultValue"),
value: prop("value"),
isEqual: import_utils.isEqual,
onChange(value) {
const context = getContext();
const collection2 = prop("collection");
const selectedItemMap = context.get("selectedItemMap");
const proposed = (0, import_collection.deriveSelectionState)({
values: value,
collection: collection2,
selectedItemMap
});
const effectiveValue = prop("value") ?? value;
const effective = effectiveValue === value ? proposed : (0, import_collection.deriveSelectionState)({
values: effectiveValue,
collection: collection2,
selectedItemMap: proposed.nextSelectedItemMap
});
context.set("selectedItemMap", effective.nextSelectedItemMap);
return prop("onValueChange")?.({ value, items: proposed.selectedItems });
}
})),
highlightedValue: bindable(() => ({
defaultValue: prop("defaultHighlightedValue") || null,
value: prop("highlightedValue"),
sync: true,
onChange(value) {
prop("onHighlightChange")?.({
highlightedValue: value,
highlightedItem: prop("collection").find(value),
highlightedIndex: prop("collection").indexOf(value)
});
}
})),
highlightedItem: bindable(() => ({
defaultValue: null
})),
selectedItemMap: bindable(() => {
return {
defaultValue: (0, import_collection.createSelectedItemMap)({
selectedItems: initialSelectedItems,
collection: prop("collection")
})
};
}),
focused: bindable(() => ({
sync: true,
defaultValue: false
}))
};
},
refs() {
return {
typeahead: { ...import_dom_query.getByTypeahead.defaultOptions },
focusVisible: false,
inputState: { autoHighlight: false, focused: false }
};
},
computed: {
hasSelectedItems: ({ context }) => context.get("value").length > 0,
isTypingAhead: ({ refs }) => refs.get("typeahead").keysSoFar !== "",
isInteractive: ({ prop }) => !prop("disabled"),
selection: ({ context, prop }) => {
const selection = new import_collection.Selection(context.get("value"));
selection.selectionMode = prop("selectionMode");
selection.deselectable = !!prop("deselectable");
return selection;
},
multiple: ({ prop }) => prop("selectionMode") === "multiple" || prop("selectionMode") === "extended",
selectedItems: ({ context, prop }) => (0, import_collection.resolveSelectedItems)({
values: context.get("value"),
collection: prop("collection"),
selectedItemMap: context.get("selectedItemMap")
}),
valueAsString: ({ computed, prop }) => prop("collection").stringifyItems(computed("selectedItems"))
},
initialState() {
return "idle";
},
watch({ context, prop, track, action }) {
track([() => context.get("value").toString()], () => {
action(["syncSelectedItems"]);
});
track([() => context.get("highlightedValue")], () => {
action(["syncHighlightedItem"]);
});
track([() => prop("collection").toString()], () => {
action(["syncHighlightedValue"]);
});
},
effects: ["trackFocusVisible"],
on: {
"HIGHLIGHTED_VALUE.SET": {
actions: ["setHighlightedItem"]
},
"ITEM.SELECT": {
actions: ["selectItem"]
},
"ITEM.CLEAR": {
actions: ["clearItem"]
},
"VALUE.SET": {
actions: ["setSelectedItems"]
},
"VALUE.CLEAR": {
actions: ["clearSelectedItems"]
},
"HIGHLIGHT.FIRST": {
actions: ["highlightFirstValue"]
},
"HIGHLIGHT.LAST": {
actions: ["highlightLastValue"]
},
"HIGHLIGHT.NEXT": {
actions: ["highlightNextValue"]
},
"HIGHLIGHT.PREV": {
actions: ["highlightPreviousValue"]
}
},
states: {
idle: {
effects: ["scrollToHighlightedItem"],
on: {
"INPUT.FOCUS": {
actions: ["setFocused", "setInputState"]
},
"CONTENT.FOCUS": [
{
guard: or("hasSelectedValue", "hasHighlightedValue"),
actions: ["setFocused"]
},
{
actions: ["setFocused", "setDefaultHighlightedValue"]
}
],
"CONTENT.BLUR": {
actions: ["clearFocused", "clearInputState"]
},
"ITEM.CLICK": {
actions: ["setHighlightedItem", "selectHighlightedItem"]
},
"CONTENT.TYPEAHEAD": {
actions: ["setFocused", "highlightMatchingItem"]
},
"ITEM.POINTER_MOVE": {
actions: ["highlightItem"]
},
"ITEM.POINTER_LEAVE": {
actions: ["clearHighlightedItem"]
},
NAVIGATE: {
actions: ["setFocused", "setHighlightedItem", "selectWithKeyboard"]
}
}
}
},
implementations: {
guards: {
hasSelectedValue: ({ context }) => context.get("value").length > 0,
hasHighlightedValue: ({ context }) => context.get("highlightedValue") != null
},
effects: {
trackFocusVisible: ({ scope, refs }) => {
return (0, import_focus_visible.trackFocusVisible)({
root: scope.getRootNode?.(),
onChange(details) {
refs.set("focusVisible", details.isFocusVisible);
}
});
},
scrollToHighlightedItem({ context, prop, scope }) {
const exec = (immediate) => {
const highlightedValue = context.get("highlightedValue");
if (highlightedValue == null) return;
const modality = (0, import_focus_visible.getInteractionModality)();
if (modality === "pointer") return;
const contentEl2 = dom.getContentEl(scope);
const scrollToIndexFn = prop("scrollToIndexFn");
if (scrollToIndexFn) {
const highlightedIndex = prop("collection").indexOf(highlightedValue);
scrollToIndexFn?.({
index: highlightedIndex,
immediate,
getElement() {
return dom.getItemEl(scope, highlightedValue);
}
});
return;
}
const itemEl = dom.getItemEl(scope, highlightedValue);
(0, import_dom_query.scrollIntoView)(itemEl, { rootEl: contentEl2, block: "nearest" });
};
(0, import_dom_query.raf)(() => {
(0, import_focus_visible.setInteractionModality)("virtual");
exec(true);
});
const contentEl = () => dom.getContentEl(scope);
return (0, import_dom_query.observeAttributes)(contentEl, {
defer: true,
attributes: ["data-activedescendant"],
callback() {
exec(false);
}
});
}
},
actions: {
selectHighlightedItem({ context, prop, event, computed }) {
const value = event.value ?? context.get("highlightedValue");
const collection2 = prop("collection");
if (value == null || !collection2.has(value)) return;
const selection = computed("selection");
if (event.shiftKey && computed("multiple") && event.anchorValue) {
const next = selection.extendSelection(collection2, event.anchorValue, value);
invokeOnSelect(selection, next, prop("onSelect"));
context.set("value", Array.from(next));
} else {
const next = selection.select(collection2, value, event.metaKey);
invokeOnSelect(selection, next, prop("onSelect"));
context.set("value", Array.from(next));
}
},
selectWithKeyboard({ context, prop, event, computed }) {
const selection = computed("selection");
const collection2 = prop("collection");
if (event.shiftKey && computed("multiple") && event.anchorValue) {
const next = selection.extendSelection(collection2, event.anchorValue, event.value);
invokeOnSelect(selection, next, prop("onSelect"));
context.set("value", Array.from(next));
return;
}
if (prop("selectOnHighlight")) {
const next = selection.replaceSelection(collection2, event.value);
invokeOnSelect(selection, next, prop("onSelect"));
context.set("value", Array.from(next));
}
},
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);
},
highlightFirstValue({ context, prop }) {
context.set("highlightedValue", prop("collection").firstValue ?? null);
},
highlightLastValue({ context, prop }) {
context.set("highlightedValue", prop("collection").lastValue ?? null);
},
highlightNextValue({ context, prop }) {
const collection2 = prop("collection");
const highlightedValue = context.get("highlightedValue");
let nextValue = null;
if ((0, import_collection.isGridCollection)(collection2) && highlightedValue) {
nextValue = collection2.getNextRowValue(highlightedValue);
} else if (highlightedValue) {
nextValue = collection2.getNextValue(highlightedValue);
}
if (!nextValue && (prop("loopFocus") || !highlightedValue)) {
nextValue = collection2.firstValue;
}
if (!nextValue) return;
context.set("highlightedValue", nextValue);
},
highlightPreviousValue({ context, prop }) {
const collection2 = prop("collection");
const highlightedValue = context.get("highlightedValue");
let nextValue = null;
if ((0, import_collection.isGridCollection)(collection2) && highlightedValue) {
nextValue = collection2.getPreviousRowValue(highlightedValue);
} else if (highlightedValue) {
nextValue = collection2.getPreviousValue(highlightedValue);
}
if (!nextValue && (prop("loopFocus") || !highlightedValue)) {
nextValue = collection2.lastValue;
}
if (!nextValue) return;
context.set("highlightedValue", nextValue);
},
clearHighlightedItem({ context }) {
context.set("highlightedValue", null);
},
selectItem({ context, prop, event, computed }) {
const collection2 = prop("collection");
const selection = computed("selection");
const next = selection.select(collection2, event.value);
invokeOnSelect(selection, next, prop("onSelect"));
context.set("value", Array.from(next));
},
clearItem({ context, event, computed }) {
const selection = computed("selection");
const value = selection.deselect(event.value);
context.set("value", Array.from(value));
},
setSelectedItems({ context, event }) {
context.set("value", event.value);
},
clearSelectedItems({ context }) {
context.set("value", []);
},
syncSelectedItems({ context, prop }) {
const next = (0, import_collection.deriveSelectionState)({
values: context.get("value"),
collection: prop("collection"),
selectedItemMap: context.get("selectedItemMap")
});
context.set("selectedItemMap", next.nextSelectedItemMap);
},
syncHighlightedItem({ context, prop }) {
const collection2 = prop("collection");
const highlightedValue = context.get("highlightedValue");
const highlightedItem = highlightedValue ? collection2.find(highlightedValue) : null;
context.set("highlightedItem", highlightedItem);
},
syncHighlightedValue({ context, prop, refs }) {
const collection2 = prop("collection");
const highlightedValue = context.get("highlightedValue");
const { autoHighlight } = refs.get("inputState");
if (autoHighlight) {
queueMicrotask(() => {
context.set("highlightedValue", prop("collection").firstValue ?? null);
});
return;
}
if (highlightedValue != null && !collection2.has(highlightedValue)) {
queueMicrotask(() => {
context.set("highlightedValue", null);
});
}
},
setFocused({ context }) {
context.set("focused", true);
},
setDefaultHighlightedValue({ context, prop }) {
const collection2 = prop("collection");
const firstValue = collection2.firstValue;
if (firstValue != null) {
context.set("highlightedValue", firstValue);
}
},
clearFocused({ context }) {
context.set("focused", false);
},
setInputState({ refs, event }) {
refs.set("inputState", { autoHighlight: !!event.autoHighlight, focused: true });
},
clearInputState({ refs }) {
refs.set("inputState", { autoHighlight: false, focused: false });
}
}
}
});
var diff = (a, b) => {
const result = new Set(a);
for (const item of b) result.delete(item);
return result;
};
function invokeOnSelect(current, next, onSelect) {
const added = diff(next, current);
for (const item of added) {
onSelect?.({ value: item });
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
machine
});