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('', () => { - + ); } diff --git a/docs/data/material/components/tooltips/DisabledTooltips.tsx b/docs/data/material/components/tooltips/DisabledTooltips.tsx index a00764ef2a437e..d4d6edf6c6fae4 100644 --- a/docs/data/material/components/tooltips/DisabledTooltips.tsx +++ b/docs/data/material/components/tooltips/DisabledTooltips.tsx @@ -4,9 +4,9 @@ import Tooltip from '@mui/material/Tooltip'; export default function DisabledTooltips() { return ( - - - + ); } diff --git a/docs/data/material/components/tooltips/DisabledTooltips.tsx.preview b/docs/data/material/components/tooltips/DisabledTooltips.tsx.preview index f83193be661f3d..9253da1e600e21 100644 --- a/docs/data/material/components/tooltips/DisabledTooltips.tsx.preview +++ b/docs/data/material/components/tooltips/DisabledTooltips.tsx.preview @@ -1,5 +1,5 @@ - - - + \ No newline at end of file diff --git a/docs/data/material/components/tooltips/tooltips.md b/docs/data/material/components/tooltips/tooltips.md index 42dc76cfee6928..f07d3a1611b1ee 100644 --- a/docs/data/material/components/tooltips/tooltips.md +++ b/docs/data/material/components/tooltips/tooltips.md @@ -147,28 +147,14 @@ You can disable this behavior (thus failing the success criterion which is requi ## Disabled elements -By default disabled elements like ` - - -``` - ## Transitions Use `slots.transition` and `slotProps.transition` to use a different transition. From 1ef6079826fdf66bcdf5318a9a5825b32222dccc Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Mon, 8 Jun 2026 23:46:59 +0800 Subject: [PATCH 6/7] update test --- packages/mui-material/src/Tooltip/Tooltip.test.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/mui-material/src/Tooltip/Tooltip.test.js b/packages/mui-material/src/Tooltip/Tooltip.test.js index 0049d13be6471c..e958cd403a7e4a 100644 --- a/packages/mui-material/src/Tooltip/Tooltip.test.js +++ b/packages/mui-material/src/Tooltip/Tooltip.test.js @@ -442,10 +442,17 @@ describe('', () => { expect(screen.getByRole('tooltip')).toBeVisible(); await user.unhover(button); + await act(async () => { + await new Promise((resolve) => { + // Wait a bit but not long enough for the tooltip to disappear + setTimeout(resolve, 250); + }); + }); await user.hover(screen.getByRole('tooltip')); await act(async () => { await new Promise((resolve) => { - setTimeout(resolve, 600); + // Wait out the close timeout until the tooltip should have disappeared + setTimeout(resolve, 300); }); }); From bc2ce4845d1955e356694cfe77fc098cf1244270 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 18 Jun 2026 11:44:25 +0700 Subject: [PATCH 7/7] [tooltip] Revert docs changes, drop MUI Button test Keep PR scoped to Tooltip source + test. Docs rework and the Tooltip+Button combined test moved out; native button test retained. --- .../components/tooltips/DisabledTooltips.js | 6 +++--- .../components/tooltips/DisabledTooltips.tsx | 6 +++--- .../tooltips/DisabledTooltips.tsx.preview | 6 +++--- .../material/components/tooltips/tooltips.md | 18 ++++++++++++++++-- .../mui-material/src/Tooltip/Tooltip.test.js | 17 ----------------- 5 files changed, 25 insertions(+), 28 deletions(-) diff --git a/docs/data/material/components/tooltips/DisabledTooltips.js b/docs/data/material/components/tooltips/DisabledTooltips.js index d4d6edf6c6fae4..a00764ef2a437e 100644 --- a/docs/data/material/components/tooltips/DisabledTooltips.js +++ b/docs/data/material/components/tooltips/DisabledTooltips.js @@ -4,9 +4,9 @@ import Tooltip from '@mui/material/Tooltip'; export default function DisabledTooltips() { return ( - + + + ); } diff --git a/docs/data/material/components/tooltips/DisabledTooltips.tsx b/docs/data/material/components/tooltips/DisabledTooltips.tsx index d4d6edf6c6fae4..a00764ef2a437e 100644 --- a/docs/data/material/components/tooltips/DisabledTooltips.tsx +++ b/docs/data/material/components/tooltips/DisabledTooltips.tsx @@ -4,9 +4,9 @@ import Tooltip from '@mui/material/Tooltip'; export default function DisabledTooltips() { return ( - + + + ); } diff --git a/docs/data/material/components/tooltips/DisabledTooltips.tsx.preview b/docs/data/material/components/tooltips/DisabledTooltips.tsx.preview index 9253da1e600e21..f83193be661f3d 100644 --- a/docs/data/material/components/tooltips/DisabledTooltips.tsx.preview +++ b/docs/data/material/components/tooltips/DisabledTooltips.tsx.preview @@ -1,5 +1,5 @@ - + + + \ No newline at end of file diff --git a/docs/data/material/components/tooltips/tooltips.md b/docs/data/material/components/tooltips/tooltips.md index f07d3a1611b1ee..42dc76cfee6928 100644 --- a/docs/data/material/components/tooltips/tooltips.md +++ b/docs/data/material/components/tooltips/tooltips.md @@ -147,14 +147,28 @@ You can disable this behavior (thus failing the success criterion which is requi ## Disabled elements -When wrapping a Material UI component that inherits from `ButtonBase`, you should add the CSS property _pointer-events: auto;_ to your element when disabled: +By default disabled elements like ` + + +``` + ## Transitions Use `slots.transition` and `slotProps.transition` to use a different transition. diff --git a/packages/mui-material/src/Tooltip/Tooltip.test.js b/packages/mui-material/src/Tooltip/Tooltip.test.js index e958cd403a7e4a..778a4a0ed9665b 100644 --- a/packages/mui-material/src/Tooltip/Tooltip.test.js +++ b/packages/mui-material/src/Tooltip/Tooltip.test.js @@ -15,7 +15,6 @@ 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'; @@ -404,22 +403,6 @@ describe('', () => { 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();