Skip to content

Dropdown: reveal the current value on open + proper listbox/menu item roles#396

Open
acusti wants to merge 6 commits into
mainfrom
dropdown-highlight-selected-value
Open

Dropdown: reveal the current value on open + proper listbox/menu item roles#396
acusti wants to merge 6 commits into
mainfrom
dropdown-highlight-selected-value

Conversation

@acusti

@acusti acusti commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

Two related improvements for controlled dropdowns.

Reveal the current value on open

On open, the item whose data-ukt-value matches props.value is made the active item (so keyboard navigation starts from the current selection) and scrolled into view — a long list opens at the current value instead of the top. In a listbox it's also marked aria-selected="true" (with a themeable --uktdd-body-bg-color-selected tint). The lookup is deliberately top-level only: search/typeahead also match per level, and no native menu opens with submenus pre-expanded, so a value matching only a submenu item is a graceful no-op.

Proper listbox / menu item roles

The dropdown set the container role (listbox/menu/dialog) but left its items as plain <li>s. On open it now fills in the roles the consumer hasn't set:

  • role=&#34;option&#34; for a searchable (listbox) dropdown, role=&#34;menuitem&#34; for a menu — and always menuitem inside a submenu (itself a menu).
  • The <ul>/<ol> wrappers get role=&#34;presentation&#34; so the listbox/menu owns its items directly rather than through an intervening list.
  • The selected option gets aria-selected, and with keepOpenOnSubmit it moves to the newly submitted option (scoped to the popup body).
  • A natively interactive item (button/link/input) or one with a consumer-set role keeps its own role; a submenu parent item in a listbox gets no role at all (its disclosure ARIA is invalid on an option).
  • Consumer-authored aria-selected anywhere in the body (true or false) means the consumer manages selection ARIA — the reveal marking and the keepOpenOnSubmit move both stand down, so a single-select listbox never shows two selected options.

