@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
JavaScript
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;