"use client"; import { InputCommandGroup, InputCommandItem, InputCommandList, InputCommandInput, } from "@/src/components/ui/input-command"; import { Command as CommandPrimitive } from "cmdk"; import { useState, useRef, useCallback, type KeyboardEvent } from "react"; import { Check } from "lucide-react"; import { cn } from "@/src/utils/tailwind"; export type AutoCompleteOption = { value: string; label: string; }; type AutoCompleteProps = { options: AutoCompleteOption[]; value: AutoCompleteOption; onValueChange?: (value: AutoCompleteOption) => void; disabled?: boolean; placeholder?: string; createLabel: string; }; export const AutoComplete = ({ options, placeholder, value, onValueChange, disabled, createLabel, }: AutoCompleteProps) => { const inputRef = useRef(null); const [isOpen, setOpen] = useState(false); const [selected, setSelected] = useState(value); const [inputValue, setInputValue] = useState(value.label || ""); const handleKeyDown = useCallback( (event: KeyboardEvent) => { const input = inputRef.current; if (!input) { return; } // Keep the options displayed when the user is typing if (!isOpen) { setOpen(true); } // This is not a default behavior of the field if (event.key === "Enter" && input.value !== "") { const optionToSelect = options.find( (option) => option.label === input.value, ); if (optionToSelect) { setSelected(optionToSelect); onValueChange?.(optionToSelect); } } if (event.key === "Escape") { input.blur(); } }, [isOpen, options, onValueChange], ); const handleBlur = useCallback(() => { setOpen(false); setInputValue(selected.label); }, [selected]); const handleSelectOption = useCallback( (selectedOption: AutoCompleteOption) => { setInputValue(selectedOption.label); setSelected(selectedOption); onValueChange?.(selectedOption); // This is a hack to prevent the input from being focused after the user selects an option // We can call this hack: "The next tick" setTimeout(() => { inputRef.current?.blur(); }, 0); }, [onValueChange], ); return (
setOpen(true)} placeholder={placeholder} disabled={disabled} />
{isOpen ? (
{options.length > 0 ? ( {options.map((option) => { const isSelected = selected.value === option.value; return ( { event.preventDefault(); event.stopPropagation(); }} onSelect={() => handleSelectOption(option)} className={cn( "flex w-full items-center gap-2", !isSelected ? "pl-8" : null, )} > {isSelected ? : null} {option.label} ); })} ) : null} { event.preventDefault(); event.stopPropagation(); }} onSelect={() => handleSelectOption({ value: inputValue, label: inputValue }) } createLabel={createLabel} {...{ inputValue, options }} />
) : ( // CommandPrimitive expects a InputCommandList child, fix to prevent errors introduced in cmdk v1.0.0 )}
); }; const InputCommandItemCreate = ({ inputValue, options, createLabel, onSelect, onMouseDown, }: { inputValue: string; options: AutoCompleteOption[]; createLabel: string; onSelect: () => void; onMouseDown: (event: React.MouseEvent) => void; }) => { const hasNoOption = !options .map(({ value }) => value) .includes(inputValue.toLowerCase()); const render = inputValue !== "" && hasNoOption; if (!render) return null; return (
{createLabel}: "{inputValue}" ); };