Repro environment
Observed downstream in the Aspire Dashboard with Microsoft.FluentUI.AspNetCore.Components 4.14.1. I also checked v4.14.3 and current main; ColumnBase.razor still renders the same header trigger without disclosure state.
The grid uses:
<FluentDataGrid HeaderCellAsButtonWithMenu="true"
ResizableColumns="true"
ResizeType="DataGridResizeType.Discrete" />
Downstream issue: microsoft/aspire#17466
Steps to reproduce
- Render a
FluentDataGrid with HeaderCellAsButtonWithMenu="true" and at least two column actions.
- Inspect a generated column-header button in the accessibility tree.
- Open its menu.
- Inspect the same button again.
Expected behavior
The header trigger exposes aria-haspopup="menu" and aria-expanded="false" while closed, then aria-expanded="true" while its menu is open. A screen reader can announce the expanded/collapsed state.
Actual behavior
A Playwright browser repro against Aspire's Resources grid found one button named Name, but aria-expanded was absent both before and after opening the menu:
Before: - button "Name"
aria-expanded: <null>
After: - button "Name"
aria-expanded: <null>
visible menu items: 2
The host and shadow button remain unchanged while the menu is open:
<fluent-button class="col-sort-button stealth" type="button" aria-label="Name">...</fluent-button>
<button class="control" part="control" type="button" aria-label="Name">...</button>
Root cause
The default HeaderCellAsButtonWithMenu template in ColumnBase.razor owns both the generated FluentButton and private _isMenuOpen state. It binds _isMenuOpen to FluentMenu.Open, but does not expose that state on the trigger:
<FluentButton Id="@_columnId"
Class="col-sort-button"
@onclick="@HandleColumnHeaderClickedAsync"
aria-label="@tooltip">
...
</FluentButton>
<FluentMenu Anchor="@_columnId" @bind-Open="@_isMenuOpen">
...
</FluentMenu>
Downstream consumers cannot safely add the attribute because _isMenuOpen, _columnId, and the click/menu lifecycle are private. Replacing HeaderCellItemTemplate also replaces the complete trigger/menu implementation.
Suggested fix
Add aria-haspopup="menu" and aria-expanded="@_isMenuOpen" to the generated header FluentButton, with browser/component coverage for false → true → false.
Repro environment
Observed downstream in the Aspire Dashboard with
Microsoft.FluentUI.AspNetCore.Components4.14.1. I also checked v4.14.3 and currentmain;ColumnBase.razorstill renders the same header trigger without disclosure state.The grid uses:
Downstream issue: microsoft/aspire#17466
Steps to reproduce
FluentDataGridwithHeaderCellAsButtonWithMenu="true"and at least two column actions.Expected behavior
The header trigger exposes
aria-haspopup="menu"andaria-expanded="false"while closed, thenaria-expanded="true"while its menu is open. A screen reader can announce the expanded/collapsed state.Actual behavior
A Playwright browser repro against Aspire's Resources grid found one button named
Name, butaria-expandedwas absent both before and after opening the menu:The host and shadow button remain unchanged while the menu is open:
Root cause
The default
HeaderCellAsButtonWithMenutemplate inColumnBase.razorowns both the generatedFluentButtonand private_isMenuOpenstate. It binds_isMenuOpentoFluentMenu.Open, but does not expose that state on the trigger:Downstream consumers cannot safely add the attribute because
_isMenuOpen,_columnId, and the click/menu lifecycle are private. ReplacingHeaderCellItemTemplatealso replaces the complete trigger/menu implementation.Suggested fix
Add
aria-haspopup="menu"andaria-expanded="@_isMenuOpen"to the generated headerFluentButton, with browser/component coverage forfalse→true→false.