"use client"; import { Check, ChevronDown } from "lucide-react"; import { useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react"; import { createPortal } from "react-dom"; import { cn } from "@/lib/utils"; export interface SelectOption { value: string; label: string; hint?: string; } /** * Custom select whose dropdown is portalled to `document.body` so it * never gets clipped by a parent `overflow-y-auto` or `overflow-hidden`. * Position is computed via `getBoundingClientRect()` + fixed positioning. */ export function Select({ value, onChange, options, placeholder = "Select…", className, size = "md", }: { value: string | null; onChange: (value: string) => void; options: SelectOption[]; placeholder?: string; className?: string; size?: "sm" | "md"; }) { const [open, setOpen] = useState(false); const triggerRef = useRef(null); const dropdownRef = useRef(null); const [pos, setPos] = useState<{ top: number; left: number; width: number }>({ top: 0, left: 0, width: 0, }); // Measure trigger to position the portalled dropdown const measure = useCallback(() => { if (!triggerRef.current) return; const rect = triggerRef.current.getBoundingClientRect(); setPos({ top: rect.bottom + 6, left: rect.left, width: rect.width }); }, []); useLayoutEffect(() => { if (open) measure(); }, [open, measure]); // Reposition on scroll / resize while open useEffect(() => { if (!open) return; const onReposition = () => measure(); window.addEventListener("scroll", onReposition, true); window.addEventListener("resize", onReposition); return () => { window.removeEventListener("scroll", onReposition, true); window.removeEventListener("resize", onReposition); }; }, [open, measure]); // Close on outside click (covers both trigger and dropdown) useEffect(() => { if (!open) return; const onDoc = (e: MouseEvent) => { const t = e.target as Node; if (triggerRef.current?.contains(t) || dropdownRef.current?.contains(t)) return; setOpen(false); }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, [open]); // Close on Escape useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open]); const selected = options.find((o) => o.value === value); return ( <>
{open && createPortal(
{options.length === 0 && (
No options
)} {options.map((o) => ( ))}
, document.body, )} ); }