import React, { useState, useRef, useEffect } from 'react'; import { cn } from '@/lib/utils'; import { ChevronDown, Check } from 'lucide-react'; export interface CustomSelectOption { value: string; label: string; } interface CustomSelectProps { options: CustomSelectOption[]; value: string; onChange: (value: string) => void; placeholder?: string; className?: string; triggerClassName?: string; dropdownClassName?: string; disabled?: boolean; } export const CustomSelect: React.FC = ({ options, value, onChange, placeholder = "Select an option", className = "", triggerClassName = "", dropdownClassName = "", disabled = false, }) => { const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); const dropdownRef = 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) return; if (e.key === 'Escape') { setIsOpen(false); } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); const currentIndex = options.findIndex(option => option.value === value); let newIndex = currentIndex; if (e.key === 'ArrowDown') { newIndex = currentIndex < options.length - 1 ? currentIndex + 1 : 0; } else { newIndex = currentIndex > 0 ? currentIndex - 1 : options.length - 1; } onChange(options[newIndex].value); } }; if (isOpen) { window.addEventListener('keydown', handleKeyDown); } return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [isOpen, options, value, onChange]); // 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) => { e.stopPropagation(); if (!disabled) { setIsOpen(!isOpen); } }; // Select an option const handleSelect = (optionValue: string, e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); onChange(optionValue); setIsOpen(false); }; return (
{/* Trigger button */}
{ e.stopPropagation(); if (!disabled) { setIsOpen(!isOpen); } }} aria-haspopup="listbox" aria-expanded={isOpen} role="combobox" tabIndex={0} > {displayValue}
{/* Dropdown menu */} {isOpen && (
{options.map((option) => (
handleSelect(option.value, e)} onTouchEnd={(e) => { e.stopPropagation(); handleSelect(option.value, e as any); }} > {option.label} {option.value === value && ( )}
))}
)}
); };