From 15bcf67a3629138be91bffdb7aff306da09e4730 Mon Sep 17 00:00:00 2001 From: Linruoxin-Sy Date: Thu, 5 Nov 2026 15:08:45 +0800 Subject: [PATCH] [menubar] Reset stale focus blocking when focus interactions are disabled --- .../popups/interactions/useFocus.test.tsx | 54 ++++++++++++++++++- .../src/utils/popups/interactions/useFocus.ts | 2 + 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/react/src/utils/popups/interactions/useFocus.test.tsx b/packages/react/src/utils/popups/interactions/useFocus.test.tsx index 92ea0d8461c..ed242904ff5 100644 --- a/packages/react/src/utils/popups/interactions/useFocus.test.tsx +++ b/packages/react/src/utils/popups/interactions/useFocus.test.tsx @@ -1,11 +1,14 @@ import { expect, vi, beforeEach, afterEach, it } from 'vitest'; import { act, fireEvent, render, screen } from '@mui/internal-test-utils'; import * as React from 'react'; -import { isJSDOM, useTestInteractions } from '#test-utils'; +import { createRenderer, isJSDOM, useTestInteractions } from '#test-utils'; import { useFloating } from '../../../../test/floating-ui-tests/useFloating'; import { useFocus } from './useFocus'; +import { useDismiss } from './useDismiss'; describe.skipIf(!isJSDOM)('useFocus', () => { + const { render: renderPopup } = createRenderer(); + beforeEach(() => { vi.useFakeTimers(); }); @@ -14,6 +17,39 @@ describe.skipIf(!isJSDOM)('useFocus', () => { vi.useRealTimers(); }); + it('opens on focus again after being disabled following Escape dismissal', async () => { + const { setProps } = await renderPopup(); + const button = screen.getByRole('button'); + + await act(async () => button.focus()); + expect(screen.getByRole('tooltip')).toBeVisible(); + + fireEvent.keyDown(button, { key: 'Escape' }); + expect(screen.queryByRole('tooltip')).toBe(null); + + await setProps({ enabled: false }); + // While disabled, the blur handler cannot clear the dismissal block. + await act(async () => button.blur()); + await setProps({ enabled: true }); + + await act(async () => button.focus()); + expect(screen.getByRole('tooltip')).toBeVisible(); + }); + + it('does not reopen on restored focus after Escape while still enabled', async () => { + await renderPopup(); + const button = screen.getByRole('button'); + + await act(async () => button.focus()); + expect(screen.getByRole('tooltip')).toBeVisible(); + + fireEvent.keyDown(button, { key: 'Escape' }); + expect(screen.queryByRole('tooltip')).toBe(null); + + fireEvent.focus(button); + expect(screen.queryByRole('tooltip')).toBe(null); + }); + it('does not reopen when focus is restored after leaving the tab', async () => { function App() { const [open, setOpen] = React.useState(false); @@ -50,3 +86,19 @@ describe.skipIf(!isJSDOM)('useFocus', () => { expect(screen.queryByRole('tooltip')).toBe(null); }); }); + +function FocusPopup({ enabled }: { enabled: boolean }) { + const [open, setOpen] = React.useState(false); + const { refs, context } = useFloating({ open, onOpenChange: setOpen }); + const { getReferenceProps, getFloatingProps } = useTestInteractions([ + useFocus(context.rootStore, { enabled }), + useDismiss(context.rootStore), + ]); + + return ( + + + {open &&
} + + ); +} diff --git a/packages/react/src/utils/popups/interactions/useFocus.ts b/packages/react/src/utils/popups/interactions/useFocus.ts index 37ba7c981ef..7231a0c37cc 100644 --- a/packages/react/src/utils/popups/interactions/useFocus.ts +++ b/packages/react/src/utils/popups/interactions/useFocus.ts @@ -52,6 +52,8 @@ export function useFocus(store: PopupInteractionStore, props: UseFocusProps = {} const domReference = store.select('anchorElement'); if (!enabled) { + blockFocusRef.current = false; + blockedReferenceRef.current = null; return undefined; }