import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { ArrowDownUp, Check } from 'lucide-react'; export interface SortOption { value: V; label: string; } interface Props { value: V; options: SortOption[]; onChange: (value: V) => void; ariaLabel?: string; } export default function SortDropdown({ value, options, onChange, ariaLabel }: Props) { const [open, setOpen] = useState(false); const [popStyle, setPopStyle] = useState({}); const triggerRef = useRef(null); const popRef = useRef(null); const current = options.find(o => o.value === value); const updatePopStyle = () => { if (!triggerRef.current) return; const rect = triggerRef.current.getBoundingClientRect(); const MARGIN = 6; const WIDTH = Math.max(rect.width, 220); const MAX_H = 300; const spaceBelow = window.innerHeight - rect.bottom - MARGIN; const spaceAbove = rect.top - MARGIN; const useAbove = spaceBelow < 160 && spaceAbove > spaceBelow; const left = Math.min( Math.max(rect.left, 8), window.innerWidth - WIDTH - 8, ); setPopStyle({ position: 'fixed', left, width: WIDTH, ...(useAbove ? { bottom: window.innerHeight - rect.top + MARGIN } : { top: rect.bottom + MARGIN }), maxHeight: Math.min(MAX_H, useAbove ? spaceAbove : spaceBelow), zIndex: 99998, }); }; useLayoutEffect(() => { if (!open) return; updatePopStyle(); }, [open]); useEffect(() => { if (!open) return; const onResize = () => updatePopStyle(); window.addEventListener('resize', onResize); window.addEventListener('scroll', onResize, true); return () => { window.removeEventListener('resize', onResize); window.removeEventListener('scroll', onResize, true); }; }, [open]); useEffect(() => { if (!open) return; const onDown = (e: MouseEvent) => { if ( !triggerRef.current?.contains(e.target as Node) && !popRef.current?.contains(e.target as Node) ) setOpen(false); }; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); }; document.addEventListener('mousedown', onDown); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); }; }, [open]); return ( <> {open && createPortal(
{options.map(opt => { const isSel = opt.value === value; return (
{ onChange(opt.value); setOpen(false); }} role="option" aria-selected={isSel} > {isSel && } {opt.label}
); })}
, document.body, )} ); }