Repository: mui/material-ui (TypeScript)
Date: 2018-10-25 22:59:39+00:00
Task type: Bug Fix

Issue:
Still having trouble with Modals and removeAttribute in 3.3.1
See issue #13349 and others referenced.

I have a menu with several items. Clicking on a menu item is supposed to open another modal but it crashes the whole React DOM with the error ```Uncaught TypeError: Cannot read property 'removeAttribute' of undefined```. I have already upgraded to 3.3.1 which was supposed to fix the issue, but it is still happening. Downgrading to 3.2.2 solves the problem and everything works as expected.




Issue discussion:
still +1, in version @3.1.1 When right/top menu with items trigger OnClick, page crashed along with error message. `Uncaught TypeError: Cannot read property 'removeAttribute' of undefined.` 😢 

Thx for the timely hotfix @3.3.1.

How can we reproduce it? 
it happen on mine too
confirmed 3.1.1 still have the same behavior. the trace is same as #13349
I have the same issue with 3.3.1. Unfortunately, I cannot downgrade to 3.2.2 as suggested above
For me it's happening when I launch a `Dialog` from a `Menu > MenuItem` component's onClick function. Haven't been able to narrow it down further yet.

Here's the complete stack trace:
```
Uncaught TypeError: Cannot read property 'removeAttribute' of undefined
    at ariaHidden (manageAriaHidden.js:28)
    at ModalManager.remove (ModalManager.js:188)
    at Portal.Modal._this.handleRendered (Modal.js:133)
    at callCallback (react-dom.development.js:11582)
    at commitUpdateEffects (react-dom.development.js:11622)
    at commitUpdateQueue (react-dom.development.js:11610)
    at commitLifeCycles (react-dom.development.js:15374)
    at commitAllLifeCycles (react-dom.development.js:16705)
    at HTMLUnknownElement.callCallback (react-dom.development.js:146)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:195)
    at invokeGuardedCallback (react-dom.development.js:245)
    at commitRoot (react-dom.development.js:16881)
    at completeRoot (react-dom.development.js:18337)
    at performWorkOnRoot (react-dom.development.js:18259)
    at performWork (react-dom.development.js:18161)
    at performSyncWork (react-dom.development.js:18134)
    at interactiveUpdates$1 (react-dom.development.js:18444)
    at interactiveUpdates (react-dom.development.js:2322)
    at dispatchInteractiveEvent (react-dom.development.js:5132)
The above error occurred in the <Portal> component:
    in Portal (created by Modal)
    in Modal (created by WithStyles(Modal))
    in WithStyles(Modal) (created by Popover)
    in Popover (created by WithStyles(Popover))
    in WithStyles(Popover) (created by Menu)
    in Menu (created by WithStyles(Menu))
...
```
I had a similar problem with dialog, using with a fragment children, fixed after replacing for a div
Here's a reproducible demo: https://codesandbox.io/s/o730293xp9
From my somewhat cursory examination of the code it seems MUI tries to access a React ref that may not be ready. It's this piece of code in ModalManager.js:

 `ariaHidden(data.modals[data.modals.length - 1].modalRef, false);`

It happens when one modal goes away and a new one appears on the same cycle.

Here's a new repro that uses two `<Menu />` elements, with the second appearing after an item in the first menu is clicked (the and first menu disappearing at the same time): https://codesandbox.io/s/j3ppz0lwq5

I think a check before the above line of code in ModalManager.js would be required. E.g. 

```
if (data.modals[data.modals.length - 1].modalRef)
   ariaHidden(...);
```
@idrm Thank you for the reproduction example! I think that we can solve the issue with a simple change. What do you think of:
```diff
--- a/packages/material-ui/src/Modal/Modal.js
+++ b/packages/material-ui/src/Modal/Modal.js
@@ -10,6 +10,7 @@ import Portal from '../Portal';
 import { createChainedFunction } from '../utils/helpers';
 import withStyles from '../styles/withStyles';
 import { createChainedFunction } from '../utils/helpers';
 import withStyles from '../styles/withStyles';
 import ModalManager from './ModalManager';
+import { ariaHidden } from './manageAriaHidden';
 import Backdrop from '../Backdrop';

 function getContainer(container, defaultContainer) {
@@ -118,10 +119,7 @@ class Modal extends React.Component {
     if (this.props.open) {
       this.handleOpened();
     } else {
-      const doc = ownerDocument(this.mountNode);
-      const container = getContainer(this.props.container, doc.body);
-      this.props.manager.add(this, container);
-      this.props.manager.remove(this);
+      ariaHidden(this.modalRef, true);
     }
   };
```
Adding & removing the modal wasn't a good call. We can directly execute the expected outcome.
Do you want to submit a pull request?
Updated to Node 10 recently and getting the same bug. 

