@zag-js/listbox
Version:
Core logic for the listbox widget implemented as a state machine
430 lines (428 loc) • 17.1 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.connect.ts
var listbox_connect_exports = {};
__export(listbox_connect_exports, {
connect: () => connect
});
module.exports = __toCommonJS(listbox_connect_exports);
var import_collection = require("@zag-js/collection");
var import_dom_query = require("@zag-js/dom-query");
var import_utils = require("@zag-js/utils");
var import_listbox = require("./listbox.anatomy.js");
var dom = __toESM(require("./listbox.dom.js"));
function connect(service, normalize) {
const { context, prop, scope, computed, send, refs } = service;
const disabled = prop("disabled");
const collection = prop("collection");
const layout = (0, import_collection.isGridCollection)(collection) ? "grid" : "list";
const focused = context.get("focused");
const focusVisible = refs.get("focusVisible") && focused;
const inputState = refs.get("inputState");
const value = context.get("value");
const selectedItems = computed("selectedItems");
const highlightedValue = context.get("highlightedValue");
const highlightedItem = context.get("highlightedItem");
const isTypingAhead = computed("isTypingAhead");
const interactive = computed("isInteractive");
const ariaActiveDescendant = highlightedValue ? dom.getItemId(scope, highlightedValue) : void 0;
function getItemState(props) {
const itemDisabled = collection.getItemDisabled(props.item);
const value2 = collection.getItemValue(props.item);
(0, import_utils.ensure)(value2, () => `[zag-js] No value found for item ${JSON.stringify(props.item)}`);
const highlighted = highlightedValue === value2;
return {
value: value2,
disabled: Boolean(disabled || itemDisabled),
focused: highlighted && focused,
focusVisible: highlighted && focusVisible,
// deprecated
highlighted: highlighted && (inputState.focused ? focused : focusVisible),
selected: context.get("value").includes(value2)
};
}
return {
empty: value.length === 0,
highlightedItem,
highlightedValue,
clearHighlightedValue() {
send({ type: "HIGHLIGHTED_VALUE.SET", value: null });
},
selectedItems,
hasSelectedItems: computed("hasSelectedItems"),
value,
valueAsString: computed("valueAsString"),
collection,
disabled: !!disabled,
selectValue(value2) {
send({ type: "ITEM.SELECT", value: value2 });
},
setValue(value2) {
send({ type: "VALUE.SET", value: value2 });
},
selectAll() {
if (!computed("multiple")) {
throw new Error("[zag-js] Cannot select all items in a single-select listbox");
}
send({ type: "VALUE.SET", value: collection.getValues() });
},
highlightValue(value2) {
send({ type: "HIGHLIGHTED_VALUE.SET", value: value2 });
},
highlightFirst() {
send({ type: "HIGHLIGHT.FIRST" });
},
highlightLast() {
send({ type: "HIGHLIGHT.LAST" });
},
highlightNext() {
send({ type: "HIGHLIGHT.NEXT" });
},
highlightPrevious() {
send({ type: "HIGHLIGHT.PREV" });
},
clearValue(value2) {
if (value2) {
send({ type: "ITEM.CLEAR", value: value2 });
} else {
send({ type: "VALUE.CLEAR" });
}
},
getItemState,
getRootProps() {
return normalize.element({
...import_listbox.parts.root.attrs,
dir: prop("dir"),
id: dom.getRootId(scope),
"data-orientation": prop("orientation"),
"data-disabled": (0, import_dom_query.dataAttr)(disabled)
});
},
getInputProps(props = {}) {
const keyboardPriority = props.keyboardPriority ?? "caret";
return normalize.input({
...import_listbox.parts.input.attrs,
dir: prop("dir"),
disabled,
"data-disabled": (0, import_dom_query.dataAttr)(disabled),
autoComplete: "off",
autoCorrect: "off",
"aria-haspopup": "listbox",
"aria-controls": dom.getContentId(scope),
"aria-autocomplete": "list",
"aria-activedescendant": ariaActiveDescendant,
spellCheck: false,
enterKeyHint: "go",
onFocus() {
queueMicrotask(() => {
send({ type: "INPUT.FOCUS", autoHighlight: !!props?.autoHighlight });
});
},
onBlur() {
send({ type: "CONTENT.BLUR", src: "input" });
},
onInput(event) {
if (!props?.autoHighlight) return;
if (event.currentTarget.value.trim()) return;
queueMicrotask(() => {
send({ type: "HIGHLIGHTED_VALUE.SET", value: null });
});
},
onKeyDown(event) {
if (event.defaultPrevented) return;
if ((0, import_dom_query.isComposingEvent)(event)) return;
const nativeEvent = (0, import_dom_query.getNativeEvent)(event);
const forwardEvent = () => {
event.preventDefault();
const win = scope.getWin();
const keyboardEvent = new win.KeyboardEvent(nativeEvent.type, nativeEvent);
dom.getContentEl(scope)?.dispatchEvent(keyboardEvent);
};
switch (nativeEvent.key) {
case "ArrowLeft":
case "ArrowRight": {
if (!(0, import_collection.isGridCollection)(collection)) return;
if (event.ctrlKey) return;
if (keyboardPriority !== "navigate") return;
forwardEvent();
break;
}
case "Home":
case "End": {
if (keyboardPriority !== "navigate") return;
if (highlightedValue == null && event.shiftKey) return;
forwardEvent();
break;
}
case "ArrowDown":
case "ArrowUp": {
forwardEvent();
break;
}
case "Enter":
if (highlightedValue != null) {
event.preventDefault();
send({ type: "ITEM.CLICK", value: highlightedValue });
}
break;
default:
break;
}
}
});
},
getLabelProps() {
return normalize.element({
dir: prop("dir"),
id: dom.getLabelId(scope),
...import_listbox.parts.label.attrs,
"data-disabled": (0, import_dom_query.dataAttr)(disabled)
});
},
getValueTextProps() {
return normalize.element({
...import_listbox.parts.valueText.attrs,
dir: prop("dir"),
"data-disabled": (0, import_dom_query.dataAttr)(disabled)
});
},
getItemProps(props) {
const itemState = getItemState(props);
return normalize.element({
id: dom.getItemId(scope, itemState.value),
role: "option",
...import_listbox.parts.item.attrs,
dir: prop("dir"),
"data-value": itemState.value,
"aria-selected": itemState.selected,
"data-selected": (0, import_dom_query.dataAttr)(itemState.selected),
"data-layout": layout,
"data-state": itemState.selected ? "checked" : "unchecked",
"data-orientation": prop("orientation"),
"data-highlighted": (0, import_dom_query.dataAttr)(itemState.highlighted),
"data-disabled": (0, import_dom_query.dataAttr)(itemState.disabled),
"aria-disabled": (0, import_dom_query.ariaAttr)(itemState.disabled),
onPointerMove(event) {
if (!props.highlightOnHover) return;
if (itemState.disabled || event.pointerType !== "mouse") return;
if (itemState.highlighted) return;
send({ type: "ITEM.POINTER_MOVE", value: itemState.value });
},
onMouseDown(event) {
event.preventDefault();
dom.getContentEl(scope)?.focus();
},
onClick(event) {
if (event.defaultPrevented) return;
if ((0, import_dom_query.isDownloadingEvent)(event)) return;
if ((0, import_dom_query.isOpeningInNewTab)(event)) return;
if ((0, import_dom_query.isContextMenuEvent)(event)) return;
if (itemState.disabled) return;
send({
type: "ITEM.CLICK",
value: itemState.value,
shiftKey: event.shiftKey,
anchorValue: highlightedValue,
metaKey: (0, import_dom_query.isCtrlOrMetaKey)(event)
});
}
});
},
getItemTextProps(props) {
const itemState = getItemState(props);
return normalize.element({
...import_listbox.parts.itemText.attrs,
"data-state": itemState.selected ? "checked" : "unchecked",
"data-disabled": (0, import_dom_query.dataAttr)(itemState.disabled),
"data-highlighted": (0, import_dom_query.dataAttr)(itemState.highlighted)
});
},
getItemIndicatorProps(props) {
const itemState = getItemState(props);
return normalize.element({
...import_listbox.parts.itemIndicator.attrs,
"aria-hidden": true,
"data-state": itemState.selected ? "checked" : "unchecked",
hidden: !itemState.selected
});
},
getItemGroupLabelProps(props) {
const { htmlFor } = props;
return normalize.element({
...import_listbox.parts.itemGroupLabel.attrs,
id: dom.getItemGroupLabelId(scope, htmlFor),
dir: prop("dir"),
role: "presentation"
});
},
getItemGroupProps(props) {
const { id } = props;
return normalize.element({
...import_listbox.parts.itemGroup.attrs,
"data-disabled": (0, import_dom_query.dataAttr)(disabled),
"data-orientation": prop("orientation"),
"data-empty": (0, import_dom_query.dataAttr)(collection.size === 0),
id: dom.getItemGroupId(scope, id),
"aria-labelledby": dom.getItemGroupLabelId(scope, id),
role: "group",
dir: prop("dir")
});
},
getContentProps() {
return normalize.element({
dir: prop("dir"),
id: dom.getContentId(scope),
role: "listbox",
...import_listbox.parts.content.attrs,
"data-activedescendant": ariaActiveDescendant,
"aria-activedescendant": ariaActiveDescendant,
"data-orientation": prop("orientation"),
"aria-multiselectable": computed("multiple") ? true : void 0,
"aria-labelledby": dom.getLabelId(scope),
tabIndex: 0,
"data-layout": layout,
"data-empty": (0, import_dom_query.dataAttr)(collection.size === 0),
style: {
"--column-count": (0, import_collection.isGridCollection)(collection) ? collection.columnCount : 1
},
onFocus() {
send({ type: "CONTENT.FOCUS" });
},
onBlur() {
send({ type: "CONTENT.BLUR" });
},
onKeyDown(event) {
if (!interactive) return;
const target = (0, import_dom_query.getEventTarget)(event);
if (!(0, import_dom_query.contains)(event.currentTarget, (0, import_dom_query.getEventTarget)(event))) return;
const shiftKey = event.shiftKey;
const keyMap = {
ArrowUp(event2) {
let nextValue = null;
if ((0, import_collection.isGridCollection)(collection) && highlightedValue) {
nextValue = collection.getPreviousRowValue(highlightedValue);
} else if (highlightedValue) {
nextValue = collection.getPreviousValue(highlightedValue);
}
if (!nextValue && (prop("loopFocus") || !highlightedValue)) {
nextValue = collection.lastValue;
}
if (!nextValue) return;
event2.preventDefault();
send({ type: "NAVIGATE", value: nextValue, shiftKey, anchorValue: highlightedValue });
},
ArrowDown(event2) {
let nextValue = null;
if ((0, import_collection.isGridCollection)(collection) && highlightedValue) {
nextValue = collection.getNextRowValue(highlightedValue);
} else if (highlightedValue) {
nextValue = collection.getNextValue(highlightedValue);
}
if (!nextValue && (prop("loopFocus") || !highlightedValue)) {
nextValue = collection.firstValue;
}
if (!nextValue) return;
event2.preventDefault();
send({ type: "NAVIGATE", value: nextValue, shiftKey, anchorValue: highlightedValue });
},
ArrowLeft() {
if (!(0, import_collection.isGridCollection)(collection) && prop("orientation") === "vertical") return;
let nextValue = highlightedValue ? collection.getPreviousValue(highlightedValue) : null;
if (!nextValue && prop("loopFocus")) {
nextValue = collection.lastValue;
}
if (!nextValue) return;
event.preventDefault();
send({ type: "NAVIGATE", value: nextValue, shiftKey, anchorValue: highlightedValue });
},
ArrowRight() {
if (!(0, import_collection.isGridCollection)(collection) && prop("orientation") === "vertical") return;
let nextValue = highlightedValue ? collection.getNextValue(highlightedValue) : null;
if (!nextValue && prop("loopFocus")) {
nextValue = collection.firstValue;
}
if (!nextValue) return;
event.preventDefault();
send({ type: "NAVIGATE", value: nextValue, shiftKey, anchorValue: highlightedValue });
},
Home(event2) {
if ((0, import_dom_query.isEditableElement)(target)) return;
event2.preventDefault();
let nextValue = collection.firstValue;
send({ type: "NAVIGATE", value: nextValue, shiftKey, anchorValue: highlightedValue });
},
End(event2) {
if ((0, import_dom_query.isEditableElement)(target)) return;
event2.preventDefault();
let nextValue = collection.lastValue;
send({ type: "NAVIGATE", value: nextValue, shiftKey, anchorValue: highlightedValue });
},
Enter() {
send({ type: "ITEM.CLICK", value: highlightedValue });
},
a(event2) {
if ((0, import_dom_query.isCtrlOrMetaKey)(event2) && computed("multiple") && !prop("disallowSelectAll")) {
event2.preventDefault();
send({ type: "VALUE.SET", value: collection.getValues() });
}
},
Space(event2) {
if (isTypingAhead && prop("typeahead")) {
send({ type: "CONTENT.TYPEAHEAD", key: event2.key });
} else {
keyMap.Enter?.(event2);
}
},
Escape(event2) {
if (prop("deselectable") && value.length > 0) {
event2.preventDefault();
event2.stopPropagation();
send({ type: "VALUE.CLEAR" });
}
}
};
const exec = keyMap[(0, import_dom_query.getEventKey)(event)];
if (exec) {
exec(event);
return;
}
if ((0, import_dom_query.isEditableElement)(target)) return;
if (import_dom_query.getByTypeahead.isValidEvent(event) && prop("typeahead")) {
send({ type: "CONTENT.TYPEAHEAD", key: event.key });
event.preventDefault();
}
}
});
}
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
connect
});