import { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; import type { ReactElement, SelectHTMLAttributes } from 'react'; import { ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; interface SelectOption { value: string; label: string; } interface FormSelectProps extends Omit, 'onChange'> { label: string; required?: boolean; error?: string; helperText?: string; options: SelectOption[]; placeholder?: string; onValueChange?: (value: string) => void; } export const FormSelect = ({ label, required = false, error, helperText, options, placeholder = 'Select Item', className, id, value, onValueChange, ...props }: FormSelectProps): ReactElement => { const [isOpen, setIsOpen] = useState(false); const [selectedValue, setSelectedValue] = useState(value as string || ''); const [dropdownStyle, setDropdownStyle] = useState<{ top?: string; bottom?: string; left: string; width: string }>({ left: '0', width: '0' }); const dropdownRef = useRef(null); const buttonRef = useRef(null); const dropdownMenuRef = useRef(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && dropdownMenuRef.current && !dropdownMenuRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; const handleScroll = (event: Event) => { // Don't close if scrolling inside the dropdown menu itself const target = event.target as HTMLElement; if (dropdownMenuRef.current && (dropdownMenuRef.current === target || dropdownMenuRef.current.contains(target))) { return; } setIsOpen(false); }; if (isOpen && buttonRef.current) { document.addEventListener('mousedown', handleClickOutside); window.addEventListener('scroll', handleScroll, true); // Calculate position when dropdown opens const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; const dropdownHeight = 240; // max-h-60 = 240px // Determine if should open upward or downward const shouldOpenUp = spaceBelow < dropdownHeight && spaceAbove > spaceBelow; // Calculate dropdown position const left = rect.left; const width = rect.width; if (shouldOpenUp) { // Position above the button const bottom = window.innerHeight - rect.top; setDropdownStyle({ bottom: `${bottom}px`, left: `${left}px`, width: `${width}px`, }); } else { // Position below the button const top = rect.bottom; setDropdownStyle({ top: `${top}px`, left: `${left}px`, width: `${width}px`, }); } } return () => { document.removeEventListener('mousedown', handleClickOutside); window.removeEventListener('scroll', handleScroll, true); }; }, [isOpen]); useEffect(() => { if (value !== undefined) { setSelectedValue(value as string); } }, [value]); const fieldId = id || `select-${label.toLowerCase().replace(/\s+/g, '-')}`; const hasError = Boolean(error); const selectedOption = options.find((opt) => opt.value === selectedValue); const handleSelect = (optionValue: string) => { setSelectedValue(optionValue); if (onValueChange) { onValueChange(optionValue); } setIsOpen(false); }; return (
{isOpen && buttonRef.current && createPortal(
    {options.map((option) => (
  • ))}
, document.body )}
{error && ( )} {helperText && !error && (

{helperText}

)}
); };