Skip to content

fix(Combobox, Popover): point trigger aria-controls at the popup that actually opens - #594

Merged
mathewtaylor merged 4 commits into
v4.2.0from
foreman/ravi/ml-123
Oct 5, 2026
Merged

mathewtaylor merged 4 commits into
v4.2.0from
foreman/ravi/ml-123

Conversation

@mathewtaylor

@mathewtaylor mathewtaylor commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What was wrong

BbCombobox asked for aria-controls="{Id}-listbox" on its trigger, and nothing renders that id: the open list is a BbCommandList, whose id comes from the command's context (command-{guid}-list).

The rendered trigger never carried that value anyway. The popover trigger primitive writes its own aria-haspopup="true" and aria-controls="{popover content id}" after the caller's attributes, and the last attribute wins. So Combobox, MultiSelect, TreeSelect and Cascader all rendered aria-haspopup="true" (which means a menu) and named the popover panel, whatever they asked for.

What changes

  • Popover trigger: a caller's aria-haspopup and aria-controls now win over the defaults. With neither given, the output is the same as before.
  • Combobox: cascades its id to the inner BbCommand through an internal named cascading value, so the list renders {Id}-list and the trigger names it through CommandContext.GetListId(Id). The search input's aria-controls names the same list. CommandContext.Id gains an internal setter, applied once in OnInitialized, before any child renders.
  • Cascader: drops its aria-controls. It named an unlabelled inner div, and the popover itself is the dialog its aria-haspopup="dialog" describes, which is what the trigger already names by default.

What each trigger renders now:

Component aria-haspopup aria-controls names
Combobox listbox (was true) the command list, role="listbox" (was the popover)
MultiSelect listbox (was true) its {Id}-listbox, role="listbox" (was the popover)
TreeSelect tree (was true) the tree, role="tree" (was the popover)
Cascader dialog (was true) the popover, role="dialog" (unchanged)
Plain popover true the popover (unchanged)

No public API changes. The API snapshot gains one line for BbCommand's new non-public HostId cascading parameter.

Tests

PopupTriggerAriaTests (bUnit, with a BbPortalHost) opens each of the five triggers and checks that aria-haspopup is right and that exactly one element has the id aria-controls names, with the expected role. For Combobox it also checks that the list holds the options, that the search input names the same list, and that two comboboxes name different lists.

Before the fix, the four component tests fail on aria-haspopup="true". With only the popover trigger fixed, the Combobox test times out waiting for the {Id}-listbox element. Full .NET suite: 1111 passed, 0 failed. The state-changes browser checks (Chrome): 13 passed.

…utes and aria-invalid (#574)

Add TriggerId, TriggerAttributes and IsInvalid to BbCombobox and BbMultiSelect. They land on the trigger button, so a <label for> can point at it, test ids reach the control, and the invalid state is announced. AdditionalAttributes stay on the root div. IsInvalid falls back to the EditContext when unset, and both triggers now show the destructive border like other inputs.

BbPopoverTrigger now folds an id it is given into PopoverContext.TriggerId and renders it once, so focus restore and click-outside use the element actually rendered. This also fixes focus restore for BbTreeSelect and BbCascader, which already passed an id.
… actually opens

The popover trigger rendered its own aria-haspopup="true" and aria-controls after the caller's attributes, so Combobox, MultiSelect, TreeSelect and Cascader never got the values they asked for. A caller's aria-haspopup and aria-controls now win.

Combobox asked for {Id}-listbox, which nothing rendered: its list takes the inner command's id. The combobox now cascades its id to the command, so the list renders the id the trigger names. Cascader drops its aria-controls so it keeps naming the dialog.
# Conflicts:
#	tests/browser/fixtures/StateChanges/Smoke.razor
#	tests/browser/state-changes.checks.mjs
# Conflicts:
#	src/BlazorBlueprint.Components/Components/Combobox/BbCombobox.razor.cs
#	src/BlazorBlueprint.Components/Components/MultiSelect/BbMultiSelect.razor.cs
#	src/BlazorBlueprint.Primitives/Primitives/Popover/BbPopoverTrigger.razor
#	tests/browser/fixtures/StateChanges/Smoke.razor
#	tests/browser/state-changes.checks.mjs
@mathewtaylor
mathewtaylor merged commit f472923 into v4.2.0 Oct 5, 2026
1 check passed
@mathewtaylor
mathewtaylor deleted the foreman/ravi/ml-123 branch October 5, 2026 06:41
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.

1 participant