Skip to content

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

Open
acusti wants to merge 4 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 4 commits into
mainfrom
dropdown-highlight-selected-value

Conversation

@acusti

@acusti acusti commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Stacked on #394 (base dropdown-value-label-pair).

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

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="option" for a searchable (listbox) dropdown, role="menuitem" for a menu — and always menuitem inside a submenu (itself a menu).
  • The <ul>/<ol> wrappers get role="presentation" so the listbox/menu owns its items directly rather than through an intervening list.
  • The selected option gets aria-selected.
  • A natively interactive item (button/link/input) or one with a consumer-set role keeps its own role.

Details

  • Both run in the body-mount (open) path, alongside the existing annotateParentItems (which already gives submenus role="menu").
  • 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

  • 5 new tests: reveal (active item on open; no-match activates nothing) and roles (listbox options + neutralized list + aria-selected; menu menuitems; consumer role preserved). Full suite: 247 passing.
  • Two minor changesets.
  • 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 on lines +7 to +12
A controlled dropdown now opens with the item whose `data-ukt-value`
matches `props.value` marked `aria-selected`, made the active item (so
keyboard navigation starts from it), and scrolled into view — so a long
list opens showing, and scrolled to, the current selection instead of the
top. The persistent selection tint is themeable via the new
`--uktdd-body-bg-color-selected` custom property.
Comment on lines +1241 to +1244
if (valueIdentity != null && dropdownElement) {
const selectedItem = getItemElements(dropdownElement)?.find(
(item) => item.dataset.uktValue === valueIdentity,
);
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