Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/menuitem-hover-focus-ring.md
Original file line number Diff line number Diff line change
@@ -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.
51 changes: 51 additions & 0 deletions packages/components/Menu/src/MenuItem/useMenuItem.test.win32.tsx
Original file line number Diff line number Diff line change
@@ -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(<Probe />);
});
});

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);
});
});
8 changes: 7 additions & 1 deletion packages/components/Menu/src/MenuItem/useMenuItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
},
Expand All @@ -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);
}
},
Expand Down