import React, { useState, useRef, useContext } from "react"; import { usePopper } from "react-popper"; import { ChevronRightIcon } from "@plane/propel/icons"; // helpers import { cn } from "../../utils"; // types import type { TContextMenuItem } from "./root"; import { ContextMenuContext, Portal } from "./root"; type ContextMenuItemProps = { handleActiveItem: () => void; handleClose: () => void; isActive: boolean; item: TContextMenuItem; }; export function ContextMenuItem(props: ContextMenuItemProps) { const { handleActiveItem, handleClose, isActive, item } = props; // Nested menu state const [isNestedOpen, setIsNestedOpen] = useState(false); const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); const [activeNestedIndex, setActiveNestedIndex] = useState(0); const nestedMenuRef = useRef(null); const contextMenuContext = useContext(ContextMenuContext); const hasNestedItems = item.nestedMenuItems && item.nestedMenuItems.length > 0; const renderedNestedItems = item.nestedMenuItems?.filter((nestedItem) => nestedItem.shouldRender !== false) || []; const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: "right-start", strategy: "fixed", modifiers: [ { name: "offset", options: { offset: [0, 4], }, }, { name: "flip", options: { fallbackPlacements: ["left-start", "right-end", "left-end", "top-start", "bottom-start"], }, }, { name: "preventOverflow", options: { padding: 8, }, }, ], }); const closeNestedMenu = React.useCallback(() => { setIsNestedOpen(false); setActiveNestedIndex(0); }, []); // Register this nested menu with the main context React.useEffect(() => { if (contextMenuContext && hasNestedItems) { return contextMenuContext.registerSubmenu(closeNestedMenu); } }, [contextMenuContext, hasNestedItems, closeNestedMenu]); const handleItemClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (hasNestedItems) { // Toggle nested menu if (!isNestedOpen && contextMenuContext) { contextMenuContext.closeAllSubmenus(); } setIsNestedOpen(!isNestedOpen); } else { // Execute action for regular items item.action(); if (item.closeOnClick !== false) handleClose(); } }; const handleMouseEnter = () => { handleActiveItem(); if (hasNestedItems) { // Close other submenus and open this one if (contextMenuContext) { contextMenuContext.closeAllSubmenus(); } setIsNestedOpen(true); } }; const handleNestedItemClick = (nestedItem: TContextMenuItem, e?: React.MouseEvent) => { if (e) { e.preventDefault(); e.stopPropagation(); } nestedItem.action(); if (nestedItem.closeOnClick !== false) { handleClose(); // Close the entire context menu } }; // Handle keyboard navigation for nested items React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (!isNestedOpen || !hasNestedItems) return; if (e.key === "ArrowDown") { e.preventDefault(); setActiveNestedIndex((prev) => (prev + 1) % renderedNestedItems.length); } if (e.key === "ArrowUp") { e.preventDefault(); setActiveNestedIndex((prev) => (prev - 1 + renderedNestedItems.length) % renderedNestedItems.length); } if (e.key === "Enter") { e.preventDefault(); const nestedItem = renderedNestedItems[activeNestedIndex]; if (!nestedItem.disabled) { handleNestedItemClick(nestedItem); } } if (e.key === "ArrowLeft") { e.preventDefault(); closeNestedMenu(); } }; if (isNestedOpen && nestedMenuRef.current) { const menuElement = nestedMenuRef.current; menuElement.addEventListener("keydown", handleKeyDown); // Ensure the menu can receive keyboard events menuElement.setAttribute("tabindex", "-1"); menuElement.focus(); return () => { menuElement.removeEventListener("keydown", handleKeyDown); }; } }, [isNestedOpen, activeNestedIndex, renderedNestedItems, hasNestedItems, closeNestedMenu]); if (item.shouldRender === false) return null; return ( <> {/* Nested Menu */} {hasNestedItems && isNestedOpen && (
{renderedNestedItems.map((nestedItem, index) => ( ))}
)} ); }