Skip to content

[Task](ui): fix keyboard navigation in Select so first arrow key press focuses first menu item #1781

Description

@franzheidl

Follow-up to #1748 / #1774.

When opening a Select via keyboard and navigating with arrow keys, several keystrokes are consumed before the first menu item appears focused. The menu items are there and selectable, but the focus ring does not appear until after a number of arrow presses.

Summary

The Select menu's dropdown options were always present in the DOM (just hidden via display: none), which caused Headless UI to sort them by DOM position while they were invisible. Since hidden elements don't participate in layout, the sort produced unreliable results — so the first several arrow-down presses couldn't locate the correct option to highlight. The fix is to only render the dropdown into the DOM when the menu is actually open, so Headless UI always sorts against visible, laid-out elements.


Root Cause

The original issue description (as per below) pointed at tabIndex on the Floating UI container as the culprit — that diagnosis turned out to be misleading. The real cause is deeper in how Headless UI's internal state machine interacts with the portal.

What was actually happening

The ListboxOptions (and all SelectOption children) were always mounted in the DOM, hidden only via a display: none style on the wrapping Floating UI container div. This means on page load, before the menu is ever opened, HUI's option registration cycle runs:

  1. Each ListboxOption registers itself with the machine via a batched microtask (RegisterOptions)
  2. The machine schedules a SortOptions action via requestAnimationFrame to sort the registered options by their DOM position (sortByDomNode / compareDocumentPosition)
  3. The sort runs against elements that are inside a display: none subtree — compareDocumentPosition can return 0 for all comparisons in this state, making the sort a no-op or producing an undefined order

When the menu is then opened and the user presses arrow-down, goToOption({ focus: Next }) fires. Because activeOptionIndex is null, HUI calls g(state) which re-runs sortByDomNode — again against elements whose DOM context was established while hidden. calculateActiveIndex gets a list it can't reliably order, so the resulting index is wrong or null. The first few arrow-down presses keep returning null or an incorrect index, with no data-active applied — until enough state has settled that the sort resolves correctly.

This explains why pressing Enter after a single arrow-down selected the correct item (HUI's logical state eventually resolved) but the visual data-active attribute didn't appear until press 4: the machine state was updating correctly but isActive was returning false for the wrong option index.

The Fix

Conditionally render the portal only when the menu is open ({open && createPortal(...)}) and remove the display toggling style. This means options only mount, register, and sort when the menu is actually open and the container is visible in the DOM — so compareDocumentPosition has a real layout to work with, and goToOption always finds a correctly-sorted option list.

The display: none / always-mounted approach was likely chosen to avoid the portal unmounting/remounting on every open/close, but it inadvertently broke HUI's DOM-order-dependent focus management.

Side note on the tabIndex change

The tabIndex={-1} on the floating container is still correct and worth keeping. Without it, getFloatingProps() leaves the container participating in sequential focus order, which can cause its own navigation issues — it just wasn't the cause of the 4-press lag.

Initial Issue Analysis and Fix (incorrect):

Root Cause

The Select menu is rendered inside a portal using Floating UI. The floating container <div> receives getFloatingProps() from @floating-ui/react's useInteractions, which spreads interaction props — including a tabIndex — onto the div. This places the container and/or the ListboxOptions element into the browser's sequential focus order. Arrow key presses are consumed navigating through these invisible focusable elements before Headless UI's virtual list navigation kicks in for the items.

### Fix
Add tabIndex={-1} explicitly to the floating container <div> in Select.component.tsx so it can still receive programmatic focus (required by Floating UI) but is excluded from sequential keyboard navigation.

Acceptance criteria

  • Opening a Select via keyboard and pressing arrow down immediately highlights the first item
  • Opening a Select via mouse and pressing arrow down immediately highlights the first item
  • Existing keyboard open/close/select behaviour does not regress

Metadata

Metadata

Assignees

Labels

ui-componentsAll tasks related to juno-ui-components library

Type

Fields

No fields configured for Task.

Projects

Status
In progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions