@diceui/combobox
Version:
Combobox is a component that allows users to select an option from a list of options.
1,261 lines (1,253 loc) • 43.4 kB
JavaScript
'use client';
import { createContext, forwardRef, useComposedRefs, Primitive, composeEventHandlers, useAnchorPositioner, useDismiss, useScrollLock, Presence, useId, useLabel, dispatchDiscreteCustomEvent, useIsomorphicLayoutEffect, DATA_ITEM_ATTR, useProgress, Portal as Portal$1, useDirection, useCollection, useAnchor, useFormControl, useControllableState, useFilterStore, useListHighlighting, VisuallyHiddenInput } from '@diceui/shared';
import * as React from 'react';
import { FloatingFocusManager } from '@floating-ui/react';
function getDataState(open) {
return open ? "open" : "closed";
}
var ROOT_NAME = "ComboboxRoot";
var [ComboboxProvider, useComboboxContext] = createContext(ROOT_NAME);
function ComboboxRootImpl(props, forwardedRef) {
const {
value: valueProp,
defaultValue,
onValueChange: onValueChangeProp,
open: openProp,
defaultOpen = false,
onOpenChange,
inputValue: inputValueProp,
onInputValueChange,
onFilter,
autoHighlight = false,
disabled = false,
exactMatch = false,
manualFiltering = false,
loop = false,
modal = false,
multiple = false,
openOnFocus = false,
preserveInputOnBlur = false,
readOnly = false,
required = false,
dir: dirProp,
name,
children,
...rootProps
} = props;
const inputRef = React.useRef(null);
const listRef = React.useRef(null);
const inputId = useId();
const labelId = useId();
const listId = useId();
const dir = useDirection(dirProp);
const { collectionRef, getItems, itemMap, groupMap, onItemRegister } = useCollection({
grouped: true
});
const { anchorRef, hasAnchor, onHasAnchorChange } = useAnchor();
const { isFormControl, onTriggerChange } = useFormControl();
const composedRef = useComposedRefs(
forwardedRef,
collectionRef,
(node) => onTriggerChange(node)
);
const [selectedText, setSelectedText] = React.useState("");
const [highlightedItem, setHighlightedItem] = React.useState(null);
const [highlightedBadgeIndex, setHighlightedBadgeIndex] = React.useState(-1);
const [hasBadgeList, setHasBadgeList] = React.useState(false);
const [open = false, setOpen] = useControllableState({
prop: openProp,
defaultProp: defaultOpen,
onChange: (newOpen) => {
if (!newOpen) {
filterStore.search = "";
}
onOpenChange?.(newOpen);
if (multiple) {
setHighlightedBadgeIndex(-1);
return;
}
if (defaultValue && !Array.isArray(defaultValue) && selectedText === "") {
setSelectedText(defaultValue);
}
}
});
const [value = multiple ? [] : "", setValue] = useControllableState({
prop: valueProp,
defaultProp: defaultValue,
onChange: onValueChangeProp
});
const [inputValue = "", setInputValue] = useControllableState({
defaultProp: !multiple && defaultValue ? String(defaultValue) : "",
prop: inputValueProp,
onChange: (payload) => {
if (disabled || readOnly) return;
onInputValueChange?.(payload);
if (autoHighlight && open) {
onHighlightMove("first");
}
}
});
const { filterStore, onItemsFilter, getIsItemVisible, getIsListEmpty } = useFilterStore({
itemMap,
groupMap,
onFilter,
exactMatch,
manualFiltering
});
const onValueChange = React.useCallback(
(newValue) => {
if (disabled || readOnly) return;
if (multiple) {
const currentValue = Array.isArray(value) ? value : [];
const typedNewValue = typeof newValue === "string" ? newValue : "";
if (!typedNewValue) return;
const newValues = currentValue.includes(typedNewValue) ? currentValue.filter((v) => v !== newValue) : [...currentValue, newValue];
setValue(newValues);
return;
}
setValue(newValue);
},
[multiple, setValue, value, disabled, readOnly]
);
const onItemRemove = React.useCallback(
(currentValue) => {
const newValues = Array.isArray(value) ? value.filter((v) => v !== currentValue) : [];
setValue(newValues);
},
[setValue, value]
);
const { onHighlightMove } = useListHighlighting({
highlightedItem,
onHighlightedItemChange: setHighlightedItem,
getItems: React.useCallback(() => {
return getItems().filter(
(item) => !item.disabled && getIsItemVisible(item.value)
);
}, [getItems, getIsItemVisible]),
getIsItemSelected: (item) => {
const selectedValue = Array.isArray(value) ? value[0] : value;
return item.value === selectedValue;
},
loop
});
return /* @__PURE__ */ React.createElement(
ComboboxProvider,
{
value,
onValueChange,
open,
onOpenChange: setOpen,
inputValue,
onInputValueChange: setInputValue,
selectedText,
onSelectedTextChange: setSelectedText,
filterStore,
onFilter,
onItemsFilter,
highlightedItem,
onHighlightedItemChange: setHighlightedItem,
highlightedBadgeIndex,
onHighlightedBadgeIndexChange: setHighlightedBadgeIndex,
onItemRegister,
onItemRemove,
onHighlightMove,
getIsItemVisible,
getIsListEmpty,
hasAnchor,
onHasAnchorChange,
hasBadgeList,
onHasBadgeListChange: setHasBadgeList,
autoHighlight,
disabled,
loop,
manualFiltering,
modal,
multiple,
openOnFocus,
preserveInputOnBlur,
readOnly,
collectionRef,
listRef,
inputRef,
anchorRef,
dir,
inputId,
labelId,
listId
},
/* @__PURE__ */ React.createElement(Primitive.div, { ...rootProps, ref: composedRef }, children, isFormControl && name && /* @__PURE__ */ React.createElement(
VisuallyHiddenInput,
{
type: "hidden",
control: collectionRef.current,
name,
value,
disabled,
readOnly,
required
}
))
);
}
var ComboboxRoot = forwardRef(ComboboxRootImpl);
ComboboxRoot.displayName = ROOT_NAME;
var Root = ComboboxRoot;
// src/combobox-anchor.tsx
var ANCHOR_NAME = "ComboboxAnchor";
var ComboboxAnchor = React.forwardRef(
(props, forwardedRef) => {
const { preventInputFocus, ...anchorProps } = props;
const context = useComboboxContext(ANCHOR_NAME);
const composedRef = useComposedRefs(
forwardedRef,
context.anchorRef,
(node) => context.onHasAnchorChange(!!node)
);
const [isFocused, setIsFocused] = React.useState(false);
return /* @__PURE__ */ React.createElement(
Primitive.div,
{
"data-state": context.open ? "open" : "closed",
"data-anchor": "",
"data-disabled": context.disabled ? "" : void 0,
"data-focused": isFocused ? "" : void 0,
dir: context.dir,
...anchorProps,
ref: composedRef,
onBlur: composeEventHandlers(
anchorProps.onBlur,
() => setIsFocused(false)
),
onClick: composeEventHandlers(anchorProps.onClick, (event) => {
if (preventInputFocus) return;
event.currentTarget.focus();
context.inputRef.current?.focus();
}),
onFocus: composeEventHandlers(
anchorProps.onFocus,
() => setIsFocused(true)
),
onPointerDown: composeEventHandlers(
anchorProps.onPointerDown,
(event) => {
if (context.disabled) return;
const target = event.target;
if (!(target instanceof HTMLElement)) return;
if (target.hasPointerCapture(event.pointerId)) {
target.releasePointerCapture(event.pointerId);
}
if (event.button === 0 && event.ctrlKey === false && event.pointerType === "mouse" && !(event.target instanceof HTMLInputElement)) {
event.preventDefault();
}
}
)
}
);
}
);
ComboboxAnchor.displayName = ANCHOR_NAME;
var Anchor = ComboboxAnchor;
var CONTENT_NAME = "ComboboxContent";
var [ComboboxContentProvider, useComboboxContentContext] = createContext(CONTENT_NAME);
var ComboboxContent = React.forwardRef(
(props, forwardedRef) => {
const {
forceMount = false,
side = "bottom",
sideOffset = 4,
align = "start",
alignOffset = 0,
arrowPadding = 0,
collisionBoundary,
collisionPadding,
sticky = "partial",
strategy = "absolute",
avoidCollisions = true,
fitViewport = false,
hideWhenDetached = false,
trackAnchor = true,
onEscapeKeyDown,
onPointerDownOutside,
style,
...contentProps
} = props;
const context = useComboboxContext(CONTENT_NAME);
const positionerContext = useAnchorPositioner({
open: context.open,
onOpenChange: context.onOpenChange,
anchorRef: context.hasAnchor ? context.anchorRef : context.inputRef,
side,
sideOffset,
align,
alignOffset,
arrowPadding,
collisionBoundary,
collisionPadding,
sticky,
strategy,
avoidCollisions,
fitViewport,
hideWhenDetached,
trackAnchor
});
const composedRef = useComposedRefs(
forwardedRef,
context.listRef,
(node) => positionerContext.refs.setFloating(node)
);
const composedStyle = React.useMemo(() => {
return {
...style,
...positionerContext.floatingStyles,
...!context.open && forceMount ? { visibility: "hidden" } : {}
};
}, [style, positionerContext.floatingStyles, context.open, forceMount]);
useDismiss({
enabled: context.open,
onDismiss: () => context.onOpenChange(false),
refs: [context.listRef, context.anchorRef],
onFocusOutside: (event) => event.preventDefault(),
onEscapeKeyDown,
onPointerDownOutside,
disableOutsidePointerEvents: context.open && context.modal,
preventScrollDismiss: context.open
});
useScrollLock({
referenceElement: context.inputRef.current,
enabled: context.open && context.modal
});
return /* @__PURE__ */ React.createElement(
ComboboxContentProvider,
{
side: positionerContext.side,
align: positionerContext.align,
onArrowChange: positionerContext.onArrowChange,
arrowDisplaced: positionerContext.arrowDisplaced,
arrowStyles: positionerContext.arrowStyles,
forceMount
},
/* @__PURE__ */ React.createElement(
FloatingFocusManager,
{
context: positionerContext.context,
modal: false,
initialFocus: context.inputRef,
returnFocus: false,
disabled: !context.open,
visuallyHiddenDismiss: true
},
/* @__PURE__ */ React.createElement(Presence, { present: forceMount || context.open }, /* @__PURE__ */ React.createElement(
Primitive.div,
{
"data-state": getDataState(context.open),
role: "listbox",
dir: context.dir,
...positionerContext.getFloatingProps(contentProps),
ref: composedRef,
style: composedStyle
}
))
)
);
}
);
ComboboxContent.displayName = CONTENT_NAME;
var Content = ComboboxContent;
// src/combobox-arrow.tsx
var ARROW_NAME = "ComboboxArrow";
var ComboboxArrow = React.forwardRef(
(props, forwardedRef) => {
const { width = 10, height = 5, ...arrowProps } = props;
const context = useComboboxContext(ARROW_NAME);
const contentContext = useComboboxContentContext(ARROW_NAME);
if (!context.open) return null;
return /* @__PURE__ */ React.createElement(
"span",
{
ref: (node) => contentContext.onArrowChange(node),
style: {
...contentContext.arrowStyles,
visibility: contentContext.arrowDisplaced ? "hidden" : void 0
}
},
/* @__PURE__ */ React.createElement(
Primitive.svg,
{
width,
height,
viewBox: "0 0 30 10",
preserveAspectRatio: "none",
"aria-hidden": contentContext.arrowDisplaced,
"data-side": contentContext.side,
"data-align": contentContext.align,
"data-displaced": contentContext.arrowDisplaced ? "" : void 0,
"data-state": getDataState(context.open),
...arrowProps,
ref: forwardedRef,
style: {
...arrowProps.style,
// ensure the svg is measured correctly
display: "block"
}
},
props.asChild ? props.children : /* @__PURE__ */ React.createElement("path", { d: "M0 10 L15 0 L30 10", fill: "currentColor" })
)
);
}
);
ComboboxArrow.displayName = ARROW_NAME;
var Arrow = ComboboxArrow;
var BADGE_LIST_NAME = "ComboboxBadgeList";
var [ComboboxBadgeListProvider, useComboboxBadgeListContext] = createContext(BADGE_LIST_NAME);
var ComboboxBadgeList = React.forwardRef((props, forwardedRef) => {
const {
forceMount = false,
orientation = "horizontal",
...badgeListProps
} = props;
const context = useComboboxContext(BADGE_LIST_NAME);
const values = Array.isArray(context.value) ? context.value : [];
const composedRef = useComposedRefs(forwardedRef, (node) => {
context.onHasBadgeListChange(!!node);
});
if (!forceMount && (!context.multiple || values.length === 0)) {
return null;
}
return /* @__PURE__ */ React.createElement(
ComboboxBadgeListProvider,
{
orientation,
badgeCount: values.length
},
/* @__PURE__ */ React.createElement(
Primitive.div,
{
role: "listbox",
"aria-multiselectable": context.multiple,
"aria-orientation": orientation,
"data-orientation": orientation,
...badgeListProps,
ref: composedRef
}
)
);
});
ComboboxBadgeList.displayName = BADGE_LIST_NAME;
var BadgeList = ComboboxBadgeList;
// src/combobox-badge-item.tsx
var BADGE_ITEM_NAME = "ComboboxBadgeItem";
var [ComboboxBadgeItemProvider, useComboboxBadgeItemContext] = createContext(BADGE_ITEM_NAME);
var ComboboxBadgeItem = React.forwardRef((props, forwardedRef) => {
const { value, disabled, ...badgeItemProps } = props;
const id = useId();
const context = useComboboxContext(BADGE_ITEM_NAME);
const badgeListContext = useComboboxBadgeListContext(BADGE_ITEM_NAME);
const index = Array.isArray(context.value) ? context.value.indexOf(value) : -1;
const isHighlighted = index === context.highlightedBadgeIndex;
const position = index + 1;
const isDisabled = disabled || context.disabled;
return /* @__PURE__ */ React.createElement(
ComboboxBadgeItemProvider,
{
value,
id,
isHighlighted,
position,
disabled: isDisabled
},
/* @__PURE__ */ React.createElement(
Primitive.div,
{
role: "option",
id,
"aria-selected": isHighlighted,
"aria-disabled": isDisabled,
"aria-orientation": badgeListContext.orientation,
"aria-posinset": position,
"aria-setsize": badgeListContext.badgeCount,
"data-disabled": isDisabled ? "" : void 0,
"data-highlighted": isHighlighted ? "" : void 0,
"data-orientation": badgeListContext.orientation,
...badgeItemProps,
ref: forwardedRef,
onFocus: composeEventHandlers(props.onFocus, () => {
if (!isDisabled) {
context.onHighlightedBadgeIndexChange(index);
}
}),
onBlur: composeEventHandlers(props.onBlur, () => {
if (context.highlightedBadgeIndex === index) {
context.onHighlightedBadgeIndexChange(-1);
}
})
}
)
);
});
ComboboxBadgeItem.displayName = BADGE_ITEM_NAME;
var BadgeItem = ComboboxBadgeItem;
var BADGE_ITEM_DELETE_NAME = "ComboboxBadgeItemDelete";
var ComboboxBadgeItemDelete = React.forwardRef((props, forwardedRef) => {
const context = useComboboxContext(BADGE_ITEM_DELETE_NAME);
const badgeItemContext = useComboboxBadgeItemContext(BADGE_ITEM_DELETE_NAME);
const buttonRef = React.useRef(null);
const composedRef = useComposedRefs(forwardedRef, buttonRef);
return /* @__PURE__ */ React.createElement(
Primitive.button,
{
type: "button",
"aria-controls": badgeItemContext.id,
"aria-disabled": badgeItemContext.disabled,
"data-disabled": badgeItemContext.disabled ? "" : void 0,
"data-highlighted": badgeItemContext.isHighlighted ? "" : void 0,
tabIndex: badgeItemContext.disabled ? void 0 : -1,
...props,
ref: composedRef,
onClick: composeEventHandlers(props.onClick, (event) => {
if (badgeItemContext.disabled) return;
event.stopPropagation();
context.onItemRemove(badgeItemContext.value);
requestAnimationFrame(() => {
context.inputRef.current?.focus();
});
}),
onPointerDown: composeEventHandlers(props.onPointerDown, (event) => {
if (badgeItemContext.disabled) return;
const target = event.target;
if (!(target instanceof Element)) return;
if (target.hasPointerCapture(event.pointerId)) {
target.releasePointerCapture(event.pointerId);
}
if (event.button === 0 && event.ctrlKey === false && event.pointerType === "mouse") {
event.preventDefault();
}
})
}
);
});
ComboboxBadgeItemDelete.displayName = BADGE_ITEM_DELETE_NAME;
var BadgeItemDelete = ComboboxBadgeItemDelete;
var CANCEL_NAME = "ComboboxCancel";
var ComboboxCancel = React.forwardRef(
(props, forwardedRef) => {
const { forceMount = false, disabled, ...cancelProps } = props;
const context = useComboboxContext(CANCEL_NAME);
const isDisabled = disabled || context.disabled;
if (!forceMount && !context.inputValue) return null;
return /* @__PURE__ */ React.createElement(
Primitive.button,
{
type: "button",
"aria-controls": context.inputId,
"data-disabled": isDisabled ? "" : void 0,
disabled: isDisabled,
...cancelProps,
ref: forwardedRef,
onClick: composeEventHandlers(cancelProps.onClick, () => {
context.onInputValueChange("");
context.filterStore.search = "";
requestAnimationFrame(() => {
context.inputRef.current?.focus();
});
}),
onPointerDown: composeEventHandlers(
cancelProps.onPointerDown,
(event) => {
if (isDisabled) return;
const target = event.target;
if (!(target instanceof Element)) return;
if (target.hasPointerCapture(event.pointerId)) {
target.releasePointerCapture(event.pointerId);
}
if (event.button === 0 && event.ctrlKey === false && event.pointerType === "mouse") {
event.preventDefault();
}
}
)
}
);
}
);
ComboboxCancel.displayName = CANCEL_NAME;
var Cancel = ComboboxCancel;
var EMPTY_NAME = "ComboboxEmpty";
var ComboboxEmpty = React.forwardRef(
(props, forwardedRef) => {
const { keepVisible = false, ...emptyProps } = props;
const context = useComboboxContext(EMPTY_NAME);
const isVisible = context.open && context.getIsListEmpty(keepVisible);
if (!isVisible) return null;
return /* @__PURE__ */ React.createElement(
Primitive.div,
{
role: "status",
"aria-live": "polite",
"aria-atomic": "true",
"data-state": "empty",
...emptyProps,
ref: forwardedRef
}
);
}
);
ComboboxEmpty.displayName = EMPTY_NAME;
var Empty = ComboboxEmpty;
var GROUP_NAME = "ComboboxGroup";
var [ComboboxGroupProvider, useComboboxGroupContext] = createContext(GROUP_NAME);
var ComboboxGroup = React.forwardRef(
(props, forwardedRef) => {
const { forceMount = false, ...groupProps } = props;
const id = useId();
const labelId = `${id}label`;
const context = useComboboxContext(GROUP_NAME);
const isVisible = forceMount || !context.filterStore.search || context.filterStore.groups?.has(id);
if (!isVisible) return null;
return /* @__PURE__ */ React.createElement(ComboboxGroupProvider, { id, labelId, forceMount }, /* @__PURE__ */ React.createElement(
Primitive.div,
{
role: "group",
id,
"aria-labelledby": labelId,
...groupProps,
ref: forwardedRef
}
));
}
);
ComboboxGroup.displayName = GROUP_NAME;
var Group = ComboboxGroup;
var GROUP_LABEL_NAME = "ComboboxGroupLabel";
var ComboboxGroupLabel = React.forwardRef((props, forwardedRef) => {
const groupContext = useComboboxGroupContext(GROUP_LABEL_NAME);
return /* @__PURE__ */ React.createElement(Primitive.div, { id: groupContext.labelId, ...props, ref: forwardedRef });
});
ComboboxGroupLabel.displayName = GROUP_LABEL_NAME;
var GroupLabel = ComboboxGroupLabel;
var ITEM_NAME = "ComboboxItem";
var ITEM_SELECT_EVENT = `${ITEM_NAME}.Select.Event`;
var [ComboboxItemProvider, useComboboxItemContext] = createContext(ITEM_NAME);
var ComboboxItem = React.forwardRef(
(props, forwardedRef) => {
const { value, label: labelProp, disabled, onSelect, ...itemProps } = props;
const context = useComboboxContext(ITEM_NAME);
const groupContext = useComboboxGroupContext(ITEM_NAME, true);
const { label, onLabelChange } = useLabel({
defaultValue: labelProp
});
const itemRef = React.useRef(null);
const composedRef = useComposedRefs(forwardedRef, itemRef);
const isPointerDownRef = React.useRef(false);
const id = useId();
const textId = `${id}text`;
const isSelected = Array.isArray(context.value) ? context.value.includes(value) : context.value === value;
const isDisabled = disabled || context.disabled || false;
const onItemSelect = React.useCallback(() => {
const itemElement = itemRef.current;
if (!itemElement) return;
if (onSelect) {
const itemSelectEvent = new CustomEvent(ITEM_SELECT_EVENT, {
bubbles: true
});
itemElement.addEventListener(
ITEM_SELECT_EVENT,
() => onSelect?.(value),
{
once: true
}
);
dispatchDiscreteCustomEvent(itemElement, itemSelectEvent);
}
if (context.multiple) {
context.onInputValueChange("");
} else {
const selectedLabel = label ?? itemElement.textContent ?? "";
context.onInputValueChange(selectedLabel);
context.onSelectedTextChange(selectedLabel);
context.onHighlightedItemChange(null);
context.onOpenChange(false);
}
context.filterStore.search = "";
context.onValueChange(value);
context.inputRef.current?.focus();
}, [
label,
value,
onSelect,
context.multiple,
context.onInputValueChange,
context.onHighlightedItemChange,
context.onOpenChange,
context.onSelectedTextChange,
context.onValueChange,
context.inputRef,
context.filterStore
]);
useIsomorphicLayoutEffect(() => {
if (value === "") {
throw new Error(`${ITEM_NAME} value cannot be an empty string`);
}
return context.onItemRegister(
{
ref: itemRef,
label,
value,
disabled: isDisabled,
onSelect
},
groupContext?.id
);
}, [
label,
value,
isDisabled,
onSelect,
groupContext?.id,
context.onItemRegister
]);
const isVisible = context.getIsItemVisible(value);
if (!isVisible) return null;
return /* @__PURE__ */ React.createElement(
ComboboxItemProvider,
{
value,
isSelected,
disabled,
textId,
onItemLabelChange: onLabelChange
},
/* @__PURE__ */ React.createElement(
Primitive.div,
{
role: "option",
id,
"aria-selected": isSelected,
"aria-disabled": isDisabled,
"aria-labelledby": textId,
...{ [DATA_ITEM_ATTR]: "" },
"data-state": isSelected ? "checked" : "unchecked",
"data-highlighted": context.highlightedItem?.ref.current?.id === id ? "" : void 0,
"data-disabled": isDisabled ? "" : void 0,
tabIndex: disabled ? void 0 : -1,
...itemProps,
ref: composedRef,
onClick: composeEventHandlers(itemProps.onClick, (event) => {
if (isDisabled || context.readOnly) return;
event?.currentTarget.focus();
onItemSelect();
}),
onPointerDown: composeEventHandlers(
itemProps.onPointerDown,
(event) => {
if (isDisabled) return;
isPointerDownRef.current = true;
const target = event.target;
if (!(target instanceof HTMLElement)) return;
if (target.hasPointerCapture(event.pointerId)) {
target.releasePointerCapture(event.pointerId);
}
if (event.button === 0 && event.ctrlKey === false && event.pointerType === "mouse") {
event.preventDefault();
}
}
),
onPointerUp: composeEventHandlers(itemProps.onPointerUp, (event) => {
if (!isPointerDownRef.current) event.currentTarget?.click();
isPointerDownRef.current = false;
}),
onPointerMove: composeEventHandlers(itemProps.onPointerMove, () => {
if (isDisabled) return;
context.onHighlightedItemChange({
ref: itemRef,
label,
value,
disabled: isDisabled
});
})
}
)
);
}
);
ComboboxItem.displayName = ITEM_NAME;
var Item = ComboboxItem;
// src/combobox-input.tsx
var INPUT_NAME = "ComboboxInput";
var ComboboxInput = React.forwardRef(
(props, forwardedRef) => {
const context = useComboboxContext(INPUT_NAME);
const composedRef = useComposedRefs(forwardedRef, context.inputRef);
const onChange = React.useCallback(
(event) => {
if (context.disabled || context.readOnly) return;
if (!context.open) context.onOpenChange(true);
const value = event.target.value;
const trimmedValue = value.trim();
requestAnimationFrame(() => {
context.onInputValueChange(value);
if (trimmedValue === "") {
context.onValueChange(trimmedValue);
context.onHighlightedItemChange(null);
}
context.filterStore.search = trimmedValue;
context.onItemsFilter();
});
},
[
context.open,
context.onOpenChange,
context.filterStore,
context.onItemsFilter,
context.onInputValueChange,
context.onValueChange,
context.onHighlightedItemChange,
context.disabled,
context.readOnly
]
);
const onFocus = React.useCallback(() => {
if (context.openOnFocus && !context.open && !context.readOnly) {
context.onOpenChange(true);
}
}, [
context.openOnFocus,
context.open,
context.readOnly,
context.onOpenChange
]);
const onBlur = React.useCallback(() => {
if (!context.multiple && context.value) {
context.onInputValueChange(context.selectedText);
return;
}
if (context.inputValue && !context.preserveInputOnBlur) {
context.onInputValueChange("");
context.onHighlightedItemChange(null);
}
if (context.multiple) {
context.onHighlightedBadgeIndexChange(-1);
}
}, [
context.multiple,
context.value,
context.preserveInputOnBlur,
context.onInputValueChange,
context.onHighlightedItemChange,
context.inputValue,
context.selectedText,
context.onHighlightedBadgeIndexChange
]);
const onKeyDown = React.useCallback(
(event) => {
function onHighlightMove(direction) {
if (direction === "selected" && context.value.length > 0) {
context.onHighlightMove("selected");
} else if (direction === "selected") {
context.onHighlightMove("first");
} else {
context.onHighlightMove(direction);
}
}
function onItemSelect() {
if (context.disabled || context.readOnly || !context.highlightedItem)
return;
const { value, label, onSelect } = context.highlightedItem;
if (!value) return;
const itemElement = context.highlightedItem.ref.current;
if (itemElement && onSelect) {
const itemSelectEvent = new CustomEvent(ITEM_SELECT_EVENT, {
bubbles: true
});
itemElement.addEventListener(
ITEM_SELECT_EVENT,
() => onSelect(value),
{
once: true
}
);
dispatchDiscreteCustomEvent(itemElement, itemSelectEvent);
}
if (context.multiple) {
context.onInputValueChange("");
} else {
context.onInputValueChange(label);
context.onSelectedTextChange(label);
context.onHighlightedItemChange(null);
context.onOpenChange(false);
}
context.filterStore.search = "";
context.onValueChange(value);
}
function onMenuOpen(direction) {
if (context.open) return;
context.onOpenChange(true);
requestAnimationFrame(() => {
if (direction) onHighlightMove(direction);
});
}
function onMenuClose() {
if (!context.open) return;
context.onOpenChange(false);
context.onHighlightedItemChange(null);
}
const isNavigationKey = [
"ArrowDown",
"ArrowUp",
"Home",
"End",
"Enter",
"Escape",
"Tab",
"PageUp",
"PageDown"
].includes(event.key);
if (isNavigationKey && event.key !== "Tab") event.preventDefault();
switch (event.key) {
case "Enter":
if (context.multiple && context.hasBadgeList && context.highlightedBadgeIndex > -1) {
const valueToRemove = context.value[context.highlightedBadgeIndex];
if (valueToRemove) {
context.onItemRemove(valueToRemove);
context.onHighlightedBadgeIndexChange(-1);
return;
}
}
if (!context.open) {
if (context.inputValue.trim()) {
onMenuOpen();
} else if (!context.multiple && context.value) {
context.onInputValueChange(context.selectedText);
}
return;
}
if (!context.highlightedItem || context.getIsListEmpty()) {
if (!context.multiple && context.value) {
context.onInputValueChange(context.selectedText);
} else {
context.onInputValueChange("");
}
context.onOpenChange(false);
return;
}
onItemSelect();
break;
case "ArrowDown":
if (context.open) {
onHighlightMove(context.highlightedItem ? "next" : "first");
} else {
onMenuOpen(context.value.length > 0 ? "selected" : "first");
}
break;
case "ArrowUp":
if (context.open) {
onHighlightMove(context.highlightedItem ? "prev" : "last");
} else {
onMenuOpen(context.value.length > 0 ? "selected" : "last");
}
break;
case "ArrowLeft": {
if (!context.multiple || !context.hasBadgeList) return;
const input = event.currentTarget;
const isAtStart = input.selectionStart === 0 && input.selectionEnd === 0;
if (!isAtStart) return;
if (context.open && isAtStart) {
context.onHighlightedItemChange(null);
const values = Array.isArray(context.value) ? context.value : [];
if (values.length > 0) {
event.preventDefault();
context.onOpenChange(false);
requestAnimationFrame(() => {
context.onHighlightedBadgeIndexChange(values.length - 1);
});
}
} else if (!context.open && context.highlightedBadgeIndex > -1) {
event.preventDefault();
context.onHighlightedBadgeIndexChange(
Math.max(0, context.highlightedBadgeIndex - 1)
);
} else if (!context.open && isAtStart) {
const values = Array.isArray(context.value) ? context.value : [];
if (values.length > 0) {
event.preventDefault();
context.onHighlightedBadgeIndexChange(values.length - 1);
}
}
break;
}
case "ArrowRight": {
if (!context.multiple || !context.hasBadgeList) return;
const input = event.currentTarget;
const isAtEnd = input.selectionStart === input.value.length && input.selectionEnd === input.value.length;
if (!isAtEnd) return;
if (!context.open && context.highlightedBadgeIndex > -1) {
event.preventDefault();
const values = Array.isArray(context.value) ? context.value : [];
if (context.highlightedBadgeIndex < values.length - 1) {
context.onHighlightedBadgeIndexChange(
context.highlightedBadgeIndex + 1
);
} else {
context.onHighlightedBadgeIndexChange(-1);
event.currentTarget.focus();
}
}
break;
}
case "Home":
if (context.open) onHighlightMove("first");
break;
case "End":
if (context.open) onHighlightMove("last");
break;
case "PageUp":
if (context.modal && context.open) onHighlightMove("prev");
break;
case "PageDown":
if (context.modal && context.open) onHighlightMove("next");
break;
case "Tab":
if (context.open && context.modal) {
event.preventDefault();
return;
}
onMenuClose();
break;
case "Backspace":
case "Delete":
if (context.multiple && context.hasBadgeList && !context.inputValue && Array.isArray(context.value) && context.value.length > 0) {
if (context.highlightedBadgeIndex > -1) {
const valueToRemove = context.value[context.highlightedBadgeIndex];
if (valueToRemove) {
context.onItemRemove(valueToRemove);
const newIndex = Math.max(
0,
context.highlightedBadgeIndex - 1
);
context.onHighlightedBadgeIndexChange(
context.value.length > 1 ? newIndex : -1
);
}
} else {
const lastValue = context.value[context.value.length - 1];
if (lastValue) {
context.onItemRemove(lastValue);
}
}
}
break;
case "Escape":
if (context.value.length > 0 && !context.multiple) {
context.onInputValueChange(context.selectedText);
} else {
context.onInputValueChange("");
}
onMenuClose();
break;
}
},
[
context.open,
context.onOpenChange,
context.inputValue,
context.onInputValueChange,
context.onHighlightedItemChange,
context.value,
context.highlightedItem,
context.onHighlightMove,
context.selectedText,
context.highlightedBadgeIndex,
context.onHighlightedBadgeIndexChange,
context.onItemRemove,
context.onSelectedTextChange,
context.onValueChange,
context.filterStore,
context.getIsListEmpty,
context.disabled,
context.hasBadgeList,
context.modal,
context.multiple,
context.readOnly
]
);
return /* @__PURE__ */ React.createElement(
Primitive.input,
{
role: "combobox",
id: context.inputId,
autoCapitalize: "off",
autoComplete: "off",
autoCorrect: "off",
spellCheck: "false",
"aria-expanded": context.open,
"aria-controls": context.listId,
"aria-labelledby": context.labelId,
"aria-autocomplete": "list",
"aria-activedescendant": context.highlightedItem?.ref?.current?.id,
"aria-disabled": context.disabled,
"aria-readonly": context.readOnly,
dir: context.dir,
disabled: context.disabled,
readOnly: context.readOnly,
...props,
ref: composedRef,
value: context.inputValue,
onChange: composeEventHandlers(props.onChange, onChange),
onFocus: composeEventHandlers(props.onFocus, onFocus),
onKeyDown: composeEventHandlers(props.onKeyDown, onKeyDown),
onBlur: composeEventHandlers(props.onBlur, onBlur)
}
);
}
);
ComboboxInput.displayName = INPUT_NAME;
var Input = ComboboxInput;
var ITEM_INDICATOR_NAME = "ComboboxItemIndicator";
var ComboboxItemIndicator = React.forwardRef((props, forwardedRef) => {
const { forceMount = false, ...indicatorProps } = props;
const itemContext = useComboboxItemContext(ITEM_INDICATOR_NAME);
if (!forceMount && !itemContext.isSelected) return null;
return /* @__PURE__ */ React.createElement(Primitive.span, { "aria-hidden": "true", ...indicatorProps, ref: forwardedRef });
});
ComboboxItemIndicator.displayName = ITEM_INDICATOR_NAME;
var ItemIndicator = ComboboxItemIndicator;
var ITEM_TEXT_NAME = "ComboboxItemText";
var ComboboxItemText = React.forwardRef((props, forwardedRef) => {
const itemContext = useComboboxItemContext(ITEM_TEXT_NAME);
const composedRef = useComposedRefs(
forwardedRef,
itemContext.onItemLabelChange
);
return /* @__PURE__ */ React.createElement(Primitive.span, { id: itemContext.textId, ...props, ref: composedRef });
});
ComboboxItemText.displayName = ITEM_TEXT_NAME;
var ItemText = ComboboxItemText;
var LABEL_NAME = "ComboboxLabel";
var ComboboxLabel = React.forwardRef(
(props, forwardedRef) => {
const context = useComboboxContext(LABEL_NAME);
return /* @__PURE__ */ React.createElement(
Primitive.label,
{
id: context.labelId,
htmlFor: context.inputId,
...props,
ref: forwardedRef
}
);
}
);
ComboboxLabel.displayName = LABEL_NAME;
var Label = ComboboxLabel;
var LOADING_NAME = "ComboboxLoading";
var ComboboxLoading = React.forwardRef(
(props, forwardedRef) => {
const context = useComboboxContext(LOADING_NAME);
const { value, max, label, ...progressProps } = props;
const progress = useProgress({ value, max });
if (!context.open) return null;
if (progress.state === "complete") return null;
return /* @__PURE__ */ React.createElement(
Primitive.div,
{
"aria-label": label,
...progress.progressProps,
...progressProps,
ref: forwardedRef
}
);
}
);
ComboboxLoading.displayName = LOADING_NAME;
var Loading = ComboboxLoading;
var PORTAL_NAME = "ComboboxPortal";
var ComboboxPortal = React.forwardRef(
(props, forwardedRef) => {
const { container, ...portalProps } = props;
return /* @__PURE__ */ React.createElement(
Portal$1,
{
container,
...portalProps,
ref: forwardedRef,
asChild: true
}
);
}
);
ComboboxPortal.displayName = PORTAL_NAME;
var Portal = ComboboxPortal;
var SEPARATOR_NAME = "ComboboxSeparator";
var ComboboxSeparator = React.forwardRef((props, forwardedRef) => {
const { keepVisible = false, ...separatorProps } = props;
const context = useComboboxContext(SEPARATOR_NAME);
const shouldRender = keepVisible || !context.filterStore.search;
if (!shouldRender) return null;
return /* @__PURE__ */ React.createElement(
Primitive.div,
{
role: "separator",
"aria-hidden": "true",
...separatorProps,
ref: forwardedRef
}
);
});
ComboboxSeparator.displayName = SEPARATOR_NAME;
var Separator = ComboboxSeparator;
var TRIGGER_NAME = "ComboboxTrigger";
var ComboboxTrigger = React.forwardRef((props, forwardedRef) => {
const { disabled, ...triggerProps } = props;
const context = useComboboxContext(TRIGGER_NAME);
const isDisabled = disabled || context.disabled;
return /* @__PURE__ */ React.createElement(
Primitive.button,
{
type: "button",
"aria-haspopup": "listbox",
"aria-expanded": context.open,
"aria-controls": context.listId,
"data-state": context.open ? "open" : "closed",
"data-disabled": isDisabled ? "" : void 0,
dir: context.dir,
disabled: isDisabled,
tabIndex: isDisabled ? void 0 : -1,
...triggerProps,
ref: forwardedRef,
onClick: composeEventHandlers(triggerProps.onClick, async () => {
const newOpenState = !context.open;
context.onOpenChange(newOpenState);
await new Promise((resolve) => requestAnimationFrame(resolve));
const input = context.inputRef.current;
if (input) {
input.focus();
const length = input.value.length;
input.setSelectionRange(length, length);
}
if (!newOpenState) return;
if (context.value.length > 0) {
context.onHighlightMove("selected");
return;
}
if (context.autoHighlight && !context.open) {
context.onHighlightMove("first");
}
}),
onPointerDown: composeEventHandlers(
triggerProps.onPointerDown,
(event) => {
if (context.disabled) return;
const target = event.target;
if (!(target instanceof Element)) return;
if (target.hasPointerCapture(event.pointerId)) {
target.releasePointerCapture(event.pointerId);
}
if (event.button === 0 && event.ctrlKey === false && event.pointerType === "mouse" && !(event.target instanceof HTMLInputElement)) {
event.preventDefault();
}
}
)
}
);
});
ComboboxTrigger.displayName = TRIGGER_NAME;
var Trigger = ComboboxTrigger;
export { Anchor, Arrow, BadgeItem, BadgeItemDelete, BadgeList, Cancel, ComboboxAnchor, ComboboxArrow, ComboboxBadgeItem, ComboboxBadgeItemDelete, ComboboxBadgeList, ComboboxCancel, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxGroupLabel, ComboboxInput, ComboboxItem, ComboboxItemIndicator, ComboboxItemText, ComboboxLabel, ComboboxLoading, ComboboxPortal, ComboboxRoot, ComboboxSeparator, ComboboxTrigger, Content, Empty, Group, GroupLabel, Input, Item, ItemIndicator, ItemText, Label, Loading, Portal, Root, Separator, Trigger };