import React, { useEffect, useRef, useState } from 'react'; import { clsx } from 'clsx'; import { Check, ChevronsUpDown } from 'lucide-react'; import Input from './Input'; import Button from './Button'; export interface ComboboxOption { value: string; label: string; disabled?: boolean; } export interface ComboboxProps { options: ComboboxOption[]; value?: string; onChange?: (value: string) => void; onSearch?: (searchTerm: string) => void; placeholder?: string; searchPlaceholder?: string; className?: string; disabled?: boolean; allowClear?: boolean; filterOptions?: (options: ComboboxOption[], searchTerm: string) => ComboboxOption[]; } /** * Combobox/Autocomplete component */ export const Combobox = ({ options, value, onChange, onSearch, placeholder = 'Select...', searchPlaceholder = 'Search...', className, disabled, allowClear = false, filterOptions, }: ComboboxProps) => { const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const comboboxRef = useRef(null); const inputRef = useRef(null); const defaultFilter = (opts: ComboboxOption[], term: string) => { if (!term) return opts; const lowerTerm = term.toLowerCase(); return opts.filter( (opt) => opt.label.toLowerCase().includes(lowerTerm) || opt.value.toLowerCase().includes(lowerTerm) ); }; const filter = filterOptions || defaultFilter; const filteredOptions = filter(options, searchTerm); const selectedOption = options.find((opt) => opt.value === value); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (comboboxRef.current && !comboboxRef.current.contains(event.target as Node)) { setIsOpen(false); setSearchTerm(''); } }; const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape' && isOpen) { setIsOpen(false); setSearchTerm(''); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleEscape); inputRef.current?.focus(); } return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleEscape); }; }, [isOpen]); const handleSelect = (optionValue: string) => { onChange?.(optionValue); setIsOpen(false); setSearchTerm(''); }; const handleSearchChange = (term: string) => { setSearchTerm(term); onSearch?.(term); }; const handleClear = (e: React.MouseEvent) => { e.stopPropagation(); onChange?.(''); setSearchTerm(''); }; return (
{ if (!isOpen) setIsOpen(true); handleSearchChange(e.target.value); }} onFocus={() => setIsOpen(true)} placeholder={placeholder} disabled={disabled} className="pr-20" />
{allowClear && value && ( )}
{isOpen && (
{filteredOptions.length === 0 ? (
No options found
) : (
    {filteredOptions.map((option) => (
  • ))}
)}
)}
); };