Repository: mui/material-ui (TypeScript)
Date: 2019-01-26 00:03:24+00:00
Task type: Bug Fix

Issue:
Modal manager crashes while switching between popovers
Modal manage crashes on this [line](https://github.com/mui-org/material-ui/blob/f7c1a216cf396f0f942e0fe1c2e9a2d6796b716b/packages/material-ui/src/Modal/ModalManager.js#L130) while jumping between popovers.

https://codesandbox.io/s/049kpv0kzv

<!-- Checked checkbox should look like this: [x] -->
- [x] This is not a v0.x issue. <!-- (v0.x is no longer maintained) -->
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Expected Behavior 🤔
User be able to switch between popovers without crashes

## Current Behavior 😯
App crashes

## Steps to Reproduce 🕹


Link:
https://codesandbox.io/s/049kpv0kzv
1. go over popover A 
2. go over popover B


| Tech         | Version |
|--------------|---------|
| Material-UI  | v3.7.0  |
| React        |         |
| Browser      |         |
| TypeScript   |         |
| etc.         |         |



Issue discussion:
I manage to fix the issue by removing the workaround we did in order to trigger modal to rerender
[https://codesandbox.io/s/45v78lx9](https://codesandbox.io/s/45v78lx9)



Solution patch:
diff --git a/docs/scripts/buildIcons.js b/docs/scripts/buildIcons.js
--- a/docs/scripts/buildIcons.js
+++ b/docs/scripts/buildIcons.js
@@ -30,5 +30,5 @@ Promise.all(promises).catch(err => {
   setTimeout(() => {
     console.log(err);
     throw err;
-  }, 0);
+  });
 });
diff --git a/docs/src/pages/demos/progress/DelayingAppearance.hooks.js b/docs/src/pages/demos/progress/DelayingAppearance.hooks.js
--- a/docs/src/pages/demos/progress/DelayingAppearance.hooks.js
+++ b/docs/src/pages/demos/progress/DelayingAppearance.hooks.js
@@ -47,7 +47,7 @@ function DelayingAppearance() {
     setQuery('progress');
     timer = setTimeout(() => {
       setQuery('success');
-    }, 2e3);
+    }, 2000);
   }
 
   return (
diff --git a/docs/src/pages/demos/progress/DelayingAppearance.js b/docs/src/pages/demos/progress/DelayingAppearance.js
--- a/docs/src/pages/demos/progress/DelayingAppearance.js
+++ b/docs/src/pages/demos/progress/DelayingAppearance.js
@@ -53,7 +53,7 @@ class DelayingAppearance extends React.Component {
       this.setState({
         query: 'success',
       });
-    }, 2e3);
+    }, 2000);
   };
 
   render() {
diff --git a/packages/material-ui/src/ButtonBase/TouchRipple.js b/packages/material-ui/src/ButtonBase/TouchRipple.js
--- a/packages/material-ui/src/ButtonBase/TouchRipple.js
+++ b/packages/material-ui/src/ButtonBase/TouchRipple.js
@@ -243,7 +243,7 @@ class TouchRipple extends React.PureComponent {
       this.startTimerCommit = null;
       this.startTimer = setTimeout(() => {
         this.stop(event, cb);
-      }, 0);
+      });
       return;
     }
 
diff --git a/packages/material-ui/src/Portal/Portal.js b/packages/material-ui/src/Portal/Portal.js
--- a/packages/material-ui/src/Portal/Portal.js
+++ b/packages/material-ui/src/Portal/Portal.js
@@ -36,13 +36,21 @@ class Portal extends React.Component {
 
       // Only rerender if needed
       if (!this.props.disablePortal) {
-        this.forceUpdate(this.props.onRendered);
+        this.forceUpdate(() => {
+          if (this.props.onRendered) {
+            // This might be triggered earlier than the componentDidUpdate of a parent element.
+            // We need to account for it.
+            clearTimeout(this.renderedTimer);
+            this.renderedTimer = setTimeout(this.props.onRendered);
+          }
+        });
       }
     }
   }
 
   componentWillUnmount() {
     this.mountNode = null;
+    clearTimeout(this.renderedTimer);
   }
 
   setMountNode(container) {
diff --git a/packages/material-ui/src/Tooltip/Tooltip.js b/packages/material-ui/src/Tooltip/Tooltip.js
--- a/packages/material-ui/src/Tooltip/Tooltip.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.js
@@ -133,7 +133,7 @@ class Tooltip extends React.Component {
       if (this.childrenRef === document.activeElement) {
         this.handleEnter(event);
       }
-    }, 0);
+    });
 
     const childrenProps = this.props.children.props;
     if (childrenProps.onFocus) {


Test patch:
diff --git a/packages/material-ui/src/Modal/Modal.test.js b/packages/material-ui/src/Modal/Modal.test.js
--- a/packages/material-ui/src/Modal/Modal.test.js
+++ b/packages/material-ui/src/Modal/Modal.test.js
@@ -1,3 +1,5 @@
+/* eslint-disable react/no-multi-comp */
+
 import React from 'react';
 import { assert } from 'chai';
 import { spy, stub } from 'sinon';
@@ -693,4 +695,46 @@ describe('<Modal />', () => {
       wrapper.setProps({ open: true });
     });
   });
