import React, { useState, useRef, useEffect } from 'react'; import { cn } from '@/lib/utils'; import { ChevronDown, Check } from 'lucide-react'; export interface CustomDropdownOption { value: string; label: string; disabled?: boolean; } interface CustomDropdownProps { options: CustomDropdownOption[]; value: string; onChange: (value: string) => void; placeholder?: string; className?: string; triggerClassName?: string; dropdownClassName?: string; disabled?: boolean; error?: boolean; 'aria-label'?: string; 'aria-describedby'?: string; } export const CustomDropdown: React.FC = ({ options, value, onChange, placeholder = "Select an option", className = "", triggerClassName = "", dropdownClassName = "", disabled = false, error = false, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, }) => { const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); const dropdownRef = useRef(null); const triggerRef = useRef(null); // Get the selected option label const selectedOption = options.find(option => option.value === value); const displayValue = selectedOption ? selectedOption.label : placeholder; // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false); } }; const handleTouchStart = (event: TouchEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); document.addEventListener('touchstart', handleTouchStart, { passive: true }); } return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('touchstart', handleTouchStart); }; }, [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); triggerRef.current?.focus(); } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); const availableOptions = options.filter(option => !option.disabled); const currentIndex = availableOptions.findIndex(option => option.value === value); let newIndex = currentIndex; if (e.key === 'ArrowDown') { newIndex = currentIndex < availableOptions.length - 1 ? currentIndex + 1 : 0; } else { newIndex = currentIndex > 0 ? currentIndex - 1 : availableOptions.length - 1; } onChange(availableOptions[newIndex].value); } else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); // Enter/Space on an option is handled by the option's click handler } }; if (isOpen) { window.addEventListener('keydown', handleKeyDown); } return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [isOpen, options, value, onChange, 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); } }; // 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); triggerRef.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 && (
{options.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 && ( )}
))}
)}
); };