import React, { useState, useRef, useEffect } from "react"; import { ChevronDown, X } from "lucide-react"; interface Option { label: string; value: string | number; } interface CustomSearchableDropdownProps extends Omit, 'onChange'> { label?: string; options: Option[]; value?: string | number; onChange?: (value: string) => void; placeholder?: string; leftIcon?: React.ReactNode; error?: string; } const CustomSearchableDropdown = React.forwardRef< HTMLDivElement, CustomSearchableDropdownProps >( ( { label, options, value, onChange, placeholder = "Select...", disabled, className = "", required, leftIcon, ...props }, ref ) => { const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [highlightedIndex, setHighlightedIndex] = useState(0); const dropdownRef = useRef(null); const inputRef = useRef(null); // Sync search term with selected value on mount or value update useEffect(() => { const selectedOption = options.find((opt) => String(opt.value) === String(value)); if (selectedOption) { setSearchTerm(selectedOption.label); } else { setSearchTerm(""); } }, [value, options]); const filteredOptions = options.filter((option) => option.label.toLowerCase().includes(searchTerm.toLowerCase()) ); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) ) { setIsOpen(false); // Revert to selected value label if closing without selection const selectedOption = options.find((opt) => String(opt.value) === String(value)); if (selectedOption) { setSearchTerm(selectedOption.label); } else if (!value) { setSearchTerm(""); } } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [value, options]); useEffect(() => { if (!isOpen) { setHighlightedIndex(0); } }, [isOpen]); const handleInputChange = (e: React.ChangeEvent) => { setSearchTerm(e.target.value); setIsOpen(true); if (e.target.value === "") { onChange?.(""); // Clear selection if input is cleared } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (!isOpen && e.key !== "Tab") { setIsOpen(true); } switch (e.key) { case "ArrowDown": e.preventDefault(); setHighlightedIndex((prev) => prev < filteredOptions.length - 1 ? prev + 1 : prev ); break; case "ArrowUp": e.preventDefault(); setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev)); break; case "Enter": e.preventDefault(); if (isOpen && filteredOptions[highlightedIndex]) { handleSelect(filteredOptions[highlightedIndex]); } break; case "Escape": e.preventDefault(); setIsOpen(false); break; case "Tab": setIsOpen(false); break; } }; const handleSelect = (option: Option) => { onChange?.(String(option.value)); setSearchTerm(option.label); setIsOpen(false); }; const handleClear = (e: React.MouseEvent) => { e.stopPropagation(); onChange?.(""); setSearchTerm(""); inputRef.current?.focus(); }; const handleFocus = () => { if (!disabled) setIsOpen(true); }; return (
{label && ( )}
{leftIcon && ( {leftIcon} )}
{searchTerm && !disabled && ( )}
{/* Dropdown Menu */} {isOpen && !disabled && filteredOptions.length > 0 && (
{filteredOptions.map((option, index) => ( ))}
)} {isOpen && !disabled && filteredOptions.length === 0 && (
No results found
)}
{props.error &&

{props.error}

}
); } ); CustomSearchableDropdown.displayName = "CustomSearchableDropdown"; export default CustomSearchableDropdown;