Repository: mui/material-ui (TypeScript)
Date: 2018-07-16 21:57:35+00:00
Task type: Bug Fix

Issue:
[Tooltip] Bugs in version 1.4.0
- [x] This is a v1.x issue.
- [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.

## Bugs
- Double tooltip: when enterDelay is higher this bug is most visible.
- Cut tooltip: when the browser scroll is visible.

## Expected Behavior v1.3.1
- Double tooltip
![image](https://user-images.githubusercontent.com/12452003/42729945-df3b724a-87b6-11e8-9e2e-440f9548d6b6.png)
- Cut tooltip
![image](https://user-images.githubusercontent.com/12452003/42729948-00ad3b52-87b7-11e8-91d1-fd87e9ee095c.png)

## Current Behavior v1.4.0
- Double tooltip
![image](https://user-images.githubusercontent.com/12452003/42729914-9e1f8a72-87b5-11e8-81a1-482eaba3194c.png)
- Cut tooltip
![image](https://user-images.githubusercontent.com/12452003/42729928-17cc2f1a-87b6-11e8-8d5f-0025f940f643.png)


## Environment

| Tech         | Version |
|--------------|---------|
| Material-UI  | v1.4.1 |
| React        | v16.4.1 |
| browser      | chrome 67 |


Issue discussion:
> when enterDelay is higher this bug is most visible.

Can it be noticed with `enterDelay={0}`?
@oliviertassinari Yes, with enterDelay={0} is noticed, but it is more difficult to reproduce.
With this example, FormControlLabel map enterDelay={0}, sometimes the other tooltip is visible.
![image](https://user-images.githubusercontent.com/12452003/42735442-e5dd07ba-8821-11e8-8879-659994b6fd34.png)

@ahce What OS are you using. Could you provide a CodeSandbox?
@oliviertassinari 
Windows 10 1709 build 16299.371
CodeSandbox: https://codesandbox.io/s/n325qlpvrl
Thanks. I might have found a solution to the problem. Let's try it out. We can remove the title synchronously on the event trigger. Right now, we wait for the next rAF. 

Solution patch:
diff --git a/.size-limit.js b/.size-limit.js
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -27,7 +27,7 @@ module.exports = [
     name: 'The size of all the modules of material-ui.',
     webpack: true,
     path: 'packages/material-ui/build/index.js',
-    limit: '94.7 KB',
+    limit: '94.8 KB',
   },
   {
     name: 'The main bundle of the docs',
diff --git a/packages/material-ui/src/Popper/Popper.js b/packages/material-ui/src/Popper/Popper.js
--- a/packages/material-ui/src/Popper/Popper.js
+++ b/packages/material-ui/src/Popper/Popper.js
@@ -111,7 +111,7 @@ class Popper extends React.Component {
           : {
               // It's using scrollParent by default, we can use the viewport when using a portal.
               preventOverflow: {
-                boundariesElement: 'viewport',
+                boundariesElement: 'window',
               },
             }),
         ...modifiers,
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
@@ -75,6 +75,11 @@ class Tooltip extends React.Component {
 
   defaultId = null;
 
+  internalState = {
+    hover: false,
+    focus: false,
+  };
+
   constructor(props) {
     super(props);
 
@@ -91,9 +96,7 @@ class Tooltip extends React.Component {
 
   componentDidMount() {
     warning(
-      !this.childrenRef ||
-        !this.childrenRef.disabled ||
-        !this.childrenRef.tagName.toLowerCase() === 'button',
+      !this.childrenRef.disabled || !this.childrenRef.tagName.toLowerCase() === 'button',
       [
         'Material-UI: you are providing a disabled `button` child to the Tooltip component.',
         'A disabled element does not fire events.',
@@ -125,18 +128,31 @@ class Tooltip extends React.Component {
     const { children, enterDelay } = this.props;
     const childrenProps = children.props;
 
-    if (event.type === 'focus' && childrenProps.onFocus) {
-      childrenProps.onFocus(event);
+    if (event.type === 'focus') {
+      this.internalState.focus = true;
+
+      if (childrenProps.onFocus) {
+        childrenProps.onFocus(event);
+      }
     }
 
-    if (event.type === 'mouseenter' && childrenProps.onMouseEnter) {
-      childrenProps.onMouseEnter(event);
+    if (event.type === 'mouseenter') {
+      this.internalState.hover = true;
+
+      if (childrenProps.onMouseEnter) {
+        childrenProps.onMouseEnter(event);
+      }
     }
 
     if (this.ignoreNonTouchEvents && event.type !== 'touchstart') {
       return;
     }
 
+    // Remove the title ahead of time.
+    // We don't want to wait for the next render commit.
+    // We would risk displaying two tooltips at the same time (native + this one).
+    this.childrenRef.setAttribute('title', '');
+
     clearTimeout(this.enterTimer);
     clearTimeout(this.leaveTimer);
     if (enterDelay) {
@@ -163,12 +179,20 @@ class Tooltip extends React.Component {
     const { children, leaveDelay } = this.props;
     const childrenProps = children.props;
 
-    if (event.type === 'blur' && childrenProps.onBlur) {
-      childrenProps.onBlur(event);
+    if (event.type === 'blur') {
+      this.internalState.focus = false;
+
+      if (childrenProps.onBlur) {
+        childrenProps.onBlur(event);
+      }
     }
 
-    if (event.type === 'mouseleave' && childrenProps.onMouseLeave) {
-      childrenProps.onMouseLeave(event);
+    if (event.type === 'mouseleave') {
+      this.internalState.hover = false;
+
+      if (childrenProps.onMouseLeave) {
+        childrenProps.onMouseLeave(event);
+      }
     }
 
     clearTimeout(this.enterTimer);
@@ -184,6 +208,10 @@ class Tooltip extends React.Component {
   };
 
   handleClose = event => {
+    if (this.internalState.focus || this.internalState.hover) {
+      return;
+    }
+
     if (!this.isControlled) {
       this.setState({ open: false });
     }


Test patch:
diff --git a/packages/material-ui/src/Tooltip/Tooltip.test.js b/packages/material-ui/src/Tooltip/Tooltip.test.js
--- a/packages/material-ui/src/Tooltip/Tooltip.test.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.test.js
@@ -20,7 +20,7 @@ describe('<Tooltip />', () => {
   };
 
   before(() => {
-    shallow = createShallow({ dive: true });
+    shallow = createShallow({ dive: true, disableLifecycleMethods: true });
     mount = createMount();
     classes = getClasses(<Tooltip {...defaultProps} />);
   });
@@ -58,11 +58,12 @@ describe('<Tooltip />', () => {
 
   it('should respond to external events', () => {
     const wrapper = shallow(<Tooltip {...defaultProps} />);
+    wrapper.instance().childrenRef = document.createElement('div');
     const children = wrapper.childAt(0).childAt(0);
     assert.strictEqual(wrapper.state().open, false);
-    children.simulate('mouseEnter', {});
+    children.simulate('mouseEnter', { type: 'mouseenter' });
     assert.strictEqual(wrapper.state().open, true);
-    children.simulate('blur', {});
+    children.simulate('mouseLeave', { type: 'mouseleave' });
     assert.strictEqual(wrapper.state().open, false);
   });
 
@@ -73,17 +74,32 @@ describe('<Tooltip />', () => {
     const wrapper = shallow(
       <Tooltip {...defaultProps} open onOpen={handleRequestOpen} onClose={handleClose} />,
     );
+    wrapper.instance().childrenRef = document.createElement('div');
     const children = wrapper.childAt(0).childAt(0);
     assert.strictEqual(handleRequestOpen.callCount, 0);
     assert.strictEqual(handleClose.callCount, 0);
-    children.simulate('mouseEnter', { type: 'mouseover' });
+    children.simulate('mouseEnter', { type: 'mouseenter' });
     assert.strictEqual(handleRequestOpen.callCount, 1);
     assert.strictEqual(handleClose.callCount, 0);
-    children.simulate('blur', { type: 'blur' });
+    children.simulate('mouseLeave', { type: 'mouseleave' });
     assert.strictEqual(handleRequestOpen.callCount, 1);
     assert.strictEqual(handleClose.callCount, 1);
   });
 
+  it('should close when the interaction is over', () => {
+    const wrapper = shallow(<Tooltip {...defaultProps} />);
+    wrapper.instance().childrenRef = document.createElement('div');
+    const children = wrapper.childAt(0).childAt(0);
+    assert.strictEqual(wrapper.state().open, false);
+    children.simulate('mouseEnter', { type: 'mouseenter' });
+    children.simulate('focus', { type: 'focus' });
+    assert.strictEqual(wrapper.state().open, true);
+    children.simulate('mouseLeave', { type: 'mouseleave' });
+    assert.strictEqual(wrapper.state().open, true);
+    children.simulate('blur', { type: 'blur' });
+    assert.strictEqual(wrapper.state().open, false);
+  });
+
   describe('touch screen', () => {
     let clock;
 
@@ -97,6 +113,7 @@ describe('<Tooltip />', () => {
 
     it('should not respond to quick events', () => {
       const wrapper = shallow(<Tooltip {...defaultProps} />);
+      wrapper.instance().childrenRef = document.createElement('div');
       const children = wrapper.childAt(0).childAt(0);
       children.simulate('touchStart', { type: 'touchstart', persist });
       children.simulate('touchEnd', { type: 'touchend', persist });
@@ -107,6 +124,7 @@ describe('<Tooltip />', () => {
 
     it('should open on long press', () => {
       const wrapper = shallow(<Tooltip {...defaultProps} />);
+      wrapper.instance().childrenRef = document.createElement('div');
       const children = wrapper.childAt(0).childAt(0);
       children.simulate('touchStart', { type: 'touchstart', persist });
       children.simulate('focus', { type: 'focus' });
@@ -114,6 +132,7 @@ describe('<Tooltip />', () => {
       clock.tick(1e3);
       assert.strictEqual(wrapper.state().open, true);
       children.simulate('touchEnd', { type: 'touchend', persist });
+      children.simulate('blur', { type: 'blur' });
       clock.tick(1500);
       assert.strictEqual(wrapper.state().open, false);
     });
@@ -138,6 +157,7 @@ describe('<Tooltip />', () => {
 
     it('should take the enterDelay into account', () => {
       const wrapper = shallow(<Tooltip enterDelay={111} {...defaultProps} />);
+      wrapper.instance().childrenRef = document.createElement('div');
       const children = wrapper.childAt(0).childAt(0);
       children.simulate('focus', { type: 'focus', persist });
       assert.strictEqual(wrapper.state().open, false);
@@ -147,6 +167,7 @@ describe('<Tooltip />', () => {
 
     it('should take the leaveDelay into account', () => {
       const wrapper = shallow(<Tooltip leaveDelay={111} {...defaultProps} />);
+      wrapper.instance().childrenRef = document.createElement('div');
       const children = wrapper.childAt(0).childAt(0);
       children.simulate('focus', { type: 'focus' });
       assert.strictEqual(wrapper.state().open, true);
@@ -169,6 +190,7 @@ describe('<Tooltip />', () => {
               </button>
             </Tooltip>,
           );
+          wrapper.instance().childrenRef = document.createElement('div');
           const children = wrapper.childAt(0).childAt(0);
           const type = name.slice(2).toLowerCase();
           children.simulate(type, { type, persist });