UNPKG

@syncfusion/react-navigations

Version:

A package of Pure React navigation components such as Toolbar and Context-menu which is used to navigate from one page to another

672 lines (671 loc) 31.2 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useRef, useImperativeHandle, forwardRef, useEffect, useState, useCallback } from 'react'; import { calculatePosition, isCollide, fit } from '@syncfusion/react-popups'; import { Browser, preRender } from '@syncfusion/react-base'; import { Animation, useProviderContext, SvgIcon, useRippleEffect, Touch } from '@syncfusion/react-base'; import * as React from 'react'; import { createPortal } from 'react-dom'; const SUBMENU_ICON = 'M7.58582 18L13.5858 12L7.58582 6L9.00003 4.58578L16.4142 12L9.00003 19.4142L7.58582 18Z'; const PREVIOUS_ICON = 'M12.4142 19L6.41424 13H21V11H6.41424L12.4142 5L11 3.58578L2.58582 12L11 20.4142L12.4142 19Z'; /** * The MenuItem component represents an individual item within a ContextMenu. * It serves as a configuration component and doesn't render anything directly. * * @example * ```jsx * <ContextMenu> * <MenuItem text="File"> * <MenuItem text="New" /> * <MenuItem text="Open" /> * <MenuItem text="Save" /> * </MenuItem> * <MenuItem separator={true} /> * <MenuItem text="Edit" icon={<svg>...</svg>}> * <MenuItem text="Cut" icon={<svg>...</svg>} /> * </MenuItem> * </ContextMenu> * ``` * * @returns {null} This is a wrapper component that doesn't render anything directly. */ export const MenuItem = () => { return null; }; const MenuListItem = (props) => { const { item, itemClasses, isFocused, hasSubmenu, isDisabled, isSelected, isSeparator, onMouseEnter, onClick, getContent, focusedItemRef, attributes } = props; const { ripple } = useProviderContext(); const { rippleMouseDown, Ripple } = useRippleEffect(ripple); const handleMouseDown = (e) => { if (ripple && !isDisabled && !isSeparator) { rippleMouseDown(e); } }; return (_jsxs("li", { ref: isFocused ? focusedItemRef : undefined, className: itemClasses, onMouseEnter: onMouseEnter, onMouseDown: handleMouseDown, onClick: onClick, tabIndex: -1, role: 'menuitem', "aria-disabled": !isSeparator ? isDisabled : undefined, "aria-haspopup": !isSeparator ? hasSubmenu : undefined, "aria-expanded": !isSeparator ? (hasSubmenu && isSelected ? true : false) : undefined, "aria-label": isSeparator ? 'separator' : (item.text || undefined), ...attributes, children: [!isSeparator && (item.url ? (_jsx("a", { className: 'sf-menu-url', href: item.url, onClick: (e) => e.stopPropagation(), children: _jsx("div", { className: 'sf-anchor-wrap', children: getContent(item) }) })) : (getContent(item))), hasSubmenu && _jsx("span", { className: 'sf-submenu-icon', children: _jsx(SvgIcon, { d: SUBMENU_ICON, "aria-label": 'submenu-icon' }) }), ripple && !isDisabled && !isSeparator && _jsx(Ripple, {})] })); }; /** * The ContextMenu component displays a menu with a list of options when triggered by a right-click. * It supports nested submenus, keyboard navigation, icons, and various animation effects. * * ```typescript * const targetRef = useRef<HTMLButtonElement>(null); * return ( * <div > * <button ref={targetRef}> Right Click Me </button> * <ContextMenu targetRef={targetRef as React.RefObject<HTMLElement>}> * <MenuItem text="Cut" /> * <MenuItem text="Copy" /> * <MenuItem text="Rename" /> * </ContextMenu> * </div> * ); * ``` */ export const ContextMenu = forwardRef((props, ref) => { const { items = [], hoverDelay = 0, onOpen, onClose, onSelect, open, offset, animation = { duration: 400, easing: 'ease', effect: 'FadeIn' }, itemOnClick, closeOnScroll = true, targetRef, className, children, itemTemplate, ...restProps } = props; const elementRef = useRef(null); const parentRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); const [openSubmenus, setOpenSubmenus] = useState([]); const hoverTimeoutRef = useRef(null); const submenuRefs = React.useRef(new Map()); const [focusedItem, setFocusedItem] = useState({ focusedItems: null, hoveredItems: null }); const focusedItemRef = useRef(null); const initialShowState = useRef(open); const { dir } = useProviderContext(); const menuItemsRef = useRef([]); const handleTargetContextMenu = useCallback((event) => { if (Browser.isIos && touchModule.current && event.originalEvent) { event.originalEvent?.preventDefault(); const touch = event.originalEvent.changedTouches[0]; setMenuPosition({ x: touch.clientX, y: touch.clientY }); } else { event.preventDefault(); setMenuPosition({ x: event.pageX, y: event.pageY }); } onOpen?.((event.originalEvent ? event.originalEvent : event)); if (onOpen && open === false) { return; } setIsOpen(true); }, []); const touchModule = useRef(Touch(Browser.isIos && targetRef ? targetRef : { current: null }, { tapHold: handleTargetContextMenu })); const refInstance = { items: menuItemsRef.current, hoverDelay, animation, open, offset, itemOnClick, targetRef, closeOnScroll, itemTemplate }; useEffect(() => { preRender('contextmenu'); return () => { submenuRefs.current?.clear(); if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); hoverTimeoutRef.current = null; } touchModule.current?.destroy?.(); }; }, []); const handleScroll = (args) => { if (isOpen && closeOnScroll && !elementRef?.current?.contains(args.target)) { onClose?.(args); if (onClose && open === true) { return; } closeMenu(); } }; useEffect(() => { if (closeOnScroll) { document.addEventListener('scroll', handleScroll, true); } return () => { document.removeEventListener('scroll', handleScroll, true); }; }, [isOpen, closeOnScroll, onClose, open]); useEffect(() => { const targetElement = targetRef?.current; if (targetElement) { targetElement.addEventListener('contextmenu', handleTargetContextMenu); } return () => { if (targetElement) { targetElement.removeEventListener('contextmenu', handleTargetContextMenu); } }; }, [targetRef]); useEffect(() => { if (!open && initialShowState.current === open) { return; } initialShowState.current = open; if (open) { if (offset && offset.left !== undefined && offset.top !== undefined) { setMenuPosition({ x: offset.left, y: offset.top }); } setIsOpen(true); } else { closeMenu(); } }, [open, offset]); useEffect(() => { if (isOpen) { let left = menuPosition.x; let top = menuPosition.y; const collide = isCollide(parentRef.current, document.documentElement, left, top); if (collide.includes('left') || collide.includes('right')) { left = left - (parentRef?.current?.offsetWidth || 0); } if (collide.includes('bottom')) { const position = fit(parentRef.current, null, { X: false, Y: true }, { top: top, left: left }); top = position.top; } if (left !== menuPosition.x || top !== menuPosition.y) { setMenuPosition({ x: left, y: top }); } applyAnimation(parentRef.current); document.addEventListener('mousedown', handleClickOutside); } else { document.removeEventListener('mousedown', handleClickOutside); } return () => document.removeEventListener('mousedown', handleClickOutside); }, [isOpen, menuPosition]); useEffect(() => { if (focusedItemRef.current) { focusedItemRef.current.focus(); } }, [focusedItem]); useEffect(() => { const filteredChildren = (children ? React.Children.toArray(children).filter((child) => React.isValidElement(child) && child.type === MenuItem) : null); const rawMenuItems = filteredChildren?.length ? parseMenuItemChildren(filteredChildren) : items; menuItemsRef.current = addMobileHeaderToNestedItems(rawMenuItems); }, [items, children]); useImperativeHandle(ref, () => ({ ...refInstance, element: elementRef.current })); useEffect(() => { if (openSubmenus.length > 0) { const pathKey = openSubmenus[openSubmenus.length - 1].parentIndex.join('-'); const currentUl = submenuRefs.current?.get(pathKey); if (Browser.isDevice) { applyAnimation(currentUl); return; } const lastSubmenu = openSubmenus[openSubmenus.length - 1]; if (lastSubmenu.positionChanged) { applyAnimation(currentUl); return; } let left = lastSubmenu.position.x; let top = lastSubmenu.position.y; const collide = isCollide(currentUl, document.documentElement, dir === 'rtl' ? left - (currentUl?.offsetWidth || 0) : left, top); if (collide.includes('left') || collide.includes('right')) { left = calculatePosition(lastSubmenu.currentTarget, dir === 'rtl' ? 'right' : 'left', 'top').left; left = dir === 'rtl' ? left : left - (currentUl?.offsetWidth || 0); } if ((dir === 'rtl' && !collide.includes('right') && !collide.includes('left'))) { left = left - (submenuRefs.current?.get(pathKey)?.offsetWidth || 0); } if (collide.includes('bottom')) { const position = fit(currentUl, null, { X: false, Y: true }, { top: top, left: left }); top = position.top; } const previousUlKey = openSubmenus.length > 1 ? openSubmenus[openSubmenus.length - 2].parentIndex.join('-') : ''; const previousUl = submenuRefs.current?.size === 1 ? parentRef.current : submenuRefs.current?.get(previousUlKey); if (previousUl && !collide.includes('right')) { const scrollBarWidth = previousUl.offsetWidth - previousUl.clientWidth; if (scrollBarWidth > 5) { left += dir === 'rtl' ? -scrollBarWidth : scrollBarWidth; } } if (lastSubmenu.position.x !== left || lastSubmenu.position.y !== top) { setOpenSubmenus((prev) => prev.map((submenu, index) => { if (index === prev.length - 1) { submenuRefs.current?.clear(); return { ...submenu, position: { x: left, y: top }, positionChanged: true }; } return submenu; })); } else { applyAnimation(currentUl); } } }, [openSubmenus]); const closeMenu = () => { setIsOpen(false); setOpenSubmenus([]); submenuRefs?.current?.clear(); setFocusedItem({ focusedItems: null, hoveredItems: null }); }; const handleClickOutside = (event) => { if (elementRef.current?.contains(event.target)) { return; } onClose?.(event); if (onClose && open === true) { return; } closeMenu(); }; const processChild = (child) => { if (!React.isValidElement(child) || child.type !== MenuItem) { return null; } const { children: subChildren, text, id, icon, url, separator, disabled, ...restProps } = child.props; const menuItem = { text, id, icon, url, separator, disabled }; if (subChildren) { const validTemplateNodes = typeof subChildren === 'function' ? subChildren : React.Children.toArray(subChildren).filter((subChild) => React.isValidElement(subChild) && subChild.type !== MenuItem); if (validTemplateNodes.length > 0) { menuItem.children = typeof validTemplateNodes !== 'function' ? (validTemplateNodes.length === 1 ? validTemplateNodes[0] : validTemplateNodes) : validTemplateNodes; } const subItems = React.Children.toArray(subChildren).map(processChild) .filter(Boolean); if (subItems.length > 0) { menuItem.items = subItems; } } if (Object.keys(restProps).length > 0) { menuItem.htmlAttributes = restProps; } return menuItem; }; const parseMenuItemChildren = (childrenNodes) => { if (!childrenNodes) { return items; } const menuItems = React.Children.toArray(childrenNodes).map(processChild).filter(Boolean); return menuItems.length > 0 ? menuItems : items; }; const addMobileHeaderToNestedItems = (menuItems) => { if (!Browser.isDevice) { return menuItems; } const processItems = (items) => { return items.map((item) => { if (item.items && item.items.length > 0) { const hasHeader = item.items.length > 0 && item.items[0]?.icon?.key === 'previous'; let processedSubItems = item.items; if (!hasHeader) { const headerItem = { text: item.text, children: item.children, icon: previousIcon, separator: false, items: [] }; processedSubItems = [headerItem, ...item.items]; } processedSubItems = processItems(processedSubItems); return { ...item, items: processedSubItems }; } return item; }); }; return processItems(menuItems); }; const handleSubmenuOpen = (parentIndexPath, target) => { if (!target || !parentRef.current) { return; } let left = menuPosition.x; let top = menuPosition.y; if (!Browser.isDevice) { const offset = calculatePosition(target, dir === 'rtl' ? 'left' : 'right', 'top'); top = offset.top; left = offset.left; } setOpenSubmenus((prev) => [ ...prev.filter((submenu) => submenu.parentIndex.length < parentIndexPath.length) .map((submenu) => ({ ...submenu, isVisible: false })), { parentIndex: parentIndexPath, position: { x: left, y: top }, isVisible: true, currentTarget: target, positionChanged: false } ]); submenuRefs.current?.clear(); }; const handleBackNavigation = () => { if (openSubmenus.length < 1) { return; } setOpenSubmenus((prev) => { const newSubmenus = prev.filter((_, index) => index !== prev.length - 1); return newSubmenus.map((submenu, index) => ({ ...submenu, isVisible: index === newSubmenus.length - 1 })); }); submenuRefs.current?.clear(); }; const applyAnimation = (targetElement) => { if (!targetElement) { return; } if (animation == null || (animation.duration && animation.duration <= 0) || animation?.effect === 'None' || targetElement.style.visibility === 'visible') { targetElement.style.visibility = 'visible'; parentRef.current?.focus(); return; } const animationRef = Animation({ duration: animation.duration, timingFunction: animation.easing, name: animation.effect, begin: (args) => { if (args?.element) { args.element.style.visibility = 'visible'; if (animation.effect === 'SlideDown') { args.element.style.maxHeight = args.element.offsetHeight + 'px'; args.element.style.overflow = 'hidden'; } } }, end: (args) => { if (args?.element) { if (animation.effect === 'SlideDown') { args.element.style.maxHeight = ''; } parentRef.current?.focus(); } } }); if (targetElement) { animationRef.animate(targetElement); } }; const navigateToNextLevel = () => { const currentFocusedItem = focusedItem?.focusedItems; const itemsToOpen = currentFocusedItem ? getItemsByPath(currentFocusedItem) : []; if (itemsToOpen.length === 0) { return; } let nextIndex = 0; while (nextIndex < itemsToOpen.length && (itemsToOpen[nextIndex].separator || itemsToOpen[nextIndex].disabled)) { nextIndex++; } if (nextIndex >= itemsToOpen.length) { return; } setFocusedItem((prev) => ({ focusedItems: [...currentFocusedItem, nextIndex], hoveredItems: prev?.hoveredItems })); let targetElement; if (openSubmenus.length > 0) { const parentPath = currentFocusedItem?.slice(0, -1); targetElement = submenuRefs.current.get(parentPath.join('-'))?.children[currentFocusedItem?.[currentFocusedItem.length - 1]]; } else { targetElement = parentRef.current?.children[currentFocusedItem?.[0]]; } openSubmenu(currentFocusedItem, targetElement); }; const openSubmenu = (parentIndexPath, target) => { if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); } hoverTimeoutRef.current = window.setTimeout(() => { handleSubmenuOpen(parentIndexPath, target); }, hoverDelay); }; const handleKeyDown = (e) => { const key = e.key; switch (key) { case 'Escape': if (openSubmenus.length > 0) { handleBackNavigation(); if (focusedItem.focusedItems && focusedItem.focusedItems.length > 1) { setFocusedItem((prev) => ({ focusedItems: prev?.focusedItems?.slice(0, -1), hoveredItems: prev?.hoveredItems })); } } else { closeMenu(); } e.preventDefault(); break; case 'Enter': case ' ': { const activeItems = openSubmenus.length > 0 ? getItemsByPath(openSubmenus[openSubmenus.length - 1].parentIndex) : menuItemsRef.current; const currentItem = focusedItem.focusedItems && focusedItem.focusedItems.length > 0 ? activeItems[focusedItem.focusedItems[focusedItem.focusedItems.length - 1]] : undefined; if (!currentItem?.items || currentItem.items.length === 0) { onSelect?.({ item: currentItem, event: e }); closeMenu(); return; } navigateToNextLevel(); e.preventDefault(); break; } case 'ArrowUp': e.preventDefault(); navigateVertical(-1); break; case 'ArrowDown': e.preventDefault(); navigateVertical(1); break; case 'ArrowLeft': e.preventDefault(); if (focusedItem.focusedItems && focusedItem.focusedItems.length > 1) { setFocusedItem((prev) => ({ focusedItems: prev?.focusedItems?.slice(0, -1), hoveredItems: prev?.hoveredItems })); } if (openSubmenus.length > 0) { handleBackNavigation(); } break; case 'ArrowRight': e.preventDefault(); navigateToNextLevel(); break; case 'Home': e.preventDefault(); navigateToPosition('first'); break; case 'End': e.preventDefault(); navigateToPosition('last'); break; default: if (key.length === 1 && /[a-zA-Z0-9]/.test(key)) { e.preventDefault(); navigateToPosition('character', key.toLowerCase()); } break; } }; const navigateToPosition = (type, char) => { const activeItems = openSubmenus.length > 0 ? getItemsByPath(openSubmenus[openSubmenus.length - 1].parentIndex) : menuItemsRef.current; if (!activeItems?.length) { return; } const currentPath = openSubmenus.length > 0 ? [...(openSubmenus[openSubmenus.length - 1]?.parentIndex || [])] : []; const currentIndex = focusedItem?.focusedItems?.length === currentPath.length + 1 ? focusedItem.focusedItems[focusedItem.focusedItems.length - 1] : -1; const isValidItem = (item) => item && !item.separator && !item.disabled; const matchesChar = (item, searchChar) => (item.text && typeof item.text === 'string' && item.text.toLowerCase().startsWith(searchChar)); let targetIndex = -1; switch (type) { case 'first': targetIndex = activeItems.findIndex(isValidItem); break; case 'last': targetIndex = activeItems.map((item, idx) => ({ item, idx })) .reverse().find(({ item }) => isValidItem(item))?.idx ?? -1; break; case 'character': if (!char || typeof char !== 'string' || char.length !== 1) { return; } { const startIndex = Math.max(0, currentIndex + 1); const searchOrder = [ ...activeItems.slice(startIndex), ...activeItems.slice(0, startIndex) ]; const foundItem = searchOrder.find((item) => isValidItem(item) && matchesChar(item, char)); if (foundItem) { targetIndex = activeItems.indexOf(foundItem); } } break; } if (targetIndex >= 0) { setFocusedItem?.((prev) => ({ focusedItems: [...currentPath, targetIndex], hoveredItems: prev?.hoveredItems || null })); } }; const navigateVertical = (direction) => { const activeItems = openSubmenus.length > 0 ? getItemsByPath(openSubmenus[openSubmenus.length - 1].parentIndex) : menuItemsRef.current; if (activeItems.length === 0) { return; } const currentPath = openSubmenus.length > 0 ? [...openSubmenus[openSubmenus.length - 1].parentIndex] : []; const currentIndex = focusedItem.focusedItems && (focusedItem.focusedItems.length === currentPath.length + 1) ? focusedItem.focusedItems[focusedItem.focusedItems.length - 1] : null; let nextIndex = currentIndex === null ? (direction > 0 ? 0 : activeItems.length - 1) : (currentIndex + direction + activeItems.length) % activeItems.length; let itemsChecked = 0; while (nextIndex < activeItems.length && (activeItems[nextIndex].separator || activeItems[nextIndex].disabled) && itemsChecked < activeItems.length) { nextIndex = (nextIndex + direction + activeItems.length) % activeItems.length; itemsChecked++; } if (itemsChecked >= activeItems.length) { return; } setFocusedItem((prev) => ({ focusedItems: [...currentPath, nextIndex], hoveredItems: prev?.hoveredItems })); }; const getItemsByPath = useCallback((indexPath) => { return indexPath.reduce((currentItems, subIndex) => currentItems[subIndex]?.items || [], menuItemsRef.current); }, []); const previousIcon = React.useMemo(() => _jsx(SvgIcon, { d: PREVIOUS_ICON, "aria-label": 'Previous' }, 'previous'), []); const getContent = (item) => { if (itemTemplate) { return item.children || itemTemplate(item); } return (_jsxs(_Fragment, { children: [item.icon && _jsx("span", { className: ['sf-menu-icon', typeof item.icon === 'string' ? item.icon : ''].filter(Boolean).join(' '), children: typeof item.icon !== 'string' && item.icon }), item.children || item.text] })); }; const renderMenuItems = (menuItems, parentIndexPath) => { return menuItems.map((item, index) => { const currentIndexPath = [...parentIndexPath, index]; const hasSubmenu = (item.items ? item.items.length > 0 : false); const isDisabled = item.disabled === true; const isHeaderItem = Browser.isDevice && item.icon?.key === 'previous'; const { className, ...restAttributes } = item.htmlAttributes || {}; const isFocused = currentIndexPath.join('-') === focusedItem.focusedItems?.join('-'); const isHovered = currentIndexPath.join('-') === focusedItem.hoveredItems?.join('-'); const isBlankIcon = !item.icon && menuItems.find((iconItem, iconIndex) => iconIndex !== index && iconItem.icon) !== undefined; const isSelected = openSubmenus.some((submenu) => { if (parentIndexPath.length === 0) { return submenu.parentIndex[0] === index; } return (parentIndexPath.length === submenu.parentIndex.length - 1 && submenu.parentIndex.slice(0, -1).join('-') === parentIndexPath.join('-') && submenu.parentIndex[submenu.parentIndex.length - 1] === index); }); const itemClasses = [ 'sf-menu-item', item.separator && 'sf-separator', isDisabled && 'sf-disabled', isHeaderItem && 'sf-menu-header', (isFocused || isHovered) && 'sf-focused', isSelected && hasSubmenu && 'sf-selected', isBlankIcon && 'sf-blank-icon', className ].filter(Boolean).join(' '); const handleMouseEnter = (e) => { setFocusedItem((prev) => ({ focusedItems: prev?.focusedItems, hoveredItems: currentIndexPath })); if (!hasSubmenu) { if (openSubmenus.length === currentIndexPath.length) { handleBackNavigation(); } else if (openSubmenus.length > currentIndexPath.length) { setOpenSubmenus(openSubmenus.slice(0, currentIndexPath.length - 1)); submenuRefs?.current?.clear(); } return; } if (!Browser.isDevice && hasSubmenu && !itemOnClick && !isDisabled) { if (openSubmenus && openSubmenus.find((submenu) => submenu.parentIndex.join('-') === currentIndexPath.join('-'))) { return; } submenuRefs?.current?.clear(); openSubmenu(currentIndexPath, e.currentTarget); } }; const handleItemClick = (e) => { e.preventDefault(); if (isDisabled) { return; } if (isHeaderItem) { handleBackNavigation(); } else if (hasSubmenu) { if (Browser.isDevice) { handleSubmenuOpen(currentIndexPath, e.currentTarget); } else if (itemOnClick) { openSubmenu(currentIndexPath, e.currentTarget); } } else { onSelect?.({ item: item, event: e }); onClose?.(e); if (onClose && open === true) { return; } closeMenu(); } }; return (_jsx(MenuListItem, { item: item, itemClasses: itemClasses, isFocused: isFocused, hasSubmenu: hasSubmenu, isDisabled: isDisabled, isSelected: isSelected, isSeparator: !!item.separator, onMouseEnter: handleMouseEnter, onClick: handleItemClick, getContent: getContent, focusedItemRef: focusedItemRef, attributes: restAttributes }, currentIndexPath.join('-'))); }); }; const renderSubmenus = () => { return openSubmenus.map(({ parentIndex, position, isVisible }) => { const submenuItems = getItemsByPath(parentIndex); const pathKey = parentIndex.join('-'); return (_jsx("ul", { ref: (el) => { if (el && submenuRefs.current) { submenuRefs.current.set(pathKey, el); } }, className: 'sf-menu-parent sf-ul', style: { left: position.x, top: position.y, display: Browser.isDevice && !isVisible ? 'none' : 'block', visibility: 'hidden' }, tabIndex: 0, role: "menu", children: renderMenuItems(submenuItems, parentIndex) }, `submenu-${pathKey}`)); }); }; const rootClassName = React.useMemo(() => { return [ 'sf-contextmenu-wrapper', dir === 'rtl' ? 'sf-rtl' : '', className ].filter(Boolean).join(' '); }, [dir]); const portalContainer = typeof document !== 'undefined' ? document.body : null; if (!portalContainer) { return null; } return (_jsx(_Fragment, { children: isOpen && createPortal(_jsxs("div", { ref: elementRef, className: rootClassName, onKeyDown: handleKeyDown, ...restProps, children: [_jsx("ul", { className: "sf-control sf-contextmenu sf-menu-parent", style: { top: menuPosition.y, left: menuPosition.x, display: Browser.isDevice && openSubmenus.length > 0 ? 'none' : 'block', visibility: 'hidden' }, role: "menu", tabIndex: 0, ref: parentRef, children: (menuItemsRef.current && menuItemsRef.current.length > 0) && renderMenuItems(menuItemsRef.current, []) }), renderSubmenus()] }), portalContainer) })); }); export default ContextMenu;