@baseplate-dev/ui-components
Version:
Shared UI component library
175 lines • 9.77 kB
JavaScript
'use client';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { Command } from 'cmdk';
import { Popover, ScrollArea as ScrollAreaPrimitive } from 'radix-ui';
import * as React from 'react';
import { MdCheck, MdUnfoldMore } from 'react-icons/md';
import { useControlledState } from '#src/hooks/use-controlled-state.js';
import { inputVariants, selectCheckVariants, selectContentVariants, selectItemVariants, } from '#src/styles/index.js';
import { cn, mergeRefs } from '#src/utils/index.js';
import { Button } from '../button/button.js';
import { ScrollBar } from '../scroll-area/scroll-area.js';
const ComboboxContext = React.createContext(null);
const DEFAULT_OPTION = { value: null, label: '' };
/**
* A control that allows users to select an option from a list of options and type to search.
*/
function Combobox({ children, value: controlledValue, onChange, searchQuery: defaultSearchQuery, onSearchQueryChange, label, disabled = false, }) {
const [isOpen, setIsOpen] = React.useState(false);
const [value, setValue] = useControlledState(controlledValue === null ? DEFAULT_OPTION : controlledValue, onChange, DEFAULT_OPTION);
const [searchQuery, setSearchQuery] = useControlledState(defaultSearchQuery, onSearchQueryChange, '');
// the value of the combobox that is currently active
const [activeValue, setActiveValue] = React.useState(value.value ?? '');
// Caches the filter query so we can maintain
// the query when animating the combobox open/close
const [filterQuery, setFilterQuery] = React.useState(searchQuery);
const inputRef = React.useRef(null);
const inputId = React.useId();
const listRef = React.useRef(null);
const contextValue = React.useMemo(() => ({
selectedLabel: value.label ?? '',
selectedValue: value.value,
onSelect: (val, lab) => {
setValue({ value: val, label: lab });
setFilterQuery(searchQuery);
setSearchQuery('');
setIsOpen(false);
},
searchQuery,
setSearchQuery: (query) => {
setFilterQuery(query);
setSearchQuery(query);
setIsOpen(true);
},
setIsOpen: (open) => {
setFilterQuery(searchQuery);
if (!open) {
setActiveValue(value.value ?? '');
setSearchQuery('');
}
setIsOpen(open);
},
isOpen,
inputId,
inputRef,
listRef,
shouldShowItem: (label) => {
if (!filterQuery) {
return true;
}
if (!label) {
return false;
}
return label.toLowerCase().includes(filterQuery.toLowerCase());
},
disabled,
}), [
value,
inputId,
searchQuery,
setSearchQuery,
setValue,
isOpen,
filterQuery,
disabled,
]);
return (_jsx(ComboboxContext.Provider, { value: contextValue, children: _jsx(Popover.Root, { open: isOpen, onOpenChange: contextValue.setIsOpen, children: _jsx(Command, { "aria-disabled": disabled, shouldFilter: false, value: activeValue, onValueChange: (val) => {
setActiveValue(val);
}, label: label, children: children }) }) }));
}
export function useComboboxContext() {
const value = React.useContext(ComboboxContext);
if (!value) {
throw new Error(`useComboboxContext must be used inside a ComboboxContext provider`);
}
return value;
}
function ComboboxInput({ className, placeholder, ref, ...rest }) {
const { setIsOpen, isOpen, inputId, searchQuery, setSearchQuery, selectedLabel, disabled, } = useComboboxContext();
const selectedLabelId = React.useId();
const inputRef = React.useRef(null);
const handleKeydown = React.useCallback((e) => {
const specialKeys = ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'];
if (e.key === 'Escape') {
setIsOpen(false);
}
else if (specialKeys.includes(e.key)) {
setIsOpen(true);
}
}, [setIsOpen]);
return (_jsx(Popover.Anchor, { children: _jsxs("div", { className: "relative", "data-cmdk-input-id": inputId, children: [_jsx(Command.Input, { asChild: true, onKeyDown: handleKeydown, disabled: disabled, onBlur: (e) => {
if (e.relatedTarget &&
e.relatedTarget instanceof Element &&
e.relatedTarget.closest(`[data-combobox-content=""]`)) {
e.target.focus();
}
}, value: searchQuery, onValueChange: setSearchQuery, className: cn(inputVariants(), 'pr-8', className), placeholder: selectedLabel ? undefined : placeholder, onClick: () => {
if (disabled) {
return;
}
if (!isOpen) {
setIsOpen(true);
}
else if (inputRef.current) {
// avoid closing the combobox if the user is selecting text
const hasSelectedEnd = inputRef.current.selectionStart ===
inputRef.current.selectionEnd &&
inputRef.current.selectionEnd === inputRef.current.value.length;
if (hasSelectedEnd) {
setIsOpen(false);
}
}
}, ...rest, "aria-describedby": `${rest['aria-describedby'] ?? ''} ${selectedLabelId}`, ref: mergeRefs(ref, inputRef), children: _jsx("input", { ...(rest['aria-labelledby']
? { 'aria-labelledby': rest['aria-labelledby'] }
: undefined) }) }), _jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center pr-8", children: _jsx("div", { id: selectedLabelId, className: cn(disabled ? 'opacity-50' : '', searchQuery ? 'hidden' : '', 'pointer-events-none truncate py-1 pl-3 text-base md:text-sm'), children: selectedLabel }) }), _jsx(Button, { className: "absolute top-1/2 right-2 -translate-y-1/2 opacity-50", type: "button", variant: "ghost", size: "icon", disabled: disabled, "aria-label": `${isOpen ? 'Close' : 'Open'} combobox`, onClick: () => {
if (disabled) {
return;
}
setIsOpen(!isOpen);
}, onKeyDown: (e) => {
if (!isOpen) {
e.stopPropagation();
}
}, children: _jsx(MdUnfoldMore, { className: "size-4" }) })] }) }));
}
function ComboboxContent({ children, className, maxHeight = '320px', style, ...rest }) {
const { inputId, listRef } = useComboboxContext();
return (_jsx(Popover.Portal, { children: _jsx(Popover.Content, { onOpenAutoFocus: (e) => {
e.preventDefault();
}, onInteractOutside: (e) => {
if (e.target &&
e.target instanceof Element &&
e.target.closest(`[data-cmdk-input-id="${inputId}"]`)) {
e.preventDefault();
}
}, className: cn(selectContentVariants({ popper: 'active' }), className), style: {
'--max-popover-height': maxHeight,
...style,
}, "data-combobox-content": "", ...rest, children: _jsxs(ScrollAreaPrimitive.Root, { type: "auto", className: "relative overflow-hidden", children: [_jsx(ScrollAreaPrimitive.Viewport, { className: cn('h-full w-full rounded-[inherit] p-1', 'max-h-[min(var(--max-popover-height),var(--radix-popover-content-available-height))] w-full min-w-(--radix-popover-trigger-width)'), style: {
'--max-popper-height': maxHeight,
}, children: _jsx(Command.List, { ref: listRef, children: children }) }), _jsx(ScrollBar, {}), _jsx(ScrollAreaPrimitive.Corner, {})] }) }) }));
}
function ComboboxEmpty({ className, ...props }) {
return _jsx(Command.Empty, { className: cn('p-2 text-sm', className), ...props });
}
function ComboboxLoading({ className, ...props }) {
return (_jsx(Command.Loading, { className: cn('flex items-center justify-center p-4 text-sm text-muted-foreground', className), ...props }));
}
const ComboboxGroup = Command.Group;
function ComboboxItem({ value, className, label, children, ref, ...rest }) {
const { selectedValue, onSelect, shouldShowItem } = useComboboxContext();
const itemRef = React.useRef(null);
const extractedLabel = label ?? (typeof children === 'string' ? children.trim() : undefined);
if (!shouldShowItem(extractedLabel ?? value)) {
return _jsx(_Fragment, {});
}
return (_jsxs(Command.Item, { value: value ?? '', onSelect: () => {
onSelect(value, extractedLabel);
}, className: cn(selectItemVariants(), className), ...rest, ref: mergeRefs(ref, itemRef), children: [children, _jsx(MdCheck, { className: cn(selectCheckVariants(), value === selectedValue ? 'opacity-100' : 'opacity-0') })] }));
}
function ComboboxAction({ value, className, children, onClick, ref, ...rest }) {
const itemRef = React.useRef(null);
return (_jsx(Command.Item, { value: value, onSelect: onClick, className: cn(selectItemVariants(), className), ...rest, ref: mergeRefs(ref, itemRef), children: children }));
}
export { Combobox, ComboboxAction, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLoading, };
//# sourceMappingURL=combobox.js.map