import * as React from 'react' import { useState, useRef, useEffect } from 'react' import { FieldWrapper } from '../shared/FieldWrapper' import type { SAILLabelPosition, SAILMarginSize } from '../../types/sail' import { ChevronDown, X, Search } from 'lucide-react' type SearchDisplay = "AUTO" | "ON" | "OFF" interface DropdownFieldBaseProps { /** Whether to allow multiple selections */ multiple: boolean /** Text to display as the field label */ label?: string /** Determines where the label appears */ labelPosition?: SAILLabelPosition /** Supplemental text about this field */ instructions?: string /** Determines if a value is required to submit the form */ required?: boolean /** Determines if the field should display as grayed out */ disabled?: boolean /** Text to display when nothing is selected */ placeholder?: string /** Array of options for the user to select */ choiceLabels: any[] /** Array of values associated with the corresponding choices */ choiceValues: any[] /** Current selected value(s) */ value?: any /** Validation errors to display below the field */ validations?: string[] /** Callback when selection changes */ saveInto?: (value: any) => void /** Callback when selection changes (React-style alias for saveInto) */ onChange?: (value: any) => void /** Validation group name (no spaces) */ validationGroup?: string /** Custom message when field is required and not provided */ requiredMessage?: string /** Displays a help icon with tooltip text */ helpTooltip?: string /** Additional text for screen readers */ accessibilityText?: string /** Determines whether component is displayed */ showWhen?: boolean /** Determines when search box displays */ searchDisplay?: SearchDisplay /** Data source (record type) - not implemented in prototype */ data?: any /** Sort configurations - not implemented in prototype */ sort?: any[] /** Space added above component */ marginAbove?: SAILMarginSize /** Space added below component */ marginBelow?: SAILMarginSize /** Additional Tailwind classes for prototype-specific styling (not part of SAIL API) */ className?: string } export const DropdownFieldBase: React.FC = ({ multiple, label, labelPosition = "ABOVE", instructions, required = false, disabled = false, placeholder, choiceLabels, choiceValues, value, validations = [], saveInto, onChange, validationGroup: _validationGroup, requiredMessage, helpTooltip, accessibilityText, showWhen = true, searchDisplay = "AUTO", data: _data, sort: _sort, marginAbove = "NONE", marginBelow = "STANDARD", className }) => { // Visibility control if (!showWhen) return null const inputId = `dropdown-${Math.random().toString(36).substr(2, 9)}` const [isOpen, setIsOpen] = useState(false) const [searchTerm, setSearchTerm] = useState('') const dropdownRef = useRef(null) const searchInputRef = useRef(null) // Determine if search should be shown const shouldShowSearch = searchDisplay === "ON" || (searchDisplay === "AUTO" && choiceLabels.length > 11) // Filter choices based on search term const filteredChoices = searchTerm ? choiceLabels .map((label, index) => ({ label, value: choiceValues[index], index })) .filter(choice => String(choice.label).toLowerCase().includes(searchTerm.toLowerCase()) ) : choiceLabels.map((label, index) => ({ label, value: choiceValues[index], index })) // Get selected values as array const selectedValues = multiple ? (Array.isArray(value) ? value : (value ? [value] : [])) : (value !== undefined && value !== null ? [value] : []) // Get display text for selected items const getDisplayText = () => { if (selectedValues.length === 0) { return placeholder || 'Select...' } if (multiple) { return selectedValues .map(val => { const index = choiceValues.indexOf(val) return index >= 0 ? choiceLabels[index] : val }) .join(', ') } else { const index = choiceValues.indexOf(value) return index >= 0 ? choiceLabels[index] : value } } // Handle selection const handleSelect = (choiceValue: any) => { const handler = onChange || saveInto if (!handler || disabled) return if (multiple) { const currentValues = Array.isArray(value) ? value : [] const newValues = currentValues.includes(choiceValue) ? currentValues.filter(v => v !== choiceValue) : [...currentValues, choiceValue] handler(newValues.length > 0 ? newValues : null) } else { handler(choiceValue) setIsOpen(false) } } // Handle clear const handleClear = (e: React.MouseEvent) => { e.stopPropagation() const handler = onChange || saveInto if (handler && !disabled) { handler(null) } } // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false) setSearchTerm('') } } if (isOpen) { document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) } }, [isOpen]) // Focus search input when dropdown opens useEffect(() => { if (isOpen && shouldShowSearch && searchInputRef.current) { searchInputRef.current.focus() } }, [isOpen, shouldShowSearch]) // Show validations const showValidations = validations.length > 0 // Show required message const showRequiredMessage = required && selectedValues.length === 0 && requiredMessage // Render the dropdown field const dropdownElement = (
{/* Dropdown trigger */} {/* Dropdown menu */} {isOpen && (
{/* Search input */} {shouldShowSearch && (
setSearchTerm(e.target.value)} placeholder="Search..." className="w-full pl-8 pr-3 py-1.5 border border-gray-300 rounded-sm text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:border-blue-500" />
)} {/* Options list */}
{filteredChoices.length === 0 ? (
No results found
) : ( filteredChoices.map(({ label: choiceLabel, value: choiceValue }) => { const isSelected = selectedValues.includes(choiceValue) return ( ) }) )}
)}
) // Footer content (validations and required message) const footerContent = ( <> {showValidations && ( )} {showRequiredMessage && (

{requiredMessage}

)} ) return ( {dropdownElement} ) }