cmdk-base
Version:
Fast, unstyled command menu React component.
957 lines (953 loc) • 39.9 kB
JavaScript
'use client';
import * as React from 'react';
import { Dialog as Dialog$1 } from '@base-ui-components/react';
import { useId } from '@base-ui-components/utils/useId';
// 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.createContext(undefined);
const useCommand = ()=>React.useContext(CommandContext);
const StoreContext = /*#__PURE__*/ React.createContext(undefined);
const useStore = ()=>React.useContext(StoreContext);
const GroupContext = /*#__PURE__*/ React.createContext(undefined);
const Command = /*#__PURE__*/ React.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();
const labelId = useId();
const inputId = useId();
const listInnerRef = React.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.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.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.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.createElement("label", {
"cmdk-label": "",
htmlFor: context.inputId,
id: context.labelId,
// Screen reader only
style: srOnlyStyles
}, label), SlottableWithNestedChildren(props, (child)=>/*#__PURE__*/ React.createElement(StoreContext.Provider, {
value: store
}, /*#__PURE__*/ React.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.forwardRef((props, forwardedRef)=>{
var _propsRef_current;
const id = useId();
const ref = React.useRef(null);
const groupContext = React.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.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.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.forwardRef((props, forwardedRef)=>{
const { heading, children, forceMount, ...etc } = props;
const id = useId();
const ref = React.useRef(null);
const headingRef = React.useRef(null);
const headingId = 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.useMemo(()=>({
id,
forceMount
}), [
forceMount
]);
return /*#__PURE__*/ React.createElement("div", {
ref: mergeRefs([
ref,
forwardedRef
]),
...etc,
"cmdk-group": "",
role: "presentation",
hidden: render ? undefined : true
}, heading && /*#__PURE__*/ React.createElement("div", {
ref: headingRef,
"cmdk-group-heading": "",
"aria-hidden": true,
id: headingId
}, heading), SlottableWithNestedChildren(props, (child)=>/*#__PURE__*/ React.createElement("div", {
"cmdk-group-items": "",
role: "group",
"aria-labelledby": heading ? headingId : undefined
}, /*#__PURE__*/ React.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.forwardRef((props, forwardedRef)=>{
const { alwaysRender, ...etc } = props;
const ref = React.useRef(null);
const render = useCmdk((state)=>!state.search);
if (!alwaysRender && !render) return null;
return /*#__PURE__*/ React.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.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.useEffect(()=>{
if (props.value != null) {
store.setState("search", props.value);
}
}, [
props.value
]);
return /*#__PURE__*/ React.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.forwardRef((props, forwardedRef)=>{
const { children, label = "Suggestions", ...etc } = props;
const ref = React.useRef(null);
const height = React.useRef(null);
const selectedItemId = useCmdk((state)=>state.selectedItemId);
const context = useCommand();
React.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.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.createElement("div", {
ref: mergeRefs([
height,
context.listInnerRef
]),
"cmdk-list-sizer": ""
}, child)));
});
/**
* Renders the command menu in a Base UI Dialog.
*/ const Dialog = /*#__PURE__*/ React.forwardRef((props, forwardedRef)=>{
const { open, onOpenChange, overlayClassName, contentClassName, container, ...etc } = props;
return /*#__PURE__*/ React.createElement(Dialog$1.Root, {
open: open,
onOpenChange: onOpenChange
}, /*#__PURE__*/ React.createElement(Dialog$1.Portal, {
container: container
}, /*#__PURE__*/ React.createElement(Dialog$1.Backdrop, {
"cmdk-overlay": "",
className: overlayClassName
}), /*#__PURE__*/ React.createElement(Dialog$1.Popup, {
"aria-label": props.label,
"cmdk-dialog": "",
className: contentClassName
}, /*#__PURE__*/ React.createElement(Command, {
ref: forwardedRef,
...etc
}))));
});
/**
* Automatically renders when there are no results for the search query.
*/ const Empty = /*#__PURE__*/ React.forwardRef((props, forwardedRef)=>{
const render = useCmdk((state)=>state.filtered.count === 0);
if (!render) return null;
return /*#__PURE__*/ React.createElement("div", {
ref: forwardedRef,
...props,
"cmdk-empty": "",
role: "presentation"
});
});
/**
* You should conditionally render this with `progress` while loading asynchronous items.
*/ const Loading = /*#__PURE__*/ React.forwardRef((props, forwardedRef)=>{
const { progress, children, label = "Loading...", ...etc } = props;
return /*#__PURE__*/ React.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.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.useRef(data);
useLayoutEffect(()=>{
ref.current = data;
});
return ref;
}
const useLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
function useLazyRef(fn) {
const ref = React.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.useSyncExternalStore(store.subscribe, cb, cb);
}
function useValue(id, ref, deps, aliases = []) {
const valueRef = React.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.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.isValidElement(children)) {
return /*#__PURE__*/ React.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"
};
export { pkg as Command, Dialog as CommandDialog, Empty as CommandEmpty, Group as CommandGroup, Input as CommandInput, Item as CommandItem, List as CommandList, Loading as CommandLoading, Command as CommandRoot, Separator as CommandSeparator, defaultFilter, useCmdk as useCommandState };