"use client"; import React, { Children, type ReactElement, type ReactNode, forwardRef, useId, useState, useRef, useCallback, memo, useImperativeHandle, useEffect, } from "react"; import { useClickOutside } from "../../hooks"; import { SelectInputView } from "./SelectInput.view"; import type { SelectInputProps } from "./SelectInput.types"; import { normalizeState, normalizeSmallMediumLargeSize, normalizeLabelVariant } from "../../../lib/propNormalization"; const SelectInputContainer = forwardRef( ( { id, label, labelVariant: labelVariantProp, size: sizeProp, state: externalStateProp = "default", disabled = false, error = false, placeholder = "Choose an option", className = "", children, value, onChange, options, ...props }, ref, ) => { // Normalize props to handle both PascalCase (Figma) and lowercase (codebase) // Note: labelVariant and size are normalized for future use but not yet implemented in the view const _labelVariant = labelVariantProp ? normalizeLabelVariant(labelVariantProp) : undefined; const _size = sizeProp ? normalizeSmallMediumLargeSize(sizeProp) : undefined; // Mark as intentionally unused for future implementation void _labelVariant; void _size; const externalState = normalizeState(externalStateProp); const generatedId = useId(); const selectId = id || `select-input-${generatedId}`; const labelId = `${selectId}-label`; const [isOpen, setIsOpen] = useState(false); const [selectedValue, setSelectedValue] = useState(value || ""); const selectRef = useRef(null); const menuRef = useRef(null); // Internal state management: track if focused and how (mouse vs keyboard) const [isFocused, setIsFocused] = useState(false); const [focusMethod, setFocusMethod] = useState<"mouse" | "keyboard" | null>(null); const wasMouseDownRef = useRef(false); // Determine if we should auto-manage focus (only when state is "default" or undefined) const shouldAutoManageFocus = externalState === "default" || externalState === undefined; // Sync internal state with external value prop useEffect(() => { if (value !== undefined && value !== selectedValue) { setSelectedValue(value); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); useImperativeHandle( ref, () => selectRef.current as HTMLButtonElement | null, ); // Handle click outside to close menu useClickOutside([menuRef, selectRef], () => setIsOpen(false), isOpen); // Handle option selection const handleOptionSelect = useCallback( (optionValue: string, optionText: string) => { setSelectedValue(optionValue); setIsOpen(false); if (onChange) { onChange({ target: { value: optionValue, text: optionText } }); } if (selectRef.current) { selectRef.current.focus(); } }, [onChange], ); // Handle mouse down to detect mouse clicks const handleMouseDown = useCallback(() => { if (!disabled && shouldAutoManageFocus) { wasMouseDownRef.current = true; } }, [disabled, shouldAutoManageFocus]); // Handle select button click const handleSelectClick = useCallback(() => { if (!disabled) { setIsOpen(!isOpen); } }, [disabled, isOpen]); // Handle keyboard navigation const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (disabled) return; if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setIsOpen(!isOpen); } else if (e.key === "Escape") { setIsOpen(false); } }, [disabled, isOpen], ); // Handle focus to detect mouse vs keyboard const handleFocus = useCallback(() => { if (disabled) return; const method = wasMouseDownRef.current ? "mouse" : "keyboard"; if (shouldAutoManageFocus) { setIsFocused(true); setFocusMethod(method); wasMouseDownRef.current = false; } }, [disabled, shouldAutoManageFocus]); // Handle blur const handleBlur = useCallback(() => { if (shouldAutoManageFocus) { setIsFocused(false); setFocusMethod(null); wasMouseDownRef.current = false; } }, [shouldAutoManageFocus]); // Determine actual state: // - Active: when clicked (mouse focus) or when dropdown is open // - Focus: when tabbed (keyboard focus) // - Default: when not focused const actualState = shouldAutoManageFocus ? isOpen || isFocused ? focusMethod === "mouse" || isOpen ? "active" : "focus" : "default" : externalState; // Determine if select is filled (has selected value) const isFilled = Boolean(selectedValue && selectedValue.trim().length > 0); // Get display text for selected value const getDisplayText = (): string => { if (!selectedValue) return placeholder; if (options && Array.isArray(options)) { const selectedOption = options.find( (option) => option.value === selectedValue, ); return selectedOption ? selectedOption.label : placeholder; } const selectedOption = Children.toArray(children).find( ( child, ): child is ReactElement<{ value: string; children: ReactNode; }> => { if (!React.isValidElement(child)) return false; const props = child.props as { value?: string; children?: ReactNode; }; return props.value === selectedValue; }, ); return selectedOption ? String(selectedOption.props.children) : placeholder; }; return ( ); }, ); SelectInputContainer.displayName = "SelectInput"; export default memo(SelectInputContainer);