Skip to content

feat(menu): add focusOnItemEnter opt-out to Menu.Content#3979

Open
DavidMetcalfe wants to merge 2 commits into
radix-ui:mainfrom
DavidMetcalfe:fix/menu-focus-on-item-enter
Open

feat(menu): add focusOnItemEnter opt-out to Menu.Content#3979
DavidMetcalfe wants to merge 2 commits into
radix-ui:mainfrom
DavidMetcalfe:fix/menu-focus-on-item-enter

Conversation

@DavidMetcalfe

@DavidMetcalfe DavidMetcalfe commented Jun 28, 2026

Copy link
Copy Markdown

Summary

Adds a focusOnItemEnter?: boolean prop to Menu.Content that lets consumers opt out of the hover-to-focus behavior in MenuItemImpl. Default true — zero behavior change for existing users.

Background

MenuItemImpl.onPointerMove calls item.focus({preventScroll: true}) on every mouseover (see packages/react/menu/src/menu.tsx). The in-source comment justifies this as matching native menu UX. But when a menu contains focusable siblings (e.g. an embedded search <input>), the focus theft breaks typing and breaks backspace-key hold-to-delete.

This has been reported and is still unfixed after three years:

What this PR does

  • Adds focusOnItemEnter?: boolean to MenuContentImplProps. Inherited automatically by DropdownMenuContent, ContextMenuContent, MenubarContent, and MenuSubContent.
  • Default true preserves the current behavior.
  • false skips the item.focus() call inside the MenuItemImpl.onPointerMove handler — but leaves onItemEnter(event) and the rest of the pointermove handler untouched, so submenu hover-open via MenuSubTrigger (which uses a setTimeout(open, 100) on onPointerGraceIntentChange, not on item.focus()) is unaffected.

The name focusOnItemEnter describes what the prop gates: the focus call that fires inside the item-pointerenter handler. The onItemEnter event dispatch itself is preserved.

<DropdownMenu.Content focusOnItemEnter={false}>
  <input type="text" placeholder="Filter…" />
  <DropdownMenu.Item>Item 1</DropdownMenu.Item>
  <DropdownMenu.Item>Item 2</DropdownMenu.Item>
</DropdownMenu.Content>

Visual tradeoff

When focusOnItemEnter={false}, items no longer set data-highlighted on hover (that attribute tracks focus state). Consumers who want a hover visual should style via :hover or a separate onPointerEnter mechanism. Documented in the JSDoc.

Test plan

  • pnpm test — 378 tests pass (was 373), 5 new vitest cases in dropdown-menu.test.tsx:
    • Default-behavior regression guard (focusOnItemEnter omitted → item still gets focus on hover)
    • Opt-out keeps focus on a sibling <input>
    • Multi-row scenario (focus stays on input across many pointer moves)
    • Keyboard-open regression guard (RovingFocusGroup entry-focus path unaffected)
    • Submenu hover-open regression guard — verifies that MenuSubTrigger still opens the submenu on hover when the parent has focusOnItemEnter={false}
  • pnpm types:check clean across the monorepo
  • pnpm lint clean
  • pnpm format:check clean on changed files
  • pnpm build clean

Storybook

Adds a WithSearchInput story to apps/storybook/stories/dropdown-menu.stories.tsx showing the default and opt-out side-by-side.

Changeset

Adds a .changeset/focus-on-item-enter.md marking minor for @radix-ui/react-menu, @radix-ui/react-dropdown-menu, @radix-ui/react-context-menu, @radix-ui/react-menubar.

Files changed

  • packages/react/menu/src/menu.tsx (+52/-1)
  • packages/react/dropdown-menu/src/dropdown-menu.test.tsx (+129)
  • apps/storybook/stories/dropdown-menu.stories.tsx (+116)
  • .changeset/focus-on-item-enter.md (+22, new)

API name

focusOnItemEnter follows existing Radix boolean-prop conventions (modal, loop, forceMount, trapFocus). I'm open to alternative names if maintainers prefer — disableHoverFocus or autoFocusItems={false} have both been requested in #2193 by other users. Happy to rename + push a follow-up if needed.

Open question

API scope: currently the prop lives only on Menu.Content (and inheritors via the MenuRootContentTypeProps chain). Should it also cascade from Menu.Root / DropdownMenu.Root for menu-wide convenience, given that submenus instantiate their own MenuContentProvider? I went with per-Content scope because each submenu can independently control its own focus-on-hover behavior, but happy to add a Root-level cascade if maintainers want it.

Adds a `focusOnItemEnter?: boolean` prop to `Menu.Content` (and through
inheritance, `DropdownMenu.Content`, `ContextMenu.Content`, `Menubar.Content`,
and `Menu.SubContent`).

Default `true` preserves the current behaviour where moving the pointer
over a menu item focuses it (matches native menu UX).

Set to `false` when the menu contains focusable siblings (e.g. an embedded
search <input>) that the user may want to keep typing in without their
caret being stolen by every row the pointer passes over.

Submenu hover-open is unaffected — `MenuSubTrigger` opens submenus via a
setTimeout on `onPointerGraceIntentChange`, not via `item.focus()`.

Fixes radix-ui#2193

Includes:
- 5 new vitest cases in dropdown-menu.test.tsx (default regression guard,
  focus retention on multiple pointermoves, keyboard-open regression guard,
  submenu-hover-open regression guard under focusOnItemEnter={false})
- `WithSearchInput` Storybook story showing side-by-side default vs opt-out
- Changeset entry (minor bump for menu + dropdown-menu + context-menu + menubar)
@changeset-bot

changeset-bot Bot commented Jun 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ae17938

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@radix-ui/react-menu Minor
@radix-ui/react-dropdown-menu Minor
@radix-ui/react-context-menu Minor
@radix-ui/react-menubar Minor
radix-ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Resolve conflict in dropdown-menu.test.tsx: keep our focusOnItemEnter
tests alongside upstream's new ref stability tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant