Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@material-ui to @mui #31

Open
prakash2091 opened this issue Dec 28, 2021 · 3 comments
Open

@material-ui to @mui #31

prakash2091 opened this issue Dec 28, 2021 · 3 comments

Comments

@prakash2091
Copy link

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...

@RuellePaul
Copy link

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';

@elisherer
Copy link

elisherer commented Jan 16, 2022

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;

@jonenst
Copy link

jonenst commented Mar 30, 2022

someone published this code in https://www.npmjs.com/package/material-ui-nested-menu-item-v5 do you know who ? Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants