Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/mui-material/src/Tooltip/Tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
47 changes: 47 additions & 0 deletions packages/mui-material/src/Tooltip/Tooltip.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,53 @@ describe('<Tooltip />', () => {
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 (
<Tooltip
enterDelay={0}
leaveDelay={100}
onClose={handleClose}
title="Some information"
slotProps={{ transition: { timeout: 0 } }}
>
<button disabled={disabled} onClick={() => setDisabled(true)}>
Disable
</button>
</Tooltip>
);
}

const { user } = render(<TestCase />);

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;
Expand Down
Loading