diff --git a/packages/mui-material/src/Tooltip/Tooltip.js b/packages/mui-material/src/Tooltip/Tooltip.js index cede32e7240910..f0ffb0c969f66a 100644 --- a/packages/mui-material/src/Tooltip/Tooltip.js +++ b/packages/mui-material/src/Tooltip/Tooltip.js @@ -357,6 +357,9 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) { ); const handleMouseOver = (event) => { + if (childNode?.disabled) { + return; + } if (ignoreNonTouchEvents.current && event.type !== 'touchstart') { return; } diff --git a/packages/mui-material/src/Tooltip/Tooltip.test.js b/packages/mui-material/src/Tooltip/Tooltip.test.js index 213c2906a67982..9f4bbd3e9f2372 100644 --- a/packages/mui-material/src/Tooltip/Tooltip.test.js +++ b/packages/mui-material/src/Tooltip/Tooltip.test.js @@ -1132,6 +1132,53 @@ describe('', () => { expect(handleClose.callCount).to.equal(1); }); + it('stays closed when a stray mouseover lands while the disabled child is closing', async () => { + // Deterministic regression test for the flaky "stuck open" tooltip: + // when the focused child becomes disabled the close is scheduled via the React + // #9142 native-blur workaround, but a layout-shift `mouseover` on the interactive + // popper used to cancel that pending close and reopen the tooltip. A disabled + // anchor must never (re)open. `leaveDelay` opens a deterministic window in which to + // dispatch the stray `mouseover` before the close fires. + clock.restore(); + const handleClose = spy(); + + function TestCase() { + const [disabled, setDisabled] = React.useState(false); + return ( + + + + ); + } + + const { user } = render(); + + await user.tab(); + await waitFor(() => { + expect(screen.getByRole('tooltip')).toBeVisible(); + }); + + // Disabling the focused child schedules the close (leaveDelay window still pending). + await user.keyboard('{Enter}'); + + // A stray `mouseover` reaches the interactive popper before the close fires. + fireEvent.mouseOver(screen.getByRole('tooltip')); + + // The disabled anchor must still close (and not reopen). + await waitFor(() => { + expect(screen.queryByRole('tooltip')).to.equal(null); + }); + expect(handleClose.callCount).to.equal(1); + }); + it('closes on blur', async () => { const eventLog = []; const transitionTimeout = 0;