import React, { useLayoutEffect, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; const MENU_GAP = 4; const VIEWPORT_PAD = 8; function useAnchoredMenuPosition( open: boolean, anchorRef: React.RefObject, menuRef: React.RefObject ) { const [coords, setCoords] = useState<{ top: number; left: number } | null>(null); useLayoutEffect(() => { if (!open) { setCoords(null); return; } const update = () => { const anchor = anchorRef.current; const menu = menuRef.current; if (!anchor || !menu) return; const anchorRect = anchor.getBoundingClientRect(); const menuWidth = menu.offsetWidth; const menuHeight = menu.offsetHeight; let top = anchorRect.bottom + MENU_GAP; if (top + menuHeight > window.innerHeight - VIEWPORT_PAD) { top = anchorRect.top - menuHeight - MENU_GAP; } top = Math.max( VIEWPORT_PAD, Math.min(top, window.innerHeight - menuHeight - VIEWPORT_PAD) ); let left = anchorRect.right - menuWidth; left = Math.max( VIEWPORT_PAD, Math.min(left, window.innerWidth - menuWidth - VIEWPORT_PAD) ); setCoords({ top, left }); }; update(); const raf = requestAnimationFrame(update); window.addEventListener('resize', update); window.addEventListener('scroll', update, true); return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', update); window.removeEventListener('scroll', update, true); }; }, [open, anchorRef, menuRef]); return coords; } interface AnchoredMenuProps { open: boolean; onClose: () => void; anchorRef: React.RefObject; children: React.ReactNode; className?: string; } export const AnchoredMenu: React.FC = ({ open, onClose, anchorRef, children, className = '', }) => { const menuRef = useRef(null); const coords = useAnchoredMenuPosition(open, anchorRef, menuRef); useEffect(() => { if (!open) return; const handlePointerDown = (e: PointerEvent) => { const target = e.target as Node; if (menuRef.current?.contains(target) || anchorRef.current?.contains(target)) return; onClose(); }; document.addEventListener('pointerdown', handlePointerDown); return () => document.removeEventListener('pointerdown', handlePointerDown); }, [open, onClose, anchorRef]); if (!open) return null; return createPortal(
e.stopPropagation()} > {children}
, document.body ); };