From 583b3eb522d462d10ae9548924022ebe1cb0cef8 Mon Sep 17 00:00:00 2001 From: uditDewan Date: Wed, 14 Oct 2026 17:46:41 -0400 Subject: [PATCH] fix(menu): don't show focus ring on hover-initiated MenuItem focus On hover, onHoverIn hides the focus ring and useHoverFocusEffect then focuses the item. onFocus decided whether the focus came from hover by reading pressable.state.focused, which is stale at that point, so it always re-enabled the focus ring. Track hover-initiated focus with a ref instead so onFocus sees it synchronously. Fixes #4083 --- .changeset/menuitem-hover-focus-ring.md | 5 ++ .../src/MenuItem/useMenuItem.test.win32.tsx | 51 +++++++++++++++++++ .../Menu/src/MenuItem/useMenuItem.ts | 8 ++- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 .changeset/menuitem-hover-focus-ring.md create mode 100644 packages/components/Menu/src/MenuItem/useMenuItem.test.win32.tsx diff --git a/.changeset/menuitem-hover-focus-ring.md b/.changeset/menuitem-hover-focus-ring.md new file mode 100644 index 00000000000..a9483cf1fed --- /dev/null +++ b/.changeset/menuitem-hover-focus-ring.md @@ -0,0 +1,5 @@ +--- +'@fluentui-react-native/menu': patch +--- + +Fix `MenuItem` showing a focus ring when focus moves to it from mouse hover on win32. The hover-initiated focus is now tracked with a ref instead of stale pressable state. diff --git a/packages/components/Menu/src/MenuItem/useMenuItem.test.win32.tsx b/packages/components/Menu/src/MenuItem/useMenuItem.test.win32.tsx new file mode 100644 index 00000000000..6fcc84cfe92 --- /dev/null +++ b/packages/components/Menu/src/MenuItem/useMenuItem.test.win32.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { act } from 'react'; +import * as renderer from 'react-test-renderer'; + +import type { MenuItemInfo } from './MenuItem.types'; +import { useMenuItem } from './useMenuItem'; + +describe('useMenuItem focus ring (win32)', () => { + let info: MenuItemInfo; + const Probe = () => { + info = useMenuItem({ componentRef: React.useRef({ focus: jest.fn(), blur: jest.fn() } as any) }); + return null; + }; + + beforeEach(() => { + act(() => { + renderer.create(); + }); + }); + + it('keeps the focus ring hidden when hover moves focus before React re-renders', () => { + // Simulate the native focus event firing with the handlers from the same render as onHoverIn. + const { onHoverIn, onFocus } = info.props; + act(() => { + onHoverIn({} as any); + onFocus({} as any); + }); + expect(info.props.enableFocusRing).toBe(false); + }); + + it('shows the focus ring for a keyboard focus after a hover focus', () => { + act(() => { + info.props.onHoverIn({} as any); + }); + act(() => { + info.props.onFocus({} as any); + }); + expect(info.props.enableFocusRing).toBe(false); + + act(() => { + info.props.onBlur({} as any); + }); + act(() => { + info.props.onHoverOut({} as any); + }); + act(() => { + info.props.onFocus({} as any); + }); + expect(info.props.enableFocusRing).toBe(true); + }); +}); diff --git a/packages/components/Menu/src/MenuItem/useMenuItem.ts b/packages/components/Menu/src/MenuItem/useMenuItem.ts index 7364567e474..20d3f6f9d98 100644 --- a/packages/components/Menu/src/MenuItem/useMenuItem.ts +++ b/packages/components/Menu/src/MenuItem/useMenuItem.ts @@ -68,12 +68,16 @@ export const useMenuItem = (props: MenuItemProps): MenuItemInfo => { useHoverFocusEffect(pressable.state.hovered, componentRef); const [enableFocusRing, setEnableFocusRing] = React.useState(!pressable.state.hovered); + // Tracked in a ref rather than state: the focus caused by hovering can fire before React re-renders, so state read in + // onFocus would still be the pre-hover value. + const isHoverFocus = React.useRef(false); const onHoverIn = React.useCallback( (e) => { pressable.props.onHoverIn(e); // when it's a hover focus, set enableFocusRing explicitly to false if (!pressable.state.focused) { + isHoverFocus.current = true; setEnableFocusRing(false); } }, @@ -84,7 +88,9 @@ export const useMenuItem = (props: MenuItemProps): MenuItemInfo => { (e) => { pressable.props.onFocus(e); // when it's not a hover focus, set enableFocusRing explicitly to true - if (!pressable.state.focused) { + if (isHoverFocus.current) { + isHoverFocus.current = false; + } else { setEnableFocusRing(true); } },