Details

  • Both run in the body-mount (open) path, alongside the existing annotateParentItems (which already gives submenus role=&#34;menu&#34;). The per-open work is latched per body element (a WeakSet), since an inline ref callback re-attaches on every re-render while open and must not re-run showPopover() or read its own aria-selected annotation back as consumer-authored.
  • setActiveItem's event becomes optional — it's only used to build the onActiveItem payload, which is skipped without an event — so an item can be activated programmatically on open without a synthetic event or a spurious callback.
  • aria-selected is listbox-only (it isn't valid on a menuitem); menus get the visual active highlight but no aria-selected.

Testing

  • 9 new tests: reveal (active item on open; no-match activates nothing), roles (listbox options + neutralized list + aria-selected; menu menuitems; consumer role preserved; listbox submenu parent keeps no role), and selection ARIA (aria-selected moves on submit with keepOpenOnSubmit, scoped to the popup body; consumer-authored aria-selected wins). Full suite: 251 passing.
  • Two minor changesets.
  • README: the ARIA attributes section documents the item roles, reveal-on-open, and the selected tint.
  • OpensToCurrentValue Storybook story — a long list with the value far down, so opening scrolls straight to it.
  • build / test / tsc / lint / format all green.

Follow-up (not here)

aria-activedescendant on the searchable input (announcing the active option as you arrow through it) is the natural next a11y step for the combobox interaction — left out to keep this focused on roles + reveal.

🤖 Generated with Claude Code

@acusti acusti changed the title Dropdown: reveal the current value when the menu opens Dropdown: reveal the current value on open + proper listbox/menu item roles Jul 16, 2026
@acusti
acusti force-pushed the dropdown-highlight-selected-value branch from 32457be to c303417 Compare July 16, 2026 15:45
@acusti
acusti force-pushed the dropdown-value-label-pair branch from a59b8d3 to 60ef117 Compare July 16, 2026 17:46
@acusti
acusti force-pushed the dropdown-highlight-selected-value branch 2 times, most recently from 027bd7a to a58042b Compare July 16, 2026 18:13
@acusti
acusti changed the base branch from dropdown-value-label-pair to main July 16, 2026 22:35
acusti and others added 4 commits July 16, 2026 22:36
A controlled dropdown opened at the top of its list regardless of the current
value. On open, mark the item whose data-ukt-value matches props.value
aria-selected, make it the active item so keyboard navigation starts there,
and scroll it into view — so a long list opens showing, and scrolled to, the
current selection.

setActiveItem's event becomes optional (it only builds the onActiveItem
payload, which is skipped when there's no event) so an item can be activated
programmatically on open. Adds a themeable --uktdd-body-bg-color-selected tint
for the persistent selection marker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dropdown set the container role (listbox/menu/dialog) but left its items
as plain <li>s, so a searchable listbox exposed no options and a menu no
menuitems. On open, fill in the roles the consumer hasn't set: option for a
listbox, menuitem for a menu (and for any submenu, itself a menu), with the
<ul>/<ol> wrappers marked role="presentation" so the container owns its items
directly rather than through an intervening list.

Skip items that carry their own semantics — a natively interactive item
(button, link, input) or one with a consumer-set role keeps it. aria-selected
on the current option is already set by the reveal-on-open behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The open-time annotation marks the item matching props.value, but with
keepOpenOnSubmit the body outlives the submit, so selecting another item
left the tint (and aria-selected) on the previous selection until the
next open. Move it to the submitted option in handleSubmitItem; a closing
body unmounts, so reopening re-derives it from props.value as before.

Also document that the open-time annotations (roles, aria-selected,
reveal) run once per open, so items rendered into an already-open body
aren't annotated until the next open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcJxNoQKN6Yx5hmYpwJHMJ
The ARIA attributes section covered the trigger, body, submenu, and
menubar annotations but not the item-level ones added alongside them:
option/menuitem role fill-in, the presentation role on list wrappers,
aria-selected on the current listbox option, and the reveal-on-open
behavior with its --uktdd-body-bg-color-selected custom property.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcJxNoQKN6Yx5hmYpwJHMJ
@acusti
acusti force-pushed the dropdown-highlight-selected-value branch from a58042b to c48606a Compare July 16, 2026 22:37
@acusti
acusti requested a review from Copilot July 16, 2026 22:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread .changeset/dropdown-reveal-value-on-open.md
Comment thread packages/dropdown/src/Dropdown.tsx
The reveal changeset claimed the matching item is marked aria-selected,
but that only happens in a listbox (aria-selected isn't valid on a
menuitem) — say so, matching the README and TSDoc. Also state at the
reveal site that the top-level-only lookup is deliberate:
search/typeahead also match per level, and no native menu opens with
submenus pre-expanded, so a value matching only a submenu item is a
graceful no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcJxNoQKN6Yx5hmYpwJHMJ

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment thread packages/dropdown/src/helpers.ts
Comment thread packages/dropdown/src/Dropdown.tsx
Comment thread packages/dropdown/src/Dropdown.tsx
Three fixes to the open-time annotations, all following the existing
your-values-win rule:

- A submenu parent item in a listbox no longer gets role="option": its
  aria-haspopup/aria-expanded (from annotateParentItems) are invalid on
  an option, and no valid role exists for a disclosure parent inside a
  listbox, so its role is left to the consumer.
- Consumer-authored aria-selected anywhere in the body means the consumer
  manages selection ARIA: the reveal-on-open marking and the
  keepOpenOnSubmit move both stand down, so a single-select listbox never
  shows two selected options.
- The keepOpenOnSubmit aria-selected move is scoped to the popup body, so
  it can't strip aria-selected from consumer content outside it (e.g. a
  custom trigger).

The body ref callback now latches per body element (a WeakSet): an inline
ref re-attaches on every re-render while open, and the per-open work must
run exactly once — showPopover() throws on an already-shown popover, and
the ownership check would otherwise read the reveal's own aria-selected
annotation back as consumer-authored on the second run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcJxNoQKN6Yx5hmYpwJHMJ
@acusti
acusti force-pushed the dropdown-highlight-selected-value branch from a7b1711 to 6238202 Compare July 17, 2026 06:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants