[Menu] Add focusItem('first' | 'last' | 'none') imperative action#4815
[Menu] Add focusItem('first' | 'last' | 'none') imperative action#4815chsmc-ant wants to merge 2 commits into
Conversation
Bundle size
PerformanceTotal duration: 1,143.38 ms +51.22 ms(+4.7%) | Renders: 50 (+0) | Paint: 1,762.60 ms +98.82 ms(+5.9%) No significant changes — details 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: |
Addresses review feedback: - Rename to focusItem and use directional targets instead of a raw index, so disabled/hidden items are skipped via getMinListIndex/getMaxListIndex. - Add an optional focusItem parameter to MenuHandle.open() with the same values.
|
Thanks for the feedback @atomiks @michaldudak — pushed 974094c which:
PR description updated to match. Happy to bikeshed |
When a menu is opened programmatically (via controlled
open, orMenuHandle.open(), oronOpenChange(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.This PR adds an imperative
focusItem(target: 'first' | 'last' | 'none')action so callers can ask the menu to highlight an item once it opens:Menu.RootactionsRef— alongsideunmount/close:MenuHandle.open(triggerId, focusItem?)— same values as a second parameter:The requested target is stored as
pendingFocusItemonMenuStoreand resolved inMenuRootonceitemDomElementsis populated, usinggetMinListIndex/getMaxListIndexso hidden items are skipped — the same logicuseListNavigationuses for its own initial sync.useListNavigation's existing layout effect then moves DOM focus to the resolved item.A
Menu.Root.FocusItemtype alias is exported for the'first' | 'last' | 'none'union.