Skip to content

[menu][combobox] A held arrow key should stop at the end of the list instead of looping #6202

Open

Description

@jonastreub

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions