import React, { useState, useRef, useEffect } from 'react'; import { cn } from '@/lib/utils'; import { ChevronDown, Check, Search, X } from 'lucide-react'; import { Input } from '@/components/ui/input'; export interface SearchableDropdownOption { value: string; label: string; disabled?: boolean; } interface SearchableDropdownProps { options: SearchableDropdownOption[]; value: string; onChange: (value: string) => void; placeholder?: string; searchPlaceholder?: string; className?: string; triggerClassName?: string; dropdownClassName?: string; disabled?: boolean; error?: boolean; 'aria-label'?: string; 'aria-describedby'?: string; emptyMessage?: string; } export const SearchableDropdown: React.FC = ({ options, value, onChange, placeholder = "Select an option", searchPlaceholder = "Search...", className = "", triggerClassName = "", dropdownClassName = "", disabled = false, error = false, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, emptyMessage = "No options found" }) => { const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const containerRef = useRef(null); const dropdownRef = useRef(null); const triggerRef = useRef(null); const searchInputRef = useRef(null); // Get the selected option label const selectedOption = options.find(option => option.value === value); const displayValue = selectedOption ? selectedOption.label : placeholder; // Filter options based on search term const filteredOptions = options.filter(option => option.label.toLowerCase().includes(searchTerm.toLowerCase()) ); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false); setSearchTerm(''); } }; const handleTouchStart = (event: TouchEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false); setSearchTerm(''); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); document.addEventListener('touchstart', handleTouchStart, { passive: true }); } return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('touchstart', handleTouchStart); }; }, [isOpen]); // Focus search input when dropdown opens useEffect(() => { if (isOpen && searchInputRef.current) { setTimeout(() => { searchInputRef.current?.focus(); }, 100); } }, [isOpen]); // Handle keyboard navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (!isOpen) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!disabled) { setIsOpen(true); } } return; } if (e.key === 'Escape') { setIsOpen(false); setSearchTerm(''); triggerRef.current?.focus(); } }; if (isOpen) { window.addEventListener('keydown', handleKeyDown); } return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [isOpen, disabled]); // Handle scroll locking when dropdown is open useEffect(() => { if (isOpen && dropdownRef.current) { // Scroll selected item into view const selectedItem = dropdownRef.current.querySelector('[aria-selected="true"]'); if (selectedItem) { selectedItem.scrollIntoView({ block: 'nearest' }); } } }, [isOpen, value]); // Toggle dropdown const handleToggle = (e: React.MouseEvent | React.KeyboardEvent) => { e.stopPropagation(); if (!disabled) { setIsOpen(!isOpen); if (!isOpen) { setSearchTerm(''); } } }; // Select an option const handleSelect = (optionValue: string, e: React.MouseEvent | React.KeyboardEvent) => { e.stopPropagation(); e.preventDefault(); const option = options.find(opt => opt.value === optionValue); if (option && !option.disabled) { onChange(optionValue); setIsOpen(false); setSearchTerm(''); triggerRef.current?.focus(); } }; // Clear search const handleClearSearch = () => { setSearchTerm(''); searchInputRef.current?.focus(); }; return (
{/* Trigger button */}
{ e.stopPropagation(); if (!disabled) { setIsOpen(!isOpen); } }} aria-haspopup="listbox" aria-expanded={isOpen} aria-label={ariaLabel} aria-describedby={ariaDescribedBy} role="combobox" tabIndex={disabled ? -1 : 0} > {displayValue}
{/* Dropdown menu */} {isOpen && (
{/* Search input */}
setSearchTerm(e.target.value)} className="pl-8 pr-8 h-8 text-sm" onClick={(e) => e.stopPropagation()} /> {searchTerm && ( )}
{/* Options list */}
{filteredOptions.length > 0 ? (
{filteredOptions.map((option) => (
!option.disabled && handleSelect(option.value, e)} onKeyDown={(e) => { if ((e.key === 'Enter' || e.key === ' ') && !option.disabled) { handleSelect(option.value, e); } }} onTouchEnd={(e) => { e.stopPropagation(); if (!option.disabled) { handleSelect(option.value, e as any); } }} > {option.label} {option.value === value && ( )}
))}
) : (
{emptyMessage}
)}
)}
); };