diff --git a/.changeset/mosaic-menu-pointer-focus-ring.md b/.changeset/mosaic-menu-pointer-focus-ring.md new file mode 100644 index 00000000000..0ad91650ea1 --- /dev/null +++ b/.changeset/mosaic-menu-pointer-focus-ring.md @@ -0,0 +1,5 @@ +--- +'@clerk/mosaic': patch +--- + +Stop showing keyboard focus rings on menu items when a menu is opened and hovered with the mouse. Focus rings still appear when navigating the menu with the keyboard. diff --git a/packages/mosaic/src/primitives/menu/menu-root.tsx b/packages/mosaic/src/primitives/menu/menu-root.tsx index 46ff88104b6..a95c7dd358b 100644 --- a/packages/mosaic/src/primitives/menu/menu-root.tsx +++ b/packages/mosaic/src/primitives/menu/menu-root.tsx @@ -3,6 +3,7 @@ import { arrow, autoUpdate, + type ElementProps, flip, FloatingNode, FloatingTree, @@ -112,8 +113,21 @@ function MenuInner(props: MenuProps) { const click = useClick(floatingContext, { event: 'mousedown', toggle: !isNested, - ignoreMouse: isNested, + ignoreMouse: true, }); + const mousePress = useMemo( + () => ({ + reference: { + onPointerDown(event: React.PointerEvent) { + if (isNested || event.button !== 0 || (event.pointerType !== 'mouse' && event.pointerType !== 'pen')) { + return; + } + floatingContext.onOpenChange(!open, event.nativeEvent, 'click'); + }, + }, + }), + [floatingContext, isNested, open], + ); const baseRole = useRole(floatingContext, { role: 'menu' }); // `useRole` decides submenu-ness from the floating tree alone, so a menu inside a popover gets // `role="menuitem"` on its trigger with no parent menu to be an item of. `isNested` is the real answer. @@ -143,6 +157,7 @@ function MenuInner(props: MenuProps) { const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([ hover, click, + mousePress, role, dismiss, listNavigation, diff --git a/packages/mosaic/src/primitives/menu/menu.test.tsx b/packages/mosaic/src/primitives/menu/menu.test.tsx index 27d1f59f588..d7fde5da50e 100644 --- a/packages/mosaic/src/primitives/menu/menu.test.tsx +++ b/packages/mosaic/src/primitives/menu/menu.test.tsx @@ -58,6 +58,27 @@ describe('Menu', () => { expect(screen.getByText('Actions')).toHaveAttribute('data-open', ''); }); + it('lets a mouse press focus the trigger as it opens', async () => { + const user = userEvent.setup(); + render( + + Actions + + + Cut + + + , + ); + const onFocus = vi.fn(); + screen.getByText('Actions').addEventListener('focus', onFocus); + + await user.pointer({ keys: '[MouseLeft>]', target: screen.getByText('Actions') }); + + expect(onFocus).toHaveBeenCalled(); + expect(screen.getByText('Cut')).toBeInTheDocument(); + }); + it('closes on trigger click when open', async () => { const user = userEvent.setup(); render(