diff --git a/components/ContextMenu.tsx b/components/ContextMenu.tsx index 7315f00..76825f7 100644 --- a/components/ContextMenu.tsx +++ b/components/ContextMenu.tsx @@ -1,13 +1,16 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; -export type MenuItem = { - label: string; - action: () => void; - icon?: React.FC<{ className?: string }>; - disabled?: boolean; - shortcut?: string; -} | { type: 'separator' }; +export type MenuItem = + | { + label: string; + action?: () => void; + icon?: React.FC<{ className?: string }>; + disabled?: boolean; + shortcut?: string; + children?: MenuItem[]; + } + | { type: 'separator' }; interface ContextMenuProps { isOpen: boolean; @@ -20,6 +23,7 @@ const EDGE_MARGIN = 8; const ContextMenu: React.FC = ({ isOpen, position, items, onClose }) => { const menuRef = useRef(null); + const [activeSubmenu, setActiveSubmenu] = useState<{ depth: number; index: number } | null>(null); const [menuStyle, setMenuStyle] = useState<{ top: number; left: number; maxHeight: number; overflowY: React.CSSProperties['overflowY'] }>({ top: position.y, left: position.x, @@ -95,6 +99,12 @@ const ContextMenu: React.FC = ({ isOpen, position, items, onCl }; }, [isOpen, onClose]); + useEffect(() => { + if (!isOpen) { + setActiveSubmenu(null); + } + }, [isOpen]); + useLayoutEffect(() => { if (!isOpen) return; @@ -146,6 +156,56 @@ const ContextMenu: React.FC = ({ isOpen, position, items, onCl const overlayRoot = document.getElementById('overlay-root'); if (!overlayRoot) return null; + const renderMenuItems = (menuItems: MenuItem[], depth = 0) => ( +
    + {menuItems.map((item, index) => { + if ('type' in item) { + return
  • ; + } + + const { label, action, icon: Icon, disabled, shortcut, children } = item; + const hasChildren = Array.isArray(children) && children.length > 0; + const isSubmenuActive = activeSubmenu?.depth === depth && activeSubmenu.index === index; + + return ( +
  • setActiveSubmenu({ depth, index })} + onMouseLeave={() => setActiveSubmenu(prev => (prev?.depth === depth && prev.index === index ? null : prev))} + > + + {hasChildren && isSubmenuActive && ( +
    +
    + {renderMenuItems(children, depth + 1)} +
    +
    + )} +
  • + ); + })} +
+ ); + return ReactDOM.createPortal(
= ({ isOpen, position, items, onCl }} className="fixed z-50 w-[16.8rem] rounded-md bg-secondary p-1.5 shadow-2xl border border-border-color animate-fade-in-fast" > -
    - {/* Fix: Restructured the type guard to check for a property on the desired object type directly, which ensures proper type narrowing for the MenuItem union. */} - {items.map((item, index) => { - if ('label' in item) { - const { label, action, icon: Icon, disabled, shortcut } = item; - - return ( -
  • - -
  • - ); - } else { - return
  • ; - } - })} -
+ {renderMenuItems(items)}