Repository: mui/material-ui (TypeScript)
Date: 2020-11-11 20:46:55+00:00
Task type: Bug Fix

Issue:
[Tooltip] onOpen / onClose unexpected behaviour  
- [x] The issue is present in the latest release.
- [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.

## Current Behavior 😯

When hovering on different targets within the `Tooltip` children, it triggers `onOpen` rather than trigger it only if `open` was changed to `true`.

## Expected Behavior 🤔

I would expect the `onOpen` and `onClose` to trigger only when the `open` state is actually changed.

## Steps to Reproduce 🕹

See codesandbox for both the reproduction of the bug and also a workaround I'm using for now:
https://codesandbox.io/s/mui-tooltip-onopen-onclose-bug-txk0u

| Tech        | Version |
| ----------- | ------- |
| Material-UI | v4.11.0  |


Issue discussion:
Thanks for the report.

Can reproduce with v5 as well: https://codesandbox.io/s/mui-tooltip-onopen-onclose-bug-forked-it19t as well.
@dvh91 Interesting problem. The motivation for using *mouseover* over *mouseenter* is covered in 

https://github.com/mui-org/material-ui/blob/37d6ad0e7fed9c71fe05d1d4293667169fe46de0/packages/material-ui/src/Tooltip/Tooltip.js#L256-L258

Maybe something like this?

```diff
diff --git a/packages/material-ui/src/Tooltip/Tooltip.js b/packages/material-ui/src/Tooltip/Tooltip.js
index 8739d9c7ec..f64464b5f6 100644
--- a/packages/material-ui/src/Tooltip/Tooltip.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.js
@@ -258,7 +258,7 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) {
     // We are using the mouseover event instead of the mouseenter event to fix a hide/show issue.
     setOpenState(true);

-    if (onOpen) {
+    if (onOpen && !open) {
       onOpen(event);
     }
   };
diff --git a/packages/material-ui/src/Tooltip/Tooltip.test.js b/packages/material-ui/src/Tooltip/Tooltip.test.js
index 2a4a34850c..80811f4c81 100644
--- a/packages/material-ui/src/Tooltip/Tooltip.test.js
+++ b/packages/material-ui/src/Tooltip/Tooltip.test.js
@@ -287,13 +287,13 @@ describe('<Tooltip />', () => {
   it('should be controllable', () => {
     const eventLog = [];

-    const { getByRole } = render(
+    const { getByRole, setProps } = render(
       <Tooltip
         enterDelay={100}
         title="Hello World"
         onOpen={() => eventLog.push('open')}
         onClose={() => eventLog.push('close')}
-        open
+        open={false}
       >
         <button
           id="testChild"
@@ -314,6 +314,7 @@ describe('<Tooltip />', () => {
     });

     expect(eventLog).to.deep.equal(['mouseover', 'open']);
+    setProps({ open: true });

     fireEvent.mouseLeave(getByRole('button'));
     act(() => {
```
@oliviertassinari 
Something to note- it also applies to `onClose` when `enterDelay` > 0 and `onClose` triggered while `open` is still falsy.
@dvh91 OK, this sounds like something we can handle at the same time. 
Is it ok if I give this a try? :) 

Solution patch:
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
@@ -258,7 +258,7 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) {
     // We are using the mouseover event instead of the mouseenter event to fix a hide/show issue.
     setOpenState(true);
 
-    if (onOpen) {
+    if (onOpen && !open) {
       onOpen(event);
     }
   };
@@ -274,7 +274,7 @@ const Tooltip = React.forwardRef(function Tooltip(props, ref) {
       }, 800 + leaveDelay);
       setOpenState(false);
 
-      if (onClose) {
+      if (onClose && open) {
         onClose(event);
       }
 


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
@@ -287,13 +287,13 @@ describe('<Tooltip />', () => {
   it('should be controllable', () => {
     const eventLog = [];
 
-    const { getByRole } = render(
+    const { getByRole, setProps } = render(
       <Tooltip
         enterDelay={100}
         title="Hello World"
         onOpen={() => eventLog.push('open')}
         onClose={() => eventLog.push('close')}
-        open
+        open={false}
       >
         <button
           id="testChild"
@@ -314,6 +314,7 @@ describe('<Tooltip />', () => {
     });
 
     expect(eventLog).to.deep.equal(['mouseover', 'open']);
+    setProps({ open: true });
 
     fireEvent.mouseLeave(getByRole('button'));
     act(() => {
@@ -323,6 +324,40 @@ describe('<Tooltip />', () => {
     expect(eventLog).to.deep.equal(['mouseover', 'open', 'mouseleave', 'close']);
   });
 
+  it('should not call onOpen again if already open', () => {
+    const eventLog = [];
+    const { getByTestId } = render(
+      <Tooltip enterDelay={100} title="Hello World" onOpen={() => eventLog.push('open')} open>
+        <button data-testid="trigger" onMouseOver={() => eventLog.push('mouseover')} />
+      </Tooltip>,
+    );
+
+    expect(eventLog).to.deep.equal([]);
+
+    fireEvent.mouseOver(getByTestId('trigger'));
+    act(() => {
+      clock.tick(100);
+    });
+
+    expect(eventLog).to.deep.equal(['mouseover']);
+  });
+
+  it('should not call onClose if already closed', () => {
+    const eventLog = [];
+    const { getByTestId } = render(
+      <Tooltip title="Hello World" onClose={() => eventLog.push('close')} open={false}>
+        <button data-testid="trigger" onMouseLeave={() => eventLog.push('mouseleave')} />
+      </Tooltip>,
+    );
+
+    fireEvent.mouseLeave(getByTestId('trigger'));
+    act(() => {
+      clock.tick(0);
+    });
+
+    expect(eventLog).to.deep.equal(['mouseleave']);
+  });
+
   it('is dismissable by pressing Escape', () => {
     const transitionTimeout = 0;
     render(