UNPKG

@zag-js/listbox

Version:

Core logic for the listbox widget implemented as a state machine

440 lines (438 loc) 16.3 kB
"use strict"; 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 });