import * as React from "react"; import { cn } from "@/lib/utils"; import { CheckIcon, ChevronDownIcon } from "lucide-react" // Context for Select interface SelectOption { value: string; label: string } interface SelectContextType { open: boolean; setOpen: React.Dispatch>; selected: string; setSelected: (val: string) => void; triggerRef: HTMLButtonElement | null; setTriggerRef: (ref: HTMLButtonElement | null) => void; highlightedIndex: number; setHighlightedIndex: React.Dispatch>; itemsRef: React.MutableRefObject<(HTMLDivElement | null)[]>; options: SelectOption[]; } const SelectContext = React.createContext(null); function Select({ value, onValueChange, options, children, ...props }: { value: string; onValueChange: (val: string) => void; options: Array<{ value: string; label: string }>; children: React.ReactNode; }) { const [open, setOpen] = React.useState(false); const [selected, setSelected] = React.useState(value); const [triggerRef, setTriggerRef] = React.useState(null); const [highlightedIndex, setHighlightedIndex] = React.useState(-1); const itemsRef = React.useRef<(HTMLDivElement | null)[]>([]); const containerRef = React.useRef(null); // Handle click outside React.useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(event.target as Node) && triggerRef !== event.target ) { setOpen(false); } }; // Handle escape key const handleEscapeKey = (event: KeyboardEvent) => { if (event.key === "Escape") { setOpen(false); } }; if (open) { document.addEventListener("mousedown", handleClickOutside); document.addEventListener("keydown", handleEscapeKey); } return () => { document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("keydown", handleEscapeKey); }; }, [open, triggerRef]); React.useEffect(() => { setSelected(value); }, [value]); const contextValue = React.useMemo(() => ({ open, setOpen, selected, setSelected: (val: string) => { setSelected(val); onValueChange(val); setOpen(false); }, triggerRef, setTriggerRef, highlightedIndex, setHighlightedIndex, itemsRef, options, }), [open, selected, onValueChange, highlightedIndex, options]); return (
{children}
); } function SelectTrigger({ children, className, ...props }: React.ComponentProps<'button'>) { const ctx = React.useContext(SelectContext)!; return ( ); } interface SelectItemProps { value: string; children: React.ReactNode; index?: number; className?: string; [key: string]: any; } function SelectItem({ value, children, index, className, ...props }: SelectItemProps) { const ctx = React.useContext(SelectContext)!; const selected = ctx.selected === value; const highlighted = ctx.highlightedIndex === index; const ref = React.useRef(null); React.useEffect(() => { if (highlighted && ref.current) { ref.current.scrollIntoView({ block: "nearest" }); } if (typeof index === 'number') { ctx.itemsRef.current[index] = ref.current; } }, [highlighted, index]); return (
ctx.setSelected(value)} onMouseEnter={() => typeof index === 'number' && ctx.setHighlightedIndex(index)} data-value={value} {...props} > {selected && } {children}
); } SelectItem.displayName = 'SelectItem'; function isSelectItemElement(child: any): child is React.ReactElement { return ( React.isValidElement(child) && typeof child.type === 'function' && (child.type as any).displayName === 'SelectItem' ); } function SelectContent({ children, className }: { children: React.ReactNode; className?: string }) { const ctx = React.useContext(SelectContext)!; if (!ctx.open) return null; return (
{ if (e.key === "ArrowDown") { e.preventDefault(); ctx.setHighlightedIndex((i: number) => Math.min(i + 1, ctx.itemsRef.current.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); ctx.setHighlightedIndex((i: number) => Math.max(i - 1, 0)); } else if (e.key === "Enter" && ctx.highlightedIndex >= 0) { e.preventDefault(); const item = ctx.itemsRef.current[ctx.highlightedIndex]; if (item) item.click(); } else if (e.key === "Escape") { ctx.setOpen(false); } }} > {React.Children.map(children, (child, idx) => { if (isSelectItemElement(child)) { return React.cloneElement(child as React.ReactElement, { index: idx }); } return child; })}
); } function SelectValue({ placeholder }: { placeholder?: string }) { const ctx = React.useContext(SelectContext)!; const selectedOption = ctx.options?.find((opt: any) => opt.value === ctx.selected); return ( {selectedOption ? selectedOption.label : placeholder} ); } export { Select, SelectTrigger, SelectContent, SelectItem, SelectValue };