import { SyntheticEvent, useRef, useEffect, CSSProperties } from 'react'; export interface MenuOption { icon?: string; label: string; onClick: (event: SyntheticEvent) => void; variant?: 'primary' | 'danger'; iconClassName?: string; iconWidth?: number; iconHeight?: number; } interface ContextMenuProps { isOpen: boolean; setIsOpen: (isOpen: boolean) => void; options: MenuOption[]; anchorRef: React.RefObject; position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'; offset?: { x: number; y: number }; className?: string; } export default function ContextMenu({ isOpen, setIsOpen, options, anchorRef, className = '', position = 'bottom-right', offset = { x: 0, y: 8 }, }: ContextMenuProps) { const menuRef = useRef(null); useEffect(() => { if (isOpen && menuRef.current) { const positionStyle = getMenuPosition(); if (menuRef.current) { Object.assign(menuRef.current.style, { top: positionStyle.top, left: positionStyle.left, }); } } }, [isOpen]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( menuRef.current && !menuRef.current.contains(event.target as Node) && !anchorRef.current?.contains(event.target as Node) ) { setIsOpen(false); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [isOpen, setIsOpen]); if (!isOpen) return null; const getMenuPosition = (): CSSProperties => { if (!anchorRef.current) return {}; const rect = anchorRef.current.getBoundingClientRect(); const scrollY = window.scrollY || document.documentElement.scrollTop; const scrollX = window.scrollX || document.documentElement.scrollLeft; let top = rect.bottom + scrollY + offset.y; let left = rect.right + scrollX + offset.x; // Get menu dimensions (need ref to be available) const menuWidth = menuRef.current?.offsetWidth || 144; // Default min-width const menuHeight = menuRef.current?.offsetHeight || 0; // Get viewport dimensions const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Adjust position based on specified position switch (position) { case 'bottom-left': left = rect.right + scrollX - menuWidth + offset.x; break; case 'top-right': top = rect.top + scrollY - offset.y - menuHeight; break; case 'top-left': top = rect.top + scrollY - offset.y - menuHeight; left = rect.right + scrollX - menuWidth + offset.x; break; // bottom-right is default } if (left + menuWidth > viewportWidth) { left = Math.max(5, viewportWidth - menuWidth - 5); } if (left < 5) { left = 5; } if (top + menuHeight > viewportHeight + scrollY) { top = rect.top + scrollY - menuHeight - offset.y; } if (top < scrollY + 5) { top = rect.bottom + scrollY + offset.y; } return { position: 'fixed', top: `${top}px`, left: `${left}px`, }; }; return (
e.stopPropagation()} >
{options.map((option, index) => ( ))}
); }