material-ui-nested-menu-item
material-ui-nested-menu-item copied to clipboard
@material-ui to @mui
I have upgraded my project from @material-ui to @mui and i see below error when i use material-ui-nested-menu-item now.
error: "./node_modules/material-ui-nested-menu-item/dist-web/index.js Module not found: Can't resolve '@material-ui/core/Menu' in '/app/node_modules/material-ui-nested-menu-item/dist-web'"
later i found that the below dependencies incompatible with latest material ui library.
"@material-ui/core": "^4.9.0", "@material-ui/icons": "^4.5.1",
kindly advice...
This library is not compatible with MUI v5, for now.
If you're working with Typescript, I adapted the code for V5 compatibility, while waiting for the maintainer to do the necessary :
In NestedMenuItem.tsx
;
import React, {useImperativeHandle, useRef, useState} from 'react';
import makeStyles from '@mui/styles/makeStyles';
import {Menu, MenuItem, MenuItemProps, MenuProps} from '@mui/material';
import {ArrowRight} from '@mui/icons-material';
import clsx from 'clsx';
export interface NestedMenuItemProps extends Omit<MenuItemProps, 'button'> {
/**
* Open state of parent `<Menu />`, used to close decendent menus when the
* root menu is closed.
*/
parentMenuOpen: boolean;
/**
* Component for the container element.
* @default 'div'
*/
component?: React.ElementType;
/**
* Effectively becomes the `children` prop passed to the `<MenuItem/>`
* element.
*/
label?: React.ReactNode;
/**
* @default <ArrowRight />
*/
rightIcon?: React.ReactNode;
/**
* Props passed to container element.
*/
ContainerProps?: React.HTMLAttributes<HTMLElement> & React.RefAttributes<HTMLElement | null>;
/**
* Props passed to sub `<Menu/>` element
*/
MenuProps?: Omit<MenuProps, 'children'>;
/**
* @see https://material-ui.com/api/list-item/
*/
button?: true | undefined;
}
const TRANSPARENT = 'rgba(0,0,0,0)';
const useMenuItemStyles = makeStyles(theme => ({
root: (props: any) => ({
backgroundColor: props.open ? theme.palette.action.hover : TRANSPARENT
})
}));
/**
* Use as a drop-in replacement for `<MenuItem>` when you need to add cascading
* menu elements as children to this component.
*/
const NestedMenuItem = React.forwardRef<HTMLLIElement | null, NestedMenuItemProps>(function NestedMenuItem(props, ref) {
const {
parentMenuOpen,
label,
rightIcon = <ArrowRight />,
children,
className,
tabIndex: tabIndexProp,
ContainerProps: ContainerPropsProp = {},
...MenuItemProps
} = props;
const {ref: containerRefProp, ...ContainerProps} = ContainerPropsProp;
const menuItemRef = useRef<HTMLLIElement>(null);
useImperativeHandle(ref, () => menuItemRef.current);
const containerRef = useRef<HTMLDivElement>(null);
useImperativeHandle(containerRefProp, () => containerRef.current);
const menuContainerRef = useRef<HTMLDivElement>(null);
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
setIsSubMenuOpen(true);
if (ContainerProps?.onMouseEnter) {
ContainerProps.onMouseEnter(event);
}
};
const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
setIsSubMenuOpen(false);
if (ContainerProps?.onMouseLeave) {
ContainerProps.onMouseLeave(event);
}
};
// Check if any immediate children are active
const isSubmenuFocused = () => {
const active = containerRef.current?.ownerDocument?.activeElement;
// @ts-ignore
for (const child of menuContainerRef.current?.children ?? []) {
if (child === active) {
return true;
}
}
return false;
};
const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
if (event.target === containerRef.current) {
setIsSubMenuOpen(true);
}
if (ContainerProps?.onFocus) {
ContainerProps.onFocus(event);
}
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Escape') {
return;
}
if (isSubmenuFocused()) {
event.stopPropagation();
}
const active = containerRef.current?.ownerDocument?.activeElement;
if (event.key === 'ArrowLeft' && isSubmenuFocused()) {
containerRef.current?.focus();
}
if (event.key === 'ArrowRight' && event.target === containerRef.current && event.target === active) {
const firstChild = menuContainerRef.current?.children[0] as HTMLElement | undefined;
firstChild?.focus();
}
};
const open = isSubMenuOpen && parentMenuOpen;
const menuItemClasses = useMenuItemStyles({open});
// Root element must have a `tabIndex` attribute for keyboard navigation
let tabIndex;
if (!props.disabled) {
tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1;
}
return (
<div
{...ContainerProps}
ref={containerRef}
onFocus={handleFocus}
tabIndex={tabIndex}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onKeyDown={handleKeyDown}
>
<MenuItem {...MenuItemProps} className={clsx(menuItemClasses.root, className)} ref={menuItemRef}>
{label}
{rightIcon}
</MenuItem>
<Menu
// Set pointer events to 'none' to prevent the invisible Popover div
// from capturing events for clicks and hovers
style={{pointerEvents: 'none'}}
anchorEl={menuItemRef.current}
anchorOrigin={{
vertical: 'top',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left'
}}
open={open}
autoFocus={false}
disableAutoFocus
disableEnforceFocus
onClose={() => {
setIsSubMenuOpen(false);
}}
>
<div ref={menuContainerRef} style={{pointerEvents: 'auto'}}>
{children}
</div>
</Menu>
</div>
);
});
export default NestedMenuItem;
You can then import it like so :
import NestedMenuItem from 'src/components/NestedMenuItem';
Thanks @RuellePaul, I edited your file:
- Removed the clsx and makeStyles dependencies
- Corrected some TypeScript checks
- Added prop:
rightAnchored
- set to true if the menu is right anchored so the submenu needs to be opened on the left. - Added flexGrow: 1 between the text and the arrow right (so the arrow will be aligned to the right)
- Removed
React
from the imports from 'react' - Fixed the link in the comment of 'button' (mui.com)
import {
forwardRef,
useImperativeHandle,
useRef,
useState,
FocusEvent,
KeyboardEvent,
MouseEvent,
ElementType,
ReactNode,
HTMLAttributes,
RefAttributes,
} from "react";
import { Menu, MenuItem, MenuItemProps, MenuProps, styled } from "@mui/material";
import { ArrowRight } from "@mui/icons-material";
export interface NestedMenuItemProps extends Omit<MenuItemProps, "button"> {
/**
* Open state of parent `<Menu />`, used to close descendent menus when the
* root menu is closed.
*/
parentMenuOpen: boolean;
/**
* Component for the container element.
* @default 'div'
*/
component?: ElementType;
/**
* Effectively becomes the `children` prop passed to the `<MenuItem/>`
* element.
*/
label?: ReactNode;
/**
* @default <ArrowRight />
*/
rightIcon?: ReactNode;
/**
* Props passed to container element.
*/
ContainerProps?: HTMLAttributes<HTMLElement> & RefAttributes<HTMLElement | null>;
/**
* Props passed to sub `<Menu/>` element
*/
MenuProps?: Omit<MenuProps, "children">;
/**
* @see https://mui.com/api/list-item/
*/
button?: true | undefined;
/**
*
*/
rightAnchored?: boolean;
}
const TRANSPARENT = "rgba(0,0,0,0)";
const StyledMenuItem = styled(MenuItem)(({ theme }) => ({
backgroundColor: TRANSPARENT,
"&[data-open]": {
backgroundColor: theme.palette.action.hover,
},
}));
/**
* Use as a drop-in replacement for `<MenuItem>` when you need to add cascading
* menu elements as children to this component.
*/
const NestedMenuItem = forwardRef<HTMLLIElement | null, NestedMenuItemProps>(function NestedMenuItem(props, ref) {
const {
parentMenuOpen,
label,
rightIcon = <ArrowRight />,
children,
className,
tabIndex: tabIndexProp,
ContainerProps: ContainerPropsProp = {},
rightAnchored,
...MenuItemProps
} = props;
const { ref: containerRefProp, ...ContainerProps } = ContainerPropsProp;
const menuItemRef = useRef<HTMLLIElement>(null as unknown as HTMLLIElement);
useImperativeHandle(ref, () => menuItemRef.current);
const containerRef = useRef<HTMLDivElement>(null);
useImperativeHandle(containerRefProp, () => containerRef.current);
const menuContainerRef = useRef<HTMLDivElement>(null);
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false);
const handleMouseEnter = (event: MouseEvent<HTMLElement>) => {
setIsSubMenuOpen(true);
if (ContainerProps?.onMouseEnter) {
ContainerProps.onMouseEnter(event);
}
};
const handleMouseLeave = (event: MouseEvent<HTMLElement>) => {
setIsSubMenuOpen(false);
if (ContainerProps?.onMouseLeave) {
ContainerProps.onMouseLeave(event);
}
};
// Check if any immediate children are active
const isSubmenuFocused = () => {
const active = containerRef.current?.ownerDocument?.activeElement;
// @ts-ignore
for (const child of menuContainerRef.current?.children ?? []) {
if (child === active) {
return true;
}
}
return false;
};
const handleFocus = (event: FocusEvent<HTMLElement>) => {
if (event.target === containerRef.current) {
setIsSubMenuOpen(true);
}
if (ContainerProps?.onFocus) {
ContainerProps.onFocus(event);
}
};
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "Escape") {
return;
}
if (isSubmenuFocused()) {
event.stopPropagation();
}
const active = containerRef.current?.ownerDocument?.activeElement;
if (event.key === "ArrowLeft" && isSubmenuFocused()) {
containerRef.current?.focus();
}
if (event.key === "ArrowRight" && event.target === containerRef.current && event.target === active) {
const firstChild = menuContainerRef.current?.children[0] as HTMLElement | undefined;
firstChild?.focus();
}
};
const open = isSubMenuOpen && parentMenuOpen;
// Root element must have a `tabIndex` attribute for keyboard navigation
let tabIndex;
if (!props.disabled) {
tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1;
}
return (
<div
{...ContainerProps}
ref={containerRef}
onFocus={handleFocus}
tabIndex={tabIndex}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onKeyDown={handleKeyDown}
>
<StyledMenuItem {...MenuItemProps} data-open={open || undefined} className={className} ref={menuItemRef}>
{label}
<div style={{ flexGrow: 1 }} />
{rightIcon}
</StyledMenuItem>
<Menu
// Set pointer events to 'none' to prevent the invisible Popover div
// from capturing events for clicks and hovers
style={{ pointerEvents: "none" }}
anchorEl={menuItemRef.current}
anchorOrigin={{
vertical: "top",
horizontal: rightAnchored ? "left" : "right",
}}
transformOrigin={{
vertical: "top",
horizontal: rightAnchored ? "right" : "left",
}}
open={open}
autoFocus={false}
disableAutoFocus
disableEnforceFocus
onClose={() => {
setIsSubMenuOpen(false);
}}
>
<div ref={menuContainerRef} style={{ pointerEvents: "auto" }}>
{children}
</div>
</Menu>
</div>
);
});
export default NestedMenuItem;
someone published this code in https://www.npmjs.com/package/material-ui-nested-menu-item-v5 do you know who ? Thanks