UNPKG

cmdk-base

Version:

Fast, unstyled command menu React component.

990 lines (983 loc) 41.2 kB
'use client'; Object.defineProperty(exports, '__esModule', { value: true }); var React = require('react'); var react = require('@base-ui-components/react'); var useId = require('@base-ui-components/utils/useId'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return n; } var React__namespace = /*#__PURE__*/_interopNamespace(React); // This is a fork of https://github.com/pacocoursey/cmdk/blob/main/cmdk/src/command-score.ts // The scores are arranged so that a continuous match of characters will // result in a total score of 1. // // The best case, this character is a match, and either this is the start // of the string, or the previous character was also a match. const SCORE_CONTINUE_MATCH = 1, // A new match at the start of a word scores better than a new match // elsewhere as it's more likely that the user will type the starts // of fragments. // NOTE: We score word jumps between spaces slightly higher than slashes, brackets // hyphens, etc. SCORE_SPACE_WORD_JUMP = 0.9, SCORE_NON_SPACE_WORD_JUMP = 0.8, // Any other match isn't ideal, but we include it for completeness. SCORE_CHARACTER_JUMP = 0.17, // If the user transposed two letters, it should be significantly penalized. // // i.e. "ouch" is more likely than "curtain" when "uc" is typed. SCORE_TRANSPOSITION = 0.1, // The goodness of a match should decay slightly with each missing // character. // // i.e. "bad" is more likely than "bard" when "bd" is typed. // // This will not change the order of suggestions based on SCORE_* until // 100 characters are inserted between matches. PENALTY_SKIPPED = 0.999, // The goodness of an exact-case match should be higher than a // case-insensitive match by a small amount. // // i.e. "HTML" is more likely than "haml" when "HM" is typed. // // This will not change the order of suggestions based on SCORE_* until // 1000 characters are inserted between matches. PENALTY_CASE_MISMATCH = 0.9999, // Match higher for letters closer to the beginning of the word // PENALTY_DISTANCE_FROM_START = 0.9, // If the word has more characters than the user typed, it should // be penalised slightly. // // i.e. "html" is more likely than "html5" if I type "html". // // However, it may well be the case that there's a sensible secondary // ordering (like alphabetical) that it makes sense to rely on when // there are many prefix matches, so we don't make the penalty increase // with the number of tokens. PENALTY_NOT_COMPLETE = 0.99; const IS_GAP_REGEXP = /[\\\/_+.#"@\[\(\{&]/, COUNT_GAPS_REGEXP = /[\\\/_+.#"@\[\(\{&]/g, IS_SPACE_REGEXP = /[\s-]/, COUNT_SPACE_REGEXP = /[\s-]/g; function commandScoreInner(value, abbreviation, lowerString, lowerAbbreviation, stringIndex, abbreviationIndex, memoizedResults) { if (abbreviationIndex === abbreviation.length) { if (stringIndex === value.length) { return SCORE_CONTINUE_MATCH; } return PENALTY_NOT_COMPLETE; } const memoizeKey = `${stringIndex},${abbreviationIndex}`; if (memoizedResults[memoizeKey] !== undefined) { return memoizedResults[memoizeKey]; } const abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex); let index = lowerString.indexOf(abbreviationChar, stringIndex); let highScore = 0; let score, transposedScore, wordBreaks, spaceBreaks; while(index >= 0){ score = commandScoreInner(value, abbreviation, lowerString, lowerAbbreviation, index + 1, abbreviationIndex + 1, memoizedResults); if (score > highScore) { if (index === stringIndex) { score *= SCORE_CONTINUE_MATCH; } else if (IS_GAP_REGEXP.test(value.charAt(index - 1))) { score *= SCORE_NON_SPACE_WORD_JUMP; wordBreaks = value.slice(stringIndex, index - 1).match(COUNT_GAPS_REGEXP); if (wordBreaks && stringIndex > 0) { score *= Math.pow(PENALTY_SKIPPED, wordBreaks.length); } } else if (IS_SPACE_REGEXP.test(value.charAt(index - 1))) { score *= SCORE_SPACE_WORD_JUMP; spaceBreaks = value.slice(stringIndex, index - 1).match(COUNT_SPACE_REGEXP); if (spaceBreaks && stringIndex > 0) { score *= Math.pow(PENALTY_SKIPPED, spaceBreaks.length); } } else { score *= SCORE_CHARACTER_JUMP; if (stringIndex > 0) { score *= Math.pow(PENALTY_SKIPPED, index - stringIndex); } } if (value.charAt(index) !== abbreviation.charAt(abbreviationIndex)) { score *= PENALTY_CASE_MISMATCH; } } if (score < SCORE_TRANSPOSITION && lowerString.charAt(index - 1) === lowerAbbreviation.charAt(abbreviationIndex + 1) || lowerAbbreviation.charAt(abbreviationIndex + 1) === lowerAbbreviation.charAt(abbreviationIndex) && // allow duplicate letters. Ref #7428 lowerString.charAt(index - 1) !== lowerAbbreviation.charAt(abbreviationIndex)) { transposedScore = commandScoreInner(value, abbreviation, lowerString, lowerAbbreviation, index + 1, abbreviationIndex + 2, memoizedResults); if (transposedScore * SCORE_TRANSPOSITION > score) { score = transposedScore * SCORE_TRANSPOSITION; } } if (score > highScore) { highScore = score; } index = lowerString.indexOf(abbreviationChar, index + 1); } memoizedResults[memoizeKey] = highScore; return highScore; } function formatInput(value) { // convert all valid space characters to space so they match each other return value.toLowerCase().replace(COUNT_SPACE_REGEXP, " "); } function commandScore(string, abbreviation, aliases) { /* NOTE: * in the original, we used to do the lower-casing on each recursive call, but this meant that toLowerCase() * was the dominating cost in the algorithm, passing both is a little ugly, but considerably faster. */ string = aliases && aliases.length > 0 ? `${string + " " + aliases.join(" ")}` : string; return commandScoreInner(string, abbreviation, formatInput(string), formatInput(abbreviation), 0, 0, {}); } // This is a fork of https://github.com/pacocoursey/cmdk/blob/main/cmdk/src/index.tsx // @ts-nocheck /* eslint-disable */ const GROUP_SELECTOR = `[cmdk-group=""]`; const GROUP_ITEMS_SELECTOR = `[cmdk-group-items=""]`; const GROUP_HEADING_SELECTOR = `[cmdk-group-heading=""]`; const ITEM_SELECTOR = `[cmdk-item=""]`; const VALID_ITEM_SELECTOR = `${ITEM_SELECTOR}:not([aria-disabled="true"])`; const SELECT_EVENT = `cmdk-item-select`; const VALUE_ATTR = `data-value`; const defaultFilter = (value, search, keywords)=>commandScore(value, search, keywords); const CommandContext = /*#__PURE__*/ React__namespace.createContext(undefined); const useCommand = ()=>React__namespace.useContext(CommandContext); const StoreContext = /*#__PURE__*/ React__namespace.createContext(undefined); const useStore = ()=>React__namespace.useContext(StoreContext); const GroupContext = /*#__PURE__*/ React__namespace.createContext(undefined); const Command = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{ const state = useLazyRef(()=>{ var _props_value, _ref; return { /** Value of the search query. */ search: "", /** Currently selected item value. */ value: (_ref = (_props_value = props.value) != null ? _props_value : props.defaultValue) != null ? _ref : "", /** Currently selected item id. */ selectedItemId: undefined, filtered: { /** The count of all visible items. */ count: 0, /** Map from visible item id to its search score. */ items: new Map(), /** Set of groups with at least one visible item. */ groups: new Set() } }; }); const allItems = useLazyRef(()=>new Set()) // [...itemIds] ; const allGroups = useLazyRef(()=>new Map()) // groupId → [...itemIds] ; const ids = useLazyRef(()=>new Map()) // id → { value, keywords } ; const listeners = useLazyRef(()=>new Set()) // [...rerenders] ; const propsRef = useAsRef(props); const { label, children, value, onValueChange, filter, shouldFilter, loop, disablePointerSelection = false, vimBindings = true, ...etc } = props; const listId = useId.useId(); const labelId = useId.useId(); const inputId = useId.useId(); const listInnerRef = React__namespace.useRef(null); const schedule = useScheduleLayoutEffect(); /** Controlled mode `value` handling. */ useLayoutEffect(()=>{ if (value !== undefined) { const v = value.trim(); state.current.value = v; store.emit(); } }, [ value ]); useLayoutEffect(()=>{ schedule(6, scrollSelectedIntoView); }, []); const store = React__namespace.useMemo(()=>{ return { subscribe: (cb)=>{ listeners.current.add(cb); return ()=>listeners.current.delete(cb); }, snapshot: ()=>{ return state.current; }, setState: (key, value, opts)=>{ if (Object.is(state.current[key], value)) return; state.current[key] = value; if (key === "search") { // Filter synchronously before emitting back to children filterItems(); sort(); schedule(1, selectFirstItem); } else if (key === "value") { var _propsRef_current; // Force focus input or root so accessibility works if (document.activeElement.hasAttribute("cmdk-input") || document.activeElement.hasAttribute("cmdk-root")) { var _document_getElementById; const input = document.getElementById(inputId); if (input) input.focus(); else (_document_getElementById = document.getElementById(listId)) == null ? undefined : _document_getElementById.focus(); } schedule(7, ()=>{ var _getSelectedItem; state.current.selectedItemId = (_getSelectedItem = getSelectedItem()) == null ? undefined : _getSelectedItem.id; store.emit(); }); // opts is a boolean referring to whether it should NOT be scrolled into view if (!opts) { // Scroll the selected item into view schedule(5, scrollSelectedIntoView); } if (((_propsRef_current = propsRef.current) == null ? undefined : _propsRef_current.value) !== undefined) { // If controlled, just call the callback instead of updating state internally const newValue = value != null ? value : ""; propsRef.current.onValueChange == null ? undefined : propsRef.current.onValueChange.call(propsRef.current, newValue); return; } } // Notify subscribers that state has changed store.emit(); }, emit: ()=>{ listeners.current.forEach((l)=>l()); } }; }, []); const context = React__namespace.useMemo(()=>({ // Keep id → {value, keywords} mapping up-to-date value: (id, value, keywords)=>{ var _ids_current_get; if (value !== ((_ids_current_get = ids.current.get(id)) == null ? undefined : _ids_current_get.value)) { ids.current.set(id, { value, keywords }); state.current.filtered.items.set(id, score(value, keywords)); schedule(2, ()=>{ sort(); store.emit(); }); } }, // Track item lifecycle (mount, unmount) item: (id, groupId)=>{ allItems.current.add(id); // Track this item within the group if (groupId) { if (!allGroups.current.has(groupId)) { allGroups.current.set(groupId, new Set([ id ])); } else { allGroups.current.get(groupId).add(id); } } // Batch this, multiple items can mount in one pass // and we should not be filtering/sorting/emitting each time schedule(3, ()=>{ filterItems(); sort(); // Could be initial mount, select the first item if none already selected if (!state.current.value) { selectFirstItem(); } store.emit(); }); return ()=>{ ids.current.delete(id); allItems.current.delete(id); state.current.filtered.items.delete(id); const selectedItem = getSelectedItem(); // Batch this, multiple items could be removed in one pass schedule(4, ()=>{ filterItems(); // The item removed have been the selected one, // so selection should be moved to the first if ((selectedItem == null ? undefined : selectedItem.getAttribute("id")) === id) selectFirstItem(); store.emit(); }); }; }, // Track group lifecycle (mount, unmount) group: (id)=>{ if (!allGroups.current.has(id)) { allGroups.current.set(id, new Set()); } return ()=>{ ids.current.delete(id); allGroups.current.delete(id); }; }, filter: ()=>{ return propsRef.current.shouldFilter; }, label: label || props["aria-label"], getDisablePointerSelection: ()=>{ return propsRef.current.disablePointerSelection; }, listId, inputId, labelId, listInnerRef }), []); function score(value, keywords) { var _propsRef_current; var _propsRef_current_filter; const filter = (_propsRef_current_filter = (_propsRef_current = propsRef.current) == null ? undefined : _propsRef_current.filter) != null ? _propsRef_current_filter : defaultFilter; return value ? filter(value, state.current.search, keywords) : 0; } /** Sorts items by score, and groups by highest item score. */ function sort() { if (!state.current.search || // Explicitly false, because true | undefined is the default propsRef.current.shouldFilter === false) { return; } const scores = state.current.filtered.items; // Sort the groups const groups = []; state.current.filtered.groups.forEach((value)=>{ const items = allGroups.current.get(value); // Get the maximum score of the group's items let max = 0; items.forEach((item)=>{ const score = scores.get(item); max = Math.max(score, max); }); groups.push([ value, max ]); }); // Sort items within groups to bottom // Sort items outside of groups // Sort groups to bottom (pushes all non-grouped items to the top) const listInsertionElement = listInnerRef.current; // Sort the items getValidItems().sort((a, b)=>{ const valueA = a.getAttribute("id"); const valueB = b.getAttribute("id"); var _scores_get, _scores_get1; return ((_scores_get = scores.get(valueB)) != null ? _scores_get : 0) - ((_scores_get1 = scores.get(valueA)) != null ? _scores_get1 : 0); }).forEach((item)=>{ const group = item.closest(GROUP_ITEMS_SELECTOR); if (group) { group.appendChild(item.parentElement === group ? item : item.closest(`${GROUP_ITEMS_SELECTOR} > *`)); } else { listInsertionElement.appendChild(item.parentElement === listInsertionElement ? item : item.closest(`${GROUP_ITEMS_SELECTOR} > *`)); } }); groups.sort((a, b)=>b[1] - a[1]).forEach((group)=>{ var _listInnerRef_current; const element = (_listInnerRef_current = listInnerRef.current) == null ? undefined : _listInnerRef_current.querySelector(`${GROUP_SELECTOR}[${VALUE_ATTR}="${encodeURIComponent(group[0])}"]`); element == null ? undefined : element.parentElement.appendChild(element); }); } function selectFirstItem() { const item = getValidItems().find((item)=>item.getAttribute("aria-disabled") !== "true"); const value = item == null ? undefined : item.getAttribute(VALUE_ATTR); store.setState("value", value || undefined); } /** Filters the current items. */ function filterItems() { if (!state.current.search || // Explicitly false, because true | undefined is the default propsRef.current.shouldFilter === false) { state.current.filtered.count = allItems.current.size; // Do nothing, each item will know to show itself because search is empty return; } // Reset the groups state.current.filtered.groups = new Set(); let itemCount = 0; // Check which items should be included for (const id of allItems.current){ var _ids_current_get, _ids_current_get1; var _ids_current_get_value; const value = (_ids_current_get_value = (_ids_current_get = ids.current.get(id)) == null ? undefined : _ids_current_get.value) != null ? _ids_current_get_value : ""; var _ids_current_get_keywords; const keywords = (_ids_current_get_keywords = (_ids_current_get1 = ids.current.get(id)) == null ? undefined : _ids_current_get1.keywords) != null ? _ids_current_get_keywords : []; const rank = score(value, keywords); state.current.filtered.items.set(id, rank); if (rank > 0) itemCount++; } // Check which groups have at least 1 item shown for (const [groupId, group] of allGroups.current){ for (const itemId of group){ if (state.current.filtered.items.get(itemId) > 0) { state.current.filtered.groups.add(groupId); break; } } } state.current.filtered.count = itemCount; } function scrollSelectedIntoView() { // Wait for popover positioning to complete before scrolling requestAnimationFrame(()=>{ const item = getSelectedItem(); if (item) { var _item_parentElement; if (((_item_parentElement = item.parentElement) == null ? undefined : _item_parentElement.firstChild) === item) { var // First item in Group, ensure heading is in view _item_closest_querySelector, _item_closest; (_item_closest = item.closest(GROUP_SELECTOR)) == null ? undefined : (_item_closest_querySelector = _item_closest.querySelector(GROUP_HEADING_SELECTOR)) == null ? undefined : _item_closest_querySelector.scrollIntoView({ block: "nearest" }); } // Ensure the item is always in view item.scrollIntoView({ block: "nearest" }); } }); } /** Getters */ function getSelectedItem() { var _listInnerRef_current; return (_listInnerRef_current = listInnerRef.current) == null ? undefined : _listInnerRef_current.querySelector(`${ITEM_SELECTOR}[aria-selected="true"]`); } function getValidItems() { var _listInnerRef_current; return Array.from(((_listInnerRef_current = listInnerRef.current) == null ? undefined : _listInnerRef_current.querySelectorAll(VALID_ITEM_SELECTOR)) || []); } /** Setters */ function updateSelectedToIndex(index) { const items = getValidItems(); const item = items[index]; if (item) store.setState("value", item.getAttribute(VALUE_ATTR)); } function updateSelectedByItem(change) { var _propsRef_current; const selected = getSelectedItem(); const items = getValidItems(); const index = items.findIndex((item)=>item === selected); // Get item at this index let newSelected = items[index + change]; if ((_propsRef_current = propsRef.current) == null ? undefined : _propsRef_current.loop) { newSelected = index + change < 0 ? items[items.length - 1] : index + change === items.length ? items[0] : items[index + change]; } if (newSelected) store.setState("value", newSelected.getAttribute(VALUE_ATTR)); } function updateSelectedByGroup(change) { const selected = getSelectedItem(); let group = selected == null ? undefined : selected.closest(GROUP_SELECTOR); let item; while(group && !item){ group = change > 0 ? findNextSibling(group, GROUP_SELECTOR) : findPreviousSibling(group, GROUP_SELECTOR); item = group == null ? undefined : group.querySelector(VALID_ITEM_SELECTOR); } if (item) { store.setState("value", item.getAttribute(VALUE_ATTR)); } else { updateSelectedByItem(change); } } const last = ()=>updateSelectedToIndex(getValidItems().length - 1); const next = (e)=>{ e.preventDefault(); if (e.metaKey) { // Last item last(); } else if (e.altKey) { // Next group updateSelectedByGroup(1); } else { // Next item updateSelectedByItem(1); } }; const prev = (e)=>{ e.preventDefault(); if (e.metaKey) { // First item updateSelectedToIndex(0); } else if (e.altKey) { // Previous group updateSelectedByGroup(-1); } else { // Previous item updateSelectedByItem(-1); } }; return /*#__PURE__*/ React__namespace.createElement("div", { ref: forwardedRef, tabIndex: -1, ...etc, "cmdk-root": "", onKeyDown: (e)=>{ etc.onKeyDown == null ? undefined : etc.onKeyDown.call(etc, e); // Check if IME composition is finished before triggering key binds // This prevents unwanted triggering while user is still inputting text with IME // e.keyCode === 229 is for the CJK IME with Legacy Browser [https://w3c.github.io/uievents/#determine-keydown-keyup-keyCode] // isComposing is for the CJK IME with Modern Browser [https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent/isComposing] const isComposing = e.nativeEvent.isComposing || e.keyCode === 229; if (e.defaultPrevented || isComposing) { return; } switch(e.key){ case "n": case "j": { // vim keybind down if (vimBindings && e.ctrlKey) { next(e); } break; } case "ArrowDown": { next(e); break; } case "p": case "k": { // vim keybind up if (vimBindings && e.ctrlKey) { prev(e); } break; } case "ArrowUp": { prev(e); break; } case "Home": { // First item e.preventDefault(); updateSelectedToIndex(0); break; } case "End": { // Last item e.preventDefault(); last(); break; } case "Enter": { // Trigger item onSelect e.preventDefault(); const item = getSelectedItem(); if (item) { const event = new Event(SELECT_EVENT); item.dispatchEvent(event); } } } } }, /*#__PURE__*/ React__namespace.createElement("label", { "cmdk-label": "", htmlFor: context.inputId, id: context.labelId, // Screen reader only style: srOnlyStyles }, label), SlottableWithNestedChildren(props, (child)=>/*#__PURE__*/ React__namespace.createElement(StoreContext.Provider, { value: store }, /*#__PURE__*/ React__namespace.createElement(CommandContext.Provider, { value: context }, child)))); }); /** * Command menu item. Becomes active on pointer enter or through keyboard navigation. * Preferably pass a `value`, otherwise the value will be inferred from `children` or * the rendered item's `textContent`. */ const Item = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{ var _propsRef_current; const id = useId.useId(); const ref = React__namespace.useRef(null); const groupContext = React__namespace.useContext(GroupContext); const context = useCommand(); const propsRef = useAsRef(props); var _propsRef_current_forceMount; const forceMount = (_propsRef_current_forceMount = (_propsRef_current = propsRef.current) == null ? undefined : _propsRef_current.forceMount) != null ? _propsRef_current_forceMount : groupContext == null ? undefined : groupContext.forceMount; useLayoutEffect(()=>{ if (!forceMount) { return context.item(id, groupContext == null ? undefined : groupContext.id); } }, [ forceMount ]); const value = useValue(id, ref, [ props.value, props.children, ref ], props.keywords); const store = useStore(); const selected = useCmdk((state)=>state.value && state.value === value.current); const render = useCmdk((state)=>forceMount ? true : context.filter() === false ? true : !state.search ? true : state.filtered.items.get(id) > 0); React__namespace.useEffect(()=>{ const element = ref.current; if (!element || props.disabled) return; element.addEventListener(SELECT_EVENT, onSelect); return ()=>element.removeEventListener(SELECT_EVENT, onSelect); }, [ render, props.onSelect, props.disabled ]); function onSelect() { select(); propsRef.current.onSelect == null ? undefined : propsRef.current.onSelect.call(propsRef.current, value.current); } function select() { store.setState("value", value.current, true); } if (!render) return null; const { disabled, value: _, onSelect: __, forceMount: ___, keywords: ____, ...etc } = props; return /*#__PURE__*/ React__namespace.createElement("div", { ref: mergeRefs([ ref, forwardedRef ]), ...etc, id: id, "cmdk-item": "", role: "option", "aria-disabled": Boolean(disabled), "aria-selected": Boolean(selected), "data-disabled": Boolean(disabled), "data-selected": Boolean(selected), onPointerMove: disabled || context.getDisablePointerSelection() ? undefined : select, onClick: disabled ? undefined : onSelect }, props.children); }); /** * Group command menu items together with a heading. * Grouped items are always shown together. */ const Group = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{ const { heading, children, forceMount, ...etc } = props; const id = useId.useId(); const ref = React__namespace.useRef(null); const headingRef = React__namespace.useRef(null); const headingId = useId.useId(); const context = useCommand(); const render = useCmdk((state)=>forceMount ? true : context.filter() === false ? true : !state.search ? true : state.filtered.groups.has(id)); useLayoutEffect(()=>{ return context.group(id); }, []); useValue(id, ref, [ props.value, props.heading, headingRef ]); const contextValue = React__namespace.useMemo(()=>({ id, forceMount }), [ forceMount ]); return /*#__PURE__*/ React__namespace.createElement("div", { ref: mergeRefs([ ref, forwardedRef ]), ...etc, "cmdk-group": "", role: "presentation", hidden: render ? undefined : true }, heading && /*#__PURE__*/ React__namespace.createElement("div", { ref: headingRef, "cmdk-group-heading": "", "aria-hidden": true, id: headingId }, heading), SlottableWithNestedChildren(props, (child)=>/*#__PURE__*/ React__namespace.createElement("div", { "cmdk-group-items": "", role: "group", "aria-labelledby": heading ? headingId : undefined }, /*#__PURE__*/ React__namespace.createElement(GroupContext.Provider, { value: contextValue }, child)))); }); /** * A visual and semantic separator between items or groups. * Visible when the search query is empty or `alwaysRender` is true, hidden otherwise. */ const Separator = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{ const { alwaysRender, ...etc } = props; const ref = React__namespace.useRef(null); const render = useCmdk((state)=>!state.search); if (!alwaysRender && !render) return null; return /*#__PURE__*/ React__namespace.createElement("div", { ref: mergeRefs([ ref, forwardedRef ]), ...etc, "cmdk-separator": "", role: "separator" }); }); /** * Command menu input. * All props are forwarded to the underyling `input` element. */ const Input = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{ const { onValueChange, ...etc } = props; const isControlled = props.value != null; const store = useStore(); const search = useCmdk((state)=>state.search); const selectedItemId = useCmdk((state)=>state.selectedItemId); const context = useCommand(); React__namespace.useEffect(()=>{ if (props.value != null) { store.setState("search", props.value); } }, [ props.value ]); return /*#__PURE__*/ React__namespace.createElement("input", { ref: forwardedRef, ...etc, "cmdk-input": "", autoComplete: "off", autoCorrect: "off", spellCheck: false, "aria-autocomplete": "list", role: "combobox", "aria-expanded": true, "aria-controls": context.listId, "aria-labelledby": context.labelId, "aria-activedescendant": selectedItemId, id: context.inputId, type: "text", value: isControlled ? props.value : search, onChange: (e)=>{ if (!isControlled) { store.setState("search", e.target.value); } onValueChange == null ? undefined : onValueChange(e.target.value); } }); }); /** * Contains `Item`, `Group`, and `Separator`. * Use the `--cmdk-list-height` CSS variable to animate height based on the number of results. */ const List = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{ const { children, label = "Suggestions", ...etc } = props; const ref = React__namespace.useRef(null); const height = React__namespace.useRef(null); const selectedItemId = useCmdk((state)=>state.selectedItemId); const context = useCommand(); React__namespace.useEffect(()=>{ if (height.current && ref.current) { const el = height.current; const wrapper = ref.current; let animationFrame; const observer = new ResizeObserver(()=>{ animationFrame = requestAnimationFrame(()=>{ const height = el.offsetHeight; wrapper.style.setProperty(`--cmdk-list-height`, height.toFixed(1) + "px"); }); }); observer.observe(el); return ()=>{ cancelAnimationFrame(animationFrame); observer.unobserve(el); }; } }, []); return /*#__PURE__*/ React__namespace.createElement("div", { ref: mergeRefs([ ref, forwardedRef ]), ...etc, "cmdk-list": "", role: "listbox", tabIndex: -1, "aria-activedescendant": selectedItemId, "aria-label": label, id: context.listId }, SlottableWithNestedChildren(props, (child)=>/*#__PURE__*/ React__namespace.createElement("div", { ref: mergeRefs([ height, context.listInnerRef ]), "cmdk-list-sizer": "" }, child))); }); /** * Renders the command menu in a Base UI Dialog. */ const Dialog = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{ const { open, onOpenChange, overlayClassName, contentClassName, container, ...etc } = props; return /*#__PURE__*/ React__namespace.createElement(react.Dialog.Root, { open: open, onOpenChange: onOpenChange }, /*#__PURE__*/ React__namespace.createElement(react.Dialog.Portal, { container: container }, /*#__PURE__*/ React__namespace.createElement(react.Dialog.Backdrop, { "cmdk-overlay": "", className: overlayClassName }), /*#__PURE__*/ React__namespace.createElement(react.Dialog.Popup, { "aria-label": props.label, "cmdk-dialog": "", className: contentClassName }, /*#__PURE__*/ React__namespace.createElement(Command, { ref: forwardedRef, ...etc })))); }); /** * Automatically renders when there are no results for the search query. */ const Empty = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{ const render = useCmdk((state)=>state.filtered.count === 0); if (!render) return null; return /*#__PURE__*/ React__namespace.createElement("div", { ref: forwardedRef, ...props, "cmdk-empty": "", role: "presentation" }); }); /** * You should conditionally render this with `progress` while loading asynchronous items. */ const Loading = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{ const { progress, children, label = "Loading...", ...etc } = props; return /*#__PURE__*/ React__namespace.createElement("div", { ref: forwardedRef, ...etc, "cmdk-loading": "", role: "progressbar", "aria-valuenow": progress, "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": label }, SlottableWithNestedChildren(props, (child)=>/*#__PURE__*/ React__namespace.createElement("div", { "aria-hidden": true }, child))); }); const pkg = Object.assign(Command, { List, Item, Input, Group, Separator, Dialog, Empty, Loading }); /** * * * Helpers * * */ function findNextSibling(el, selector) { let sibling = el.nextElementSibling; while(sibling){ if (sibling.matches(selector)) return sibling; sibling = sibling.nextElementSibling; } } function findPreviousSibling(el, selector) { let sibling = el.previousElementSibling; while(sibling){ if (sibling.matches(selector)) return sibling; sibling = sibling.previousElementSibling; } } function useAsRef(data) { const ref = React__namespace.useRef(data); useLayoutEffect(()=>{ ref.current = data; }); return ref; } const useLayoutEffect = typeof window === "undefined" ? React__namespace.useEffect : React__namespace.useLayoutEffect; function useLazyRef(fn) { const ref = React__namespace.useRef(); if (ref.current === undefined) { ref.current = fn(); } return ref; } /** Run a selector against the store state. */ function useCmdk(selector) { const store = useStore(); const cb = ()=>selector(store.snapshot()); return React__namespace.useSyncExternalStore(store.subscribe, cb, cb); } function useValue(id, ref, deps, aliases = []) { const valueRef = React__namespace.useRef(); const context = useCommand(); useLayoutEffect(()=>{ var _ref_current; const value = (()=>{ for (const part of deps){ if (typeof part === "string") { return part.trim(); } if (typeof part === "object" && "current" in part) { if (part.current) { var _part_current_textContent; return (_part_current_textContent = part.current.textContent) == null ? undefined : _part_current_textContent.trim(); } return valueRef.current; } } })(); const keywords = aliases.map((alias)=>alias.trim()); context.value(id, value, keywords); (_ref_current = ref.current) == null ? undefined : _ref_current.setAttribute(VALUE_ATTR, value); valueRef.current = value; }); return valueRef; } /** Imperatively run a function on the next layout effect cycle. */ const useScheduleLayoutEffect = ()=>{ const [s, ss] = React__namespace.useState(); const fns = useLazyRef(()=>new Map()); useLayoutEffect(()=>{ fns.current.forEach((f)=>f()); fns.current = new Map(); }, [ s ]); return (id, cb)=>{ fns.current.set(id, cb); ss({}); }; }; function renderChildren(children) { const childrenType = children.type; // The children is a component if (typeof childrenType === "function") return childrenType(children.props); else if ("render" in childrenType) return childrenType.render(children.props); else return children; } function SlottableWithNestedChildren({ asChild, children }, render) { if (asChild && /*#__PURE__*/ React__namespace.isValidElement(children)) { return /*#__PURE__*/ React__namespace.cloneElement(renderChildren(children), { ref: children.ref }, render(children.props.children)); } return render(children); } // ESM is still a nightmare with Next.js so I'm just gonna copy the package code in // https://github.com/gregberge/react-merge-refs // Copyright (c) 2020 Greg Bergé function mergeRefs(refs) { return (value)=>{ refs.forEach((ref)=>{ if (typeof ref === "function") { ref(value); } else if (ref != null) { ref.current = value; } }); }; } const srOnlyStyles = { position: "absolute", width: "1px", height: "1px", padding: "0", margin: "-1px", overflow: "hidden", clip: "rect(0, 0, 0, 0)", whiteSpace: "nowrap", borderWidth: "0" }; exports.Command = pkg; exports.CommandDialog = Dialog; exports.CommandEmpty = Empty; exports.CommandGroup = Group; exports.CommandInput = Input; exports.CommandItem = Item; exports.CommandList = List; exports.CommandLoading = Loading; exports.CommandRoot = Command; exports.CommandSeparator = Separator; exports.defaultFilter = defaultFilter; exports.useCommandState = useCmdk;