From cb7a2705cc17e757deb6914305f798367a0d94e1 Mon Sep 17 00:00:00 2001
From: Mayank <9084735+mayank99@users.noreply.github.com>
Date: Thu, 4 Jun 2026 13:30:38 -0400
Subject: [PATCH 1/7] remove `Tooltip` warning about disabled buttons
Signed-off-by: Mayank <9084735+mayank99@users.noreply.github.com>
---
packages/mui-material/src/Tooltip/Tooltip.js | 28 --------------------
1 file changed, 28 deletions(-)
diff --git a/packages/mui-material/src/Tooltip/Tooltip.js b/packages/mui-material/src/Tooltip/Tooltip.js
index f0ffb0c969f66a..d9e25557cea15f 100644
--- a/packages/mui-material/src/Tooltip/Tooltip.js
+++ b/packages/mui-material/src/Tooltip/Tooltip.js
@@ -281,34 +281,6 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
let open = openState;
- if (process.env.NODE_ENV !== 'production') {
- // TODO: uncomment once we enable eslint-plugin-react-compiler // eslint-disable-next-line react-compiler/react-compiler
- // eslint-disable-next-line react-hooks/rules-of-hooks -- process.env never changes
- const { current: isControlled } = React.useRef(openProp !== undefined);
-
- // TODO: uncomment once we enable eslint-plugin-react-compiler // eslint-disable-next-line react-compiler/react-compiler
- // eslint-disable-next-line react-hooks/rules-of-hooks -- process.env never changes
- React.useEffect(() => {
- if (
- childNode &&
- childNode.disabled &&
- !isControlled &&
- title !== '' &&
- childNode.tagName.toLowerCase() === 'button'
- ) {
- console.warn(
- [
- 'MUI: You are providing a disabled `button` child to the Tooltip component.',
- 'A disabled element does not fire events.',
- "Tooltip needs to listen to the child element's events to display the title.",
- '',
- 'Add a simple wrapper element, such as a `span`.',
- ].join('\n'),
- );
- }
- }, [title, childNode, isControlled]);
- }
-
const id = useId(idProp);
const prevUserSelect = React.useRef();
From f30d046e529ad9e354a10756798224e1f08f5fdb Mon Sep 17 00:00:00 2001
From: Mayank <9084735+mayank99@users.noreply.github.com>
Date: Thu, 4 Jun 2026 13:43:05 -0400
Subject: [PATCH 2/7] update test
Signed-off-by: Mayank <9084735+mayank99@users.noreply.github.com>
---
.../mui-material/src/Tooltip/Tooltip.test.js | 38 -------------------
1 file changed, 38 deletions(-)
diff --git a/packages/mui-material/src/Tooltip/Tooltip.test.js b/packages/mui-material/src/Tooltip/Tooltip.test.js
index 3106a6578c43fb..91beea09010290 100644
--- a/packages/mui-material/src/Tooltip/Tooltip.test.js
+++ b/packages/mui-material/src/Tooltip/Tooltip.test.js
@@ -794,44 +794,6 @@ describe('', () => {
});
});
- describe('disabled button warning', () => {
- it('should not raise a warning if title is empty', () => {
- expect(() => {
- render(
-
-
- ,
- );
- }).not.toErrorDev();
- });
-
- it('should raise a warning when we are uncontrolled and can not listen to events', () => {
- expect(() => {
- render(
-
-
- ,
- );
- }).toWarnDev('MUI: You are providing a disabled `button` child to the Tooltip component');
- });
-
- it('should not raise a warning when we are controlled', () => {
- expect(() => {
- render(
-
-
- ,
- );
- }).not.toErrorDev();
- });
- });
-
describe('prop: disableInteractive', () => {
it('when false should keep the overlay open if the popper element is hovered', () => {
render(
From 5a10fee0d61538477b5487c1f6ecb7130c9ed8c4 Mon Sep 17 00:00:00 2001
From: Albert Yu
Date: Fri, 5 Jun 2026 06:56:56 +0800
Subject: [PATCH 3/7] support disabled button triggers
---
packages/mui-material/src/Tooltip/Tooltip.js | 42 +++++++++--
.../mui-material/src/Tooltip/Tooltip.test.js | 73 ++++++++++++++++++-
2 files changed, 105 insertions(+), 10 deletions(-)
diff --git a/packages/mui-material/src/Tooltip/Tooltip.js b/packages/mui-material/src/Tooltip/Tooltip.js
index d9e25557cea15f..d181976b02b54e 100644
--- a/packages/mui-material/src/Tooltip/Tooltip.js
+++ b/packages/mui-material/src/Tooltip/Tooltip.js
@@ -264,6 +264,7 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
const [childNode, setChildNode] = React.useState();
const [arrowRef, setArrowRef] = React.useState(null);
const ignoreNonTouchEvents = React.useRef(false);
+ const openedByDisabledTriggerRef = React.useRef(false);
const disableInteractive = disableInteractiveProp || followCursor;
@@ -280,6 +281,7 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
});
let open = openState;
+ const { current: isControlled } = React.useRef(openProp !== undefined);
const id = useId(idProp);
@@ -313,6 +315,7 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
* @param {React.SyntheticEvent | Event} event
*/
(event) => {
+ openedByDisabledTriggerRef.current = false;
hystersisTimer.start(800 + leaveDelay, () => {
hystersisOpen = false;
});
@@ -329,9 +332,6 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
);
const handleMouseOver = (event) => {
- if (childNode?.disabled) {
- return;
- }
if (ignoreNonTouchEvents.current && event.type !== 'touchstart') {
return;
}
@@ -354,6 +354,31 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
}
};
+ const handleTriggerMouseOver = (event) => {
+ if (childNode?.disabled && !isControlled) {
+ // A disabled trigger can open the tooltip if it receives pointer events.
+ // However, if the trigger became disabled while the tooltip was already open,
+ // stray mouseover events must not cancel the pending close.
+ if (open && !openedByDisabledTriggerRef.current) {
+ return;
+ }
+
+ openedByDisabledTriggerRef.current = true;
+ } else {
+ openedByDisabledTriggerRef.current = false;
+ }
+
+ handleMouseOver(event);
+ };
+
+ const handleInteractiveWrapperMouseOver = (event) => {
+ if (childNode?.disabled && !isControlled && !openedByDisabledTriggerRef.current) {
+ return;
+ }
+
+ handleMouseOver(event);
+ };
+
const handleMouseLeave = (event) => {
enterTimer.clear();
leaveTimer.start(leaveDelay, () => {
@@ -390,6 +415,8 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
setChildNode(event.currentTarget);
}
+ openedByDisabledTriggerRef.current = false;
+
if (isFocusVisible(event.target)) {
// Workaround for https://github.com/facebook/react/issues/9142.
// React does not fire blur when a focused element becomes disabled.
@@ -427,7 +454,7 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
touchTimer.start(enterTouchDelay, () => {
document.body.style.WebkitUserSelect = prevUserSelect.current;
- handleMouseOver(event);
+ handleTriggerMouseOver(event);
});
};
@@ -531,11 +558,14 @@ const Tooltip = React.forwardRef(function Tooltip(inProps, ref) {
}
if (!disableHoverListener) {
- childrenProps.onMouseOver = composeEventHandler(handleMouseOver, childrenProps.onMouseOver);
+ childrenProps.onMouseOver = composeEventHandler(
+ handleTriggerMouseOver,
+ childrenProps.onMouseOver,
+ );
childrenProps.onMouseLeave = composeEventHandler(handleMouseLeave, childrenProps.onMouseLeave);
if (!disableInteractive) {
- interactiveWrapperListeners.onMouseOver = handleMouseOver;
+ interactiveWrapperListeners.onMouseOver = handleInteractiveWrapperMouseOver;
interactiveWrapperListeners.onMouseLeave = handleMouseLeave;
}
}
diff --git a/packages/mui-material/src/Tooltip/Tooltip.test.js b/packages/mui-material/src/Tooltip/Tooltip.test.js
index 91beea09010290..d701b5802e31e0 100644
--- a/packages/mui-material/src/Tooltip/Tooltip.test.js
+++ b/packages/mui-material/src/Tooltip/Tooltip.test.js
@@ -15,6 +15,7 @@ import {
} from '@mui/internal-test-utils';
import { camelCase } from 'es-toolkit/string';
import Tooltip, { tooltipClasses as classes } from '@mui/material/Tooltip';
+import Button from '@mui/material/Button';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { testReset } from './Tooltip';
import describeConformance from '../../test/describeConformance';
@@ -387,6 +388,70 @@ describe('', () => {
expect(screen.queryByRole('tooltip')).to.equal(null);
});
+ it('opens when a disabled native button receives mouse events', async () => {
+ clock.restore();
+
+ const { user } = render(
+
+
+ ,
+ );
+
+ await user.hover(screen.getByRole('button'));
+
+ expect(screen.getByRole('tooltip')).toBeVisible();
+ });
+
+ it('opens when a disabled Button receives mouse events', async () => {
+ clock.restore();
+
+ const { user } = render(
+
+
+ ,
+ );
+
+ await user.hover(screen.getByRole('button'));
+
+ expect(screen.getByRole('tooltip')).toBeVisible();
+ });
+
+ it('keeps a disabled-trigger tooltip open when the tooltip is hovered', async () => {
+ clock.restore();
+
+ const { user } = render(
+
+
+ ,
+ );
+
+ const button = screen.getByRole('button');
+
+ await user.hover(button);
+ expect(screen.getByRole('tooltip')).toBeVisible();
+
+ await user.unhover(button);
+ await user.hover(screen.getByRole('tooltip'));
+ await act(async () => {
+ await new Promise((resolve) => {
+ setTimeout(resolve, 50);
+ });
+ });
+
+ expect(screen.getByRole('tooltip')).toBeVisible();
+ });
+
it('opens on the next task when reduced motion is always', () => {
const handleEntered = spy();
const theme = createTheme({
@@ -1123,9 +1188,9 @@ describe('', () => {
expect(handleClose.callCount).to.equal(1);
});
- it('stays closed when a stray mouseover lands while the disabled child is closing', async () => {
+ it('stays closed when a stray mouseover lands while the disabled trigger 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
+ // when the focused trigger 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
@@ -1157,11 +1222,11 @@ describe('', () => {
expect(screen.getByRole('tooltip')).toBeVisible();
});
- // Disabling the focused child schedules the close (leaveDelay window still pending).
+ // Disabling the focused trigger 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'));
+ await user.hover(screen.getByRole('tooltip'));
// The disabled anchor must still close (and not reopen).
await waitFor(() => {
From f57565bee3d6ea33e0092824d7b36eb048da47de Mon Sep 17 00:00:00 2001
From: Albert Yu
Date: Fri, 5 Jun 2026 07:18:48 +0800
Subject: [PATCH 4/7] fix ci
---
packages/mui-material/src/Tooltip/Tooltip.test.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/mui-material/src/Tooltip/Tooltip.test.js b/packages/mui-material/src/Tooltip/Tooltip.test.js
index d701b5802e31e0..0049d13be6471c 100644
--- a/packages/mui-material/src/Tooltip/Tooltip.test.js
+++ b/packages/mui-material/src/Tooltip/Tooltip.test.js
@@ -427,7 +427,7 @@ describe('', () => {