feat(menu): add focusOnItemEnter opt-out to Menu.Content#3979
Open
DavidMetcalfe wants to merge 2 commits into
Open
feat(menu): add focusOnItemEnter opt-out to Menu.Content#3979DavidMetcalfe wants to merge 2 commits into
DavidMetcalfe wants to merge 2 commits into
Conversation
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 detectedLatest commit: ae17938 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
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 |
This was referenced Jun 28, 2026
Resolve conflict in dropdown-menu.test.tsx: keep our focusOnItemEnter tests alongside upstream's new ref stability tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
focusOnItemEnter?: booleanprop toMenu.Contentthat lets consumers opt out of the hover-to-focus behavior inMenuItemImpl. Defaulttrue— zero behavior change for existing users.Background
MenuItemImpl.onPointerMovecallsitem.focus({preventScroll: true})on every mouseover (seepackages/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:
aria-selected#1969" though Rovingaria-selected#1969 is aboutaria-selectedroving focus and is unrelated.onPointerMove={e => e.preventDefault()},onBlurrefocus, focus-timeout hacks). None shipped upstream.What this PR does
focusOnItemEnter?: booleantoMenuContentImplProps. Inherited automatically byDropdownMenuContent,ContextMenuContent,MenubarContent, andMenuSubContent.truepreserves the current behavior.falseskips theitem.focus()call inside theMenuItemImpl.onPointerMovehandler — but leavesonItemEnter(event)and the rest of the pointermove handler untouched, so submenu hover-open viaMenuSubTrigger(which uses asetTimeout(open, 100)ononPointerGraceIntentChange, not onitem.focus()) is unaffected.The name
focusOnItemEnterdescribes what the prop gates: the focus call that fires inside the item-pointerenter handler. TheonItemEnterevent dispatch itself is preserved.Visual tradeoff
When
focusOnItemEnter={false}, items no longer setdata-highlightedon hover (that attribute tracks focus state). Consumers who want a hover visual should style via:hoveror a separateonPointerEntermechanism. Documented in the JSDoc.Test plan
pnpm test— 378 tests pass (was 373), 5 new vitest cases indropdown-menu.test.tsx:focusOnItemEnteromitted → item still gets focus on hover)<input>MenuSubTriggerstill opens the submenu on hover when the parent hasfocusOnItemEnter={false}pnpm types:checkclean across the monorepopnpm lintcleanpnpm format:checkclean on changed filespnpm buildcleanStorybook
Adds a
WithSearchInputstory toapps/storybook/stories/dropdown-menu.stories.tsxshowing the default and opt-out side-by-side.Changeset
Adds a
.changeset/focus-on-item-enter.mdmarkingminorfor@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
focusOnItemEnterfollows existing Radix boolean-prop conventions (modal,loop,forceMount,trapFocus). I'm open to alternative names if maintainers prefer —disableHoverFocusorautoFocusItems={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 theMenuRootContentTypePropschain). Should it also cascade fromMenu.Root/DropdownMenu.Rootfor menu-wide convenience, given that submenus instantiate their ownMenuContentProvider? 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.