Description
Feature request
Summary
With loopFocus (the default for Menu, Context Menu, Combobox and Autocomplete), an auto-repeated arrow key (event.repeat) should stop at the first or last item. Looping to the other end, or escaping the list with allowEscape, should take a new press. A single press, and a held key away from the ends, would behave as today.
The change is small and lives in the shared useListNavigation hook (packages/react/src/utils/popups/interactions/useListNavigation.ts): for a repeat while the highlight is inside the list, navigate as if loopFocus were false.
- const { index, wrapped } = getNextListIndex(listRef.current, currentIndex, {
+ // A held key (auto-repeat) stops at the boundary; looping or escaping takes a new press.
+ const held = event.repeat && !isIndexOutOfListBounds(listRef.current, currentIndex);
+ const { index, wrapped } = getNextListIndex(listRef.current, currentIndex, {
decrement: !isMainOrientationToEndKey(event.key, orientation, rtl),
- loopFocus,
+ loopFocus: loopFocus && !held,
allowEscape,Because it is in the shared hook, Menu, Context Menu, Menubar submenus, Combobox and Autocomplete all get it. Select does not loop, so it is unaffected. Repeats count only while the highlight is inside the list, so a held key that opens the list, or enters it from outside, still lands where it does today.
Examples in other libraries
Native macOS menus never loop: the highlight stops at the first or last item, whether the key is pressed or held (example). This proposal keeps Base UI's loopFocus for a new press, and takes the macOS stop for a held key, where looping is never what the user wants.
Motivation
Holding ArrowDown to get to the bottom of a long menu runs the highlight past the last item and straight on to the first, over and over, as the OS repeats the key. To stop on the last item you have to release the key at the right moment. With allowEscape, a held key also runs out of the list into the input and back in. Stopping at the end on a held key makes "hold to go to the bottom" (or the top) reliable, while a deliberate new press still wraps.
Related: #5968, which ignores held Enter/Space repeats on newly focused elements.
Happy to send a PR with the change and a test in useListNavigation.test.tsx (prop: loopFocus).
Activity