[menu] Expose setActiveIndex on Menu.Root actionsRef#4815
Conversation
Bundle size
PerformanceTotal duration: 1,128.30 ms +36.15 ms(+3.3%) | Renders: 50 (+0) | Paint: 1,727.56 ms +63.78 ms(+3.8%) No significant changes. Check out the code infra dashboard for more information about this PR. |
✅ Deploy Preview for base-ui ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
To help the team a bit (and your team on Claude), here is a reproduction that seems to reproduce the problem: https://stackblitz.com/edit/va1fcbzq?file=src%2FApp.tsx (repro based on this demo): Screen.Recording.2026-05-13.at.00.37.47.movIt's not clear that this should be solved with a new API; this feels like a bug: I would expect the same behavior regardless of how the menu is open: with a trigger, with the For example, in https://mui.com/material-ui/react-menu/, the focus is placed on the first menu item when the menu opens (with the keyboard) but the behavior can be configured with a prop. Let's wait for team inputs. |
60e54e8 to
e1cad93
Compare
e1cad93 to
60e54e8
Compare
|
Thanks @oliviertassinari and good call! I opened #4816 as well so we can compare this approach to that one. Will defer to the team on which they'd prefer. |
commit: |
|
Thanks for the PR @chsmc-ant Makes sense as controlled-opens are assumed to always be pointer-driven, but a keyboard-driven controlled open is also valid in which case the highlight should be set on the first item. An imperative action also makes more sense than a prop like The only issue is naming: I'm also not sure if exposing the index is a good idea, because 1) disabled items should be skipped on initial open, which should pass through the internal logic to skip them, and 2) it's likely hard to keep track of which index is the last item if the controlled open is an Possible alternative: cc: @colmtuite Side note: in case you need this behavior before release, this technically already works, if hacky: firstItem.focus();
// or firstItem.dispatchEvent(new MouseEvent('mousemove', { bubbles: true })) |
|
We have another imperative API that has the same issue: |
When a menu is opened programmatically (via controlled
openor by callingonOpenChange(true)from a custom interaction that isn't one ofMenu.Trigger's built-in open keys),useListNavigationleavesactiveIndexasnull. This is correct for pointer-driven opens, but for custom keyboard shortcuts it means every item renders withtabindex="-1"andFloatingFocusManagermoves focus to the popup container rather than the first item — the user then has to press ↓ once more before arrow navigation works.The underlying
useListNavigationhook supports this viafocusItemOnOpen, but the option only takes effect when Floating UI's own trigger handler sets the internalkeyRef— which doesn't happen for external/custom keydowns.This PR exposes the existing
setActiveIndexstate setter onMenu.Root'sactionsRef(alongsideunmountandclose), so callers can do:Because the two state updates batch into one render, the popup mounts with
activeIndexalready set anduseListNavigation's existing layout effect focuses the item — norequestAnimationFrameor manual DOM focus needed on the caller's side.setActiveIndexis already part of the internalMenuRootContext, so this only widens the imperative handle; no new state is introduced.If maintainers would prefer a declarative alternative, plumbing
useListNavigation'sfocusItemOnOpenoption through as aMenu.Rootprop is an option I'm happy to explore — it's a larger surface change and can't target an arbitrary index, but it avoids exposing the raw setter.