Solution patch:
diff --git a/.size-limit.js b/.size-limit.js
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -22,7 +22,7 @@ module.exports = [
     name: 'The size of all the material-ui modules.',
     webpack: true,
     path: 'packages/material-ui/build/index.js',
-    limit: '93.0 KB',
+    limit: '93.1 KB',
   },
   {
     name: 'The main docs bundle',
diff --git a/packages/material-ui/src/Menu/Menu.js b/packages/material-ui/src/Menu/Menu.js
--- a/packages/material-ui/src/Menu/Menu.js
+++ b/packages/material-ui/src/Menu/Menu.js
@@ -57,7 +57,7 @@ class Menu extends React.Component {
     }
   };
 
-  handleEnter = element => {
+  handleEntering = element => {
     const { disableAutoFocusItem, theme } = this.props;
     const menuList = ReactDOM.findDOMNode(this.menuListRef);
 
@@ -74,8 +74,8 @@ class Menu extends React.Component {
       menuList.style.width = `calc(100% + ${size})`;
     }
 
-    if (this.props.onEnter) {
-      this.props.onEnter(element);
+    if (this.props.onEntering) {
+      this.props.onEntering(element);
     }
   };
 
@@ -95,7 +95,7 @@ class Menu extends React.Component {
       classes,
       disableAutoFocusItem,
       MenuListProps,
-      onEnter,
+      onEntering,
       PaperProps = {},
       PopoverClasses,
       theme,
@@ -106,7 +106,7 @@ class Menu extends React.Component {
       <Popover
         getContentAnchorEl={this.getContentAnchorEl}
         classes={PopoverClasses}
-        onEnter={this.handleEnter}
+        onEntering={this.handleEntering}
         anchorOrigin={theme.direction === 'rtl' ? RTL_ORIGIN : LTR_ORIGIN}
         transformOrigin={theme.direction === 'rtl' ? RTL_ORIGIN : LTR_ORIGIN}
         PaperProps={{
diff --git a/packages/material-ui/src/Modal/Modal.js b/packages/material-ui/src/Modal/Modal.js
--- a/packages/material-ui/src/Modal/Modal.js
+++ b/packages/material-ui/src/Modal/Modal.js
@@ -11,6 +11,7 @@ import { createChainedFunction } from '../utils/helpers';
 import withStyles from '../styles/withStyles';
 import ModalManager from './ModalManager';
 import Backdrop from '../Backdrop';
+import { ariaHidden } from './manageAriaHidden';
 
 function getContainer(container, defaultContainer) {
   container = typeof container === 'function' ? container() : container;
@@ -66,7 +67,6 @@ class Modal extends React.Component {
     if (prevProps.open && !this.props.open) {
       this.handleClose();
     } else if (!prevProps.open && this.props.open) {
-      // check for focus
       this.lastFocus = ownerDocument(this.mountNode).activeElement;
       this.handleOpen();
     }
@@ -118,10 +118,7 @@ class Modal extends React.Component {
     if (this.props.open) {
       this.handleOpened();
     } else {
-      const doc = ownerDocument(this.mountNode);
-      const container = getContainer(this.props.container, doc.body);
-      this.props.manager.add(this, container);
-      this.props.manager.remove(this);
+      ariaHidden(this.modalRef, true);
     }
   };
 
diff --git a/packages/material-ui/src/Modal/ModalManager.js b/packages/material-ui/src/Modal/ModalManager.js
--- a/packages/material-ui/src/Modal/ModalManager.js
+++ b/packages/material-ui/src/Modal/ModalManager.js
@@ -153,7 +153,13 @@ class ModalManager {
       this.data.splice(containerIdx, 1);
     } else if (this.hideSiblingNodes) {
       // Otherwise make sure the next top modal is visible to a screan reader.
-      ariaHidden(data.modals[data.modals.length - 1].modalRef, false);
+      const nextTop = data.modals[data.modals.length - 1];
+      // as soon as a modal is adding its modalRef is undefined. it can't set
+      // aria-hidden because the dom element doesn't exist either
+      // when modal was unmounted before modalRef gets null
+      if (nextTop.modalRef) {
+        ariaHidden(nextTop.modalRef, false);
+      }
     }
 
     return modalIdx;
diff --git a/packages/material-ui/src/Modal/manageAriaHidden.d.ts b/packages/material-ui/src/Modal/manageAriaHidden.d.ts
--- a/packages/material-ui/src/Modal/manageAriaHidden.d.ts
+++ b/packages/material-ui/src/Modal/manageAriaHidden.d.ts
@@ -1,3 +1,7 @@
-export function ariaHidden(show: boolean, node: Node): never;
-export function hideSiblings(container: Element, mountNode: Node): never;
-export function showSiblings(container: Element, mountNode: Node): never;
+export function ariaHidden(node: Node, show: boolean): never;
+export function ariaHiddenSiblings(
+  container: Element,
+  mountNode: Node,
+  currentNode: Node,
+  show: boolean,
+): never;


Test patch:
diff --git a/packages/material-ui/src/Menu/Menu.test.js b/packages/material-ui/src/Menu/Menu.test.js
--- a/packages/material-ui/src/Menu/Menu.test.js
+++ b/packages/material-ui/src/Menu/Menu.test.js
@@ -163,16 +163,16 @@ describe('<Menu />', () => {
     });
 
     it('should call props.onEnter with element if exists', () => {
-      const onEnterSpy = spy();
-      wrapper.setProps({ onEnter: onEnterSpy });
-      instance.handleEnter(elementForHandleEnter);
-      assert.strictEqual(onEnterSpy.callCount, 1);
-      assert.strictEqual(onEnterSpy.calledWith(elementForHandleEnter), true);
+      const onEnteringSpy = spy();
+      wrapper.setProps({ onEntering: onEnteringSpy });
+      instance.handleEntering(elementForHandleEnter);
+      assert.strictEqual(onEnteringSpy.callCount, 1);
+      assert.strictEqual(onEnteringSpy.calledWith(elementForHandleEnter), true);
     });
 
     it('should call menuList focus when no menuList', () => {
       delete instance.menuListRef;
-      instance.handleEnter(elementForHandleEnter);
+      instance.handleEntering(elementForHandleEnter);
       assert.strictEqual(selectedItemFocusSpy.callCount, 0);
       assert.strictEqual(menuListFocusSpy.callCount, 1);
     });
@@ -180,7 +180,7 @@ describe('<Menu />', () => {
     it('should call menuList focus when menuList but no menuList.selectedItemRef ', () => {
       instance.menuListRef = {};
       delete instance.menuListRef.selectedItemRef;
-      instance.handleEnter(elementForHandleEnter);
+      instance.handleEntering(elementForHandleEnter);
       assert.strictEqual(selectedItemFocusSpy.callCount, 0);
       assert.strictEqual(menuListFocusSpy.callCount, 1);
     });
@@ -192,21 +192,21 @@ describe('<Menu />', () => {
       });
 
       it('should call selectedItem focus when there is a menuList.selectedItemRef', () => {
-        instance.handleEnter(elementForHandleEnter);
+        instance.handleEntering(elementForHandleEnter);
         assert.strictEqual(selectedItemFocusSpy.callCount, 1);
         assert.strictEqual(menuListFocusSpy.callCount, 0);
       });
 
       it('should not set style on list when element.clientHeight > list.clientHeight', () => {
         elementForHandleEnter.clientHeight = MENU_LIST_HEIGHT + 1;
-        instance.handleEnter(elementForHandleEnter);
+        instance.handleEntering(elementForHandleEnter);
         assert.strictEqual(menuListSpy.style.paddingRight, undefined);
         assert.strictEqual(menuListSpy.style.width, undefined);
       });
 
       it('should not set style on list when element.clientHeight == list.clientHeight', () => {
         elementForHandleEnter.clientHeight = MENU_LIST_HEIGHT;
-        instance.handleEnter(elementForHandleEnter);
+        instance.handleEntering(elementForHandleEnter);
         assert.strictEqual(menuListSpy.style.paddingRight, undefined);
         assert.strictEqual(menuListSpy.style.width, undefined);
       });
@@ -215,7 +215,7 @@ describe('<Menu />', () => {
         assert.strictEqual(menuListSpy.style.paddingRight, undefined);
         assert.strictEqual(menuListSpy.style.width, undefined);
         elementForHandleEnter.clientHeight = MENU_LIST_HEIGHT - 1;
-        instance.handleEnter(elementForHandleEnter);
+        instance.handleEntering(elementForHandleEnter);
         assert.notStrictEqual(menuListSpy.style.paddingRight, undefined);
         assert.notStrictEqual(menuListSpy.style.width, undefined);
       });
diff --git a/packages/material-ui/test/integration/NestedMenu.test.js b/packages/material-ui/test/integration/NestedMenu.test.js
new file mode 100644
--- /dev/null
+++ b/packages/material-ui/test/integration/NestedMenu.test.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import { assert } from 'chai';
+import { createMount } from 'packages/material-ui/src/test-utils';
+import Portal from 'packages/material-ui/src/Portal';
+import NestedMenu from './fixtures/menus/NestedMenu';
+
+describe('<NestedMenu> integration', () => {
+  let mount;
+
+  before(() => {
+    mount = createMount();
+  });
+
+  after(() => {
+    mount.cleanUp();
+  });
+
+  describe('mounted open', () => {
+    let wrapper;
+    let portalLayer;
+
+    before(() => {
+      wrapper = mount(<NestedMenu />);
+    });
+
+    it('should not be open', () => {
+      const firstMenu = document.getElementById('first-menu');
+      const secondMenu = document.getElementById('second-menu');
+      assert.strictEqual(firstMenu, null);
+      assert.strictEqual(secondMenu, null);
+    });
+
+    it('should focus the first item as nothing has been selected', () => {
+      wrapper.setState({ firstMenuOpen: true });
+
+      portalLayer = wrapper
+        .find(Portal)
+        .instance()
+        .getMountNode();
+      assert.strictEqual(document.activeElement, portalLayer.querySelectorAll('li')[0]);
+    });
+
+    it('should focus the first item of second menu', () => {
+      wrapper.setState({ firstMenuOpen: false, secondMenuOpen: true });
+      const secondMenu = document.getElementById('second-menu');
+      assert.strictEqual(document.activeElement, secondMenu.querySelectorAll('li')[0]);
+    });
+
+    it('should open the first menu again', () => {
+      wrapper.setState({ firstMenuOpen: true, secondMenuOpen: false });
+      const firstMenu = document.getElementById('first-menu');
+      assert.strictEqual(document.activeElement, firstMenu.querySelectorAll('li')[0]);
+    });
+
+    it('should be able to open second menu again', () => {
+      wrapper.setState({ firstMenuOpen: false, secondMenuOpen: true });
+      const secondMenu = document.getElementById('second-menu');
+      assert.strictEqual(document.activeElement, secondMenu.querySelectorAll('li')[0]);
+    });
+  });
+});
diff --git a/packages/material-ui/test/integration/fixtures/menus/NestedMenu.js b/packages/material-ui/test/integration/fixtures/menus/NestedMenu.js
new file mode 100644
--- /dev/null
+++ b/packages/material-ui/test/integration/fixtures/menus/NestedMenu.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import Menu from '@material-ui/core/Menu';
+import MenuItem from '@material-ui/core/MenuItem';
+
+class NestedMenu extends React.Component {
+  state = {
+    firstMenuOpen: false,
+    secondMenuOpen: false,
+  };
+
+  handleMenuItemClick = () => {
+    this.setState({ firstMenuOpen: false, secondMenuOpen: true });
+  };
+
+  handleMenuItemClick2 = () => {
+    this.setState({ secondMenuOpen: false });
+  };
+
+  handleClose = () => {
+    this.setState({ firstMenuOpen: false, secondMenuOpen: false });
+  };
+
+  render() {
+    const { firstMenuOpen, secondMenuOpen } = this.state;
+
+    return (
+      <div>
+        <Menu
+          id="second-menu"
+          open={secondMenuOpen}
+          onClose={this.handleClose}
+          transitionDuration={0}
+        >
+          <MenuItem onClick={this.handleMenuItemClick2}>Second Menu</MenuItem>
+        </Menu>
+        <Menu
+          id="first-menu"
+          open={firstMenuOpen}
+          onClose={this.handleClose}
+          transitionDuration={0}
+        >
+          <MenuItem onClick={this.handleMenuItemClick}>Profile 1</MenuItem>
+          <MenuItem onClick={this.handleClose}>My account</MenuItem>
+          <MenuItem onClick={this.handleClose}>Logout</MenuItem>
+        </Menu>
+      </div>
+    );
+  }
+}
+
+export default NestedMenu;