+
+  describe('prop: container', () => {
+    it('should be able to change the container', () => {
+      class TestCase extends React.Component {
+        state = {
+          anchorEl: null,
+        };
+
+        componentDidMount() {
+          this.setState(
+            () => ({
+              anchorEl: document.body,
+            }),
+            () => {
+              this.setState(
+                {
+                  anchorEl: null,
+                },
+                () => {
+                  this.setState({
+                    anchorEl: document.body,
+                  });
+                },
+              );
+            },
+          );
+        }
+
+        render() {
+          const { anchorEl } = this.state;
+          return (
+            <Modal open={Boolean(anchorEl)} container={anchorEl} {...this.props}>
+              <Fade in={Boolean(anchorEl)}>
+                <div>Hello</div>
+              </Fade>
+            </Modal>
+          );
+        }
+      }
+      mount(<TestCase />);
+    });
+  });
 });
diff --git a/packages/material-ui/src/Portal/Portal.test.js b/packages/material-ui/src/Portal/Portal.test.js
--- a/packages/material-ui/src/Portal/Portal.test.js
+++ b/packages/material-ui/src/Portal/Portal.test.js
@@ -4,6 +4,7 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import { assert } from 'chai';
 import { spy } from 'sinon';
+import PropTypes from 'prop-types';
 import { createMount, createRender } from '@material-ui/core/test-utils';
 import Portal from './Portal';
 import Select from '../Select';
@@ -185,7 +186,7 @@ describe('<Portal />', () => {
       assert.strictEqual(container.querySelectorAll('#test2').length, 1);
     });
 
-    it('should change container on prop change', () => {
+    it('should change container on prop change', done => {
       class ContainerTest extends React.Component {
         state = {
           container: null,
@@ -211,7 +212,10 @@ describe('<Portal />', () => {
 
       assert.strictEqual(document.querySelector('#test3').parentNode.nodeName, 'BODY');
       wrapper.setState({ container: wrapper.instance().containerRef });
-      assert.strictEqual(document.querySelector('#test3').parentNode.nodeName, 'DIV');
+      setTimeout(() => {
+        assert.strictEqual(document.querySelector('#test3').parentNode.nodeName, 'DIV');
+        done();
+      });
     });
 
     it('should call onRendered', () => {
@@ -224,4 +228,51 @@ describe('<Portal />', () => {
       assert.strictEqual(handleRendered.callCount, 1);
     });
   });
+
+  it('should call onRendered after child componentDidUpdate', done => {
+    class Test extends React.Component {
+      componentDidUpdate(prevProps) {
+        if (prevProps.container !== this.props.container) {
+          this.props.updateFunction();
+        }
+      }
+
+      render() {
+        return (
+          <Portal onRendered={this.props.onRendered} container={this.props.container}>
+            <div />
+          </Portal>
+        );
+      }
+    }
+    Test.propTypes = {
+      container: PropTypes.object,
+      onRendered: PropTypes.func,
+      updateFunction: PropTypes.func,
+    };
+
+    const callOrder = [];
+    const onRendered = () => {
+      callOrder.push('onRendered');
+    };
+    const updateFunction = () => {
+      callOrder.push('cDU');
+    };
+
+    const container1 = document.createElement('div');
+    const container2 = document.createElement('div');
+
+    const wrapper = mount(
+      <Test onRendered={onRendered} updateFunction={updateFunction} container={container1} />,
+    );
+
+    wrapper.setProps({ container: null });
+    wrapper.setProps({ container: container2 });
+    wrapper.setProps({ container: null });
+
+    setTimeout(() => {
+      assert.deepEqual(callOrder, ['onRendered', 'cDU', 'cDU', 'cDU', 'onRendered']);
+      done();
+    });
+  });
 });