Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fca7515
Fix View options expanded state announcement
adamint Jun 1, 2026
13de70a
Update menu expanded state after item click
adamint Jun 2, 2026
b21d13d
Restore focus after Resources view option selection
adamint Jun 1, 2026
2b05b9d
Cover resources menu focus restoration wiring
adamint Jun 2, 2026
36b83f0
Fix Resources grid keyboard activation
adamint Jun 1, 2026
fb60ce3
Stop modified Enter from nested resource controls
adamint Jun 4, 2026
abb2fe2
Fix Resources tabs reflow
adamint Jun 1, 2026
707075a
Fix popup tab focus navigation
adamint Jun 1, 2026
3442bbf
Fix dashboard menu item tests
adamint Jun 2, 2026
1167f77
Fix dashboard menu callback tests
adamint Jun 2, 2026
14a350a
Fix Aspire popup tab focus handling
adamint Jun 2, 2026
a23b200
Add browser regression for resources menu focus
adamint Jun 2, 2026
9db5d6a
Avoid popup keyboard navigation for context menus
adamint Jun 4, 2026
6462508
Fix merged Aspire menu test JS setup
adamint Jun 4, 2026
85333e9
Address resources accessibility review feedback
adamint Jun 4, 2026
fd284a6
Simplify resources grid keyboard handling
adamint Jun 5, 2026
550ceeb
Drop redundant popup keyboard JSInterop setups in AspireMenu tests
adamint Jun 11, 2026
f7dc164
Address resources accessibility review feedback
adamint Jun 12, 2026
ff7dfeb
Harden dashboard resource focus tests
adamint Jun 18, 2026
6eeac0f
Update resource parameter test for current model
adamint Jun 18, 2026
eea5c80
Remove local popup focus workaround
Jun 20, 2026
077b500
Add resources keyboard regression coverage
Jun 20, 2026
546244e
Remove obsolete resources JS hook assertion
Jun 20, 2026
a46e901
Stabilize resources URL keyboard proof
Jun 20, 2026
e0a3d2f
Keep resources URL keyboard test hermetic
Jun 20, 2026
cc26e25
Fix ConsoleLogs select variable shadowing
Jun 20, 2026
1b661cb
Add horizontal resources tab browser coverage
Jun 20, 2026
e08d900
Complete graph context menu close callbacks
Jun 25, 2026
969cc52
Fix resources tab test label after rebase
adamint Jul 6, 2026
44214ed
Add Aspire area label triage skill
adamint Jul 6, 2026
445415d
fix settings resource popup enter not working
adamint Jul 9, 2026
f4701ab
Remove temporary FluentUI menu workaround
adamint Jul 9, 2026
9c8a74e
Add Resources view options expanded state test
adamint Jul 9, 2026
ca7bf9e
Remove generated XLF whitespace change
adamint Jul 9, 2026
649bfd2
Merge remote-tracking branch 'upstream/main' into adamint/a11y-resour…
adamint Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions src/Aspire.Dashboard/Components/Controls/AspireMenu.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components.Utilities;
using Microsoft.JSInterop;

namespace Aspire.Dashboard.Components;

Expand Down Expand Up @@ -33,6 +34,19 @@ public partial class AspireMenu : FluentComponentBase
[Parameter]
public required IReadOnlyList<MenuButtonItem> Items { get; set; }

/// <summary>
/// Gets or sets a value indicating whether focus should return to <see cref="Anchor"/> after a menu item is clicked.
/// </summary>
/// <remarks>
/// Use this only for button-anchored menus where <see cref="Anchor"/> identifies the element that opened the menu.
/// Do not enable it for cursor-positioned or context menus where <see cref="Anchor"/> is only used for positioning.
/// </remarks>
[Parameter]
public bool RestoreFocusOnItemClick { get; set; }

[Inject]
public required IJSRuntime JS { get; init; }

// Each menu item is approximately 32px tall, plus 16px padding for the menu container.
private const int EstimatedItemHeight = 32;
private const int MenuVerticalPadding = 16;
Expand Down Expand Up @@ -89,11 +103,7 @@ public async Task OpenAsync(int screenWidth, int screenHeight, int clientX, int
.AddStyle("min-width", "64px")
.Build();

Open = true;
if (OpenChanged.HasDelegate)
{
await OpenChanged.InvokeAsync(Open);
}
await SetOpenAsync(true);

StateHasChanged();
}
Expand All @@ -105,15 +115,26 @@ private async Task HandleItemClicked(MenuButtonItem item)
{
await onClick();
}
Open = false;
await SetOpenAsync(false);

if (RestoreFocusOnItemClick && !string.IsNullOrEmpty(Anchor))
{
await JS.InvokeVoidAsync("focusElement", Anchor);
}
}

private Task OnOpenChanged(bool open)
private async Task OnOpenChanged(bool open)
{
await SetOpenAsync(open);
}

private async Task SetOpenAsync(bool open)
{
Open = open;

return OpenChanged.HasDelegate
? OpenChanged.InvokeAsync(open)
: Task.CompletedTask;
if (OpenChanged.HasDelegate)
{
await OpenChanged.InvokeAsync(open);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
var additionalButtonAttributes = new Dictionary<string, object>(AdditionalAttributes ?? ImmutableDictionary<string, object>.Empty)
{
{ "aria-haspopup", "true" },
{ "aria-expanded", _visible },
{ "aria-expanded", _visible ? "true" : "false" },
{ "onkeydown", (KeyboardEventArgs args) => OnKeyDown(args) }
};
}
Expand All @@ -27,4 +27,4 @@
}
</FluentButton>

<AspireMenu Anchor="@MenuButtonId" @bind-Open="@_visible" Items="_items" />
<AspireMenu Anchor="@MenuButtonId" @bind-Open="@_visible" Items="_items" RestoreFocusOnItemClick="@RestoreFocusOnItemClick" />
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ public partial class AspireMenuButton : FluentComponentBase
[Parameter]
public bool HideIcon { get; set; }

/// <summary>
/// Gets or sets a value indicating whether focus should return to this menu button after a menu item is clicked.
/// </summary>
/// <remarks>
/// Use this for button-anchored menus because the underlying menu anchor is the element that opened the menu.
/// Do not use this behavior for cursor-positioned or context menus where the anchor is only used for positioning.
/// </remarks>
[Parameter]
public bool RestoreFocusOnItemClick { get; set; }

protected override void OnParametersSet()
{
_icon = Icon ?? s_defaultIcon;
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Dashboard/Components/Controls/GridValue.razor
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
}
else
{
<span class="grid-value" title="@(ToolTip ?? Value)" id="@_cellTextId">
<span class="grid-value" title="@(ToolTip ?? Value)" id="@_cellTextId" @onkeydown:stopPropagation="true">
@if (ContentBeforeValue == null && ContentAfterValue == null && string.IsNullOrEmpty(Value))
{
<span class="empty-data"></span>
Expand Down Expand Up @@ -46,7 +46,7 @@

@* Button area *@

<div @onclick:stopPropagation="true" class="button-container">
<div @onclick:stopPropagation="true" @onkeydown:stopPropagation="true" class="button-container">

<span class="defaultHidden">
<FluentButton Appearance="Appearance.Lightweight"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
class="value-not-set-link"
disabled="@IsSetCommandDisabled"
@onclick="OnClickAsync"
@onkeydown:stopPropagation="true"
title="@ControlsStringsLoc[nameof(ControlsStrings.ParameterSetValueAction)]">
@ControlsStringsLoc[nameof(ControlsStrings.ParameterValueNotSet)]
</button>
Expand Down
32 changes: 22 additions & 10 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<AspireMenuButton ButtonAppearance="Appearance.Stealth"
Icon="@(new Icons.Regular.Size20.Options())"
Items="@_resourcesMenuItems"
RestoreFocusOnItemClick="true"
Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesChangeViewOptions)]"
slot="end" />
}
Expand Down Expand Up @@ -100,22 +101,29 @@
*@
@if (!_hideResourceGraph)
{
<FluentTabs Class="resources-tab-header" ActiveTabId="@($"tab-{PageViewModel.SelectedViewKind}")" OnTabChange="@OnTabChangeAsync" Size="null">
<FluentTabs Class="resources-tab-header"
ActiveTabId="@($"tab-{PageViewModel.SelectedViewKind}")"
OnTabChange="@OnTabChangeAsync"
Orientation="@(ViewportInformation.IsUltraLowWidth ? Orientation.Vertical : Orientation.Horizontal)"
Size="null">
<FluentTab LabelClass="tab-label"
Id="@($"tab-{ResourceViewKind.Table}")"
Label="@ControlsStringsLoc[nameof(ControlsStrings.ResourcesContainerTableTab)]"
Icon="@(new Icons.Regular.Size24.Table())">
Icon="@(new Icons.Regular.Size24.Table())"
fixed>
</FluentTab>
<FluentTab LabelClass="tab-label"
Id="@($"tab-{ResourceViewKind.Parameters}")"
Label="@ControlsStringsLoc[nameof(ControlsStrings.ResourcesContainerParametersTab)]"
Icon="@(new Icons.Regular.Size24.Key())">
Icon="@(new Icons.Regular.Size24.Key())"
fixed>
</FluentTab>
<FluentTab LabelClass="tab-label"
Id="@($"tab-{ResourceViewKind.Graph}")"
aria-label="@ControlsStringsLoc[nameof(ControlsStrings.ResourcesContainerGraphAccessibleLabel)]"
Label="@ControlsStringsLoc[nameof(ControlsStrings.ResourcesContainerGraphTab)]"
Icon="@(new Icons.Regular.Size24.ShareAndroid())">
Icon="@(new Icons.Regular.Size24.ShareAndroid())"
fixed>
</FluentTab>
</FluentTabs>
}
Expand All @@ -142,12 +150,15 @@
OnRowClick="@(r => r.ExecuteOnDefault(d => ShowResourceDetailsAsync(d.Resource, focusElementId: ScrollContainerId)))"
Class="main-grid enable-row-click">
<ChildContent>
@* FluentDataGrid rows activate on Enter/NumpadEnter from Blazor keydown handlers.
Stop Blazor keydown propagation from nested controls so keyboard activation doesn't
also open resource details. Browser-native keyboard behavior still runs on the control. *@
<AspireTemplateColumn ColumnId="@NameColumn" ColumnManager="@_manager" Title="@ControlsStringsLoc[nameof(ControlsStrings.NameColumnHeader)]" Sortable="true" SortBy="@_nameSort" Tooltip="true" TooltipText="@(c => $"{c.Resource.ResourceType}: {GetResourceName(c.Resource)}")" Class="expand-col">
@{
var indent = context.Depth * 16;
}
<span class="resources-name-container" style="margin-left: @(indent)px;">
<span @onclick:stopPropagation="true" class="main-grid-expand-container @(context.IsCollapsed ? "main-grid-collapsed" : "main-grid-expanded")">
<span @onclick:stopPropagation="true" @onkeydown:stopPropagation="true" class="main-grid-expand-container @(context.IsCollapsed ? "main-grid-collapsed" : "main-grid-expanded")">
Comment thread
adamint marked this conversation as resolved.
@if (context.Children.Count > 0)
{
<FluentButton aria-label="@ControlsStringsLoc[nameof(ControlsStrings.ToggleNesting)]" Appearance="Appearance.Lightweight" Class="main-grid-expand-button" OnClick="@(() => OnToggleCollapse(context))">
Expand Down Expand Up @@ -184,11 +195,12 @@
<GridValue Value="@paramValue.Value"
ValueDescription="@ControlsStringsLoc[nameof(ControlsStrings.PropertyGridValueColumnHeader)]"
EnableMasking="@paramValue.IsSensitive"
IsMasked="@paramValue.IsSensitive" />
IsMasked="@paramValue.IsSensitive"
StopClickPropagation="true" />
}
else if (paramValue.IsUnresolved)
{
<span class="cellText" @onclick:stopPropagation="true">
<span class="cellText" @onclick:stopPropagation="true" @onkeydown:stopPropagation="true">
<ParameterValueDisplay Resource="context.Resource"
OnExecuteCommandAsync="ExecuteResourceCommandAsync"
IsCommandExecuting="@((resource, command) => DashboardCommandExecutor.IsExecuting(resource.Name, command.Name))" />
Expand All @@ -205,7 +217,7 @@
DisplayedUrls="GetDisplayedUrls(context.Resource)" />
</AspireTemplateColumn>
<AspireTemplateColumn ColumnId="@ActionsColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.Resources.ResourcesActionsColumnHeader)]" Class="no-ellipsis">
<div class="grid-action-container" @onclick:stopPropagation="true">
<div class="grid-action-container" @onclick:stopPropagation="true" @onkeydown:stopPropagation="true">
<ResourceActions CommandSelected="async (command) => await ExecuteResourceCommandAsync(context.Resource, command)"
IsCommandExecuting="@((resource, command) => DashboardCommandExecutor.IsExecuting(resource.Name, command.Name))"
OnViewDetails="@((focusElementId) => ShowResourceDetailsAsync(context.Resource, focusElementId))"
Expand Down Expand Up @@ -250,8 +262,8 @@
}
</div>
</div>
<FluentOverlay @bind-Visible="_contextMenuOpen" OnClose="ContextMenuClosed" Transparent="true" FullScreen="true" />
<AspireMenu @bind-Open="_contextMenuOpen" @ref="_contextMenu" Anchor="resources-summary-layout-id" Anchored="false" Items="_contextMenuItems" />
<FluentOverlay Visible="_contextMenuOpen" VisibleChanged="ContextMenuOpenChangedAsync" OnClose="ContextMenuClosedAsync" Transparent="true" FullScreen="true" />
<AspireMenu Open="_contextMenuOpen" OpenChanged="ContextMenuOpenChangedAsync" @ref="_contextMenu" Anchor="resources-summary-layout-id" Anchored="false" Items="_contextMenuItems" />
</Summary>
<Details>
<ResourceDetails Resource="context"
Expand Down
31 changes: 29 additions & 2 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,7 @@ public ResourcesPageState ConvertViewModelToSerializable()
public async ValueTask DisposeAsync()
{
_aiContext?.Dispose();
CompleteContextMenuClosed();

_resourcesInteropReference?.Dispose();
_cts.Cancel();
Expand All @@ -1015,13 +1016,39 @@ public async ValueTask DisposeAsync()
await TaskHelpers.WaitIgnoreCancelAsync(_resourceSubscriptionTask);
}

private async Task ContextMenuClosed(Microsoft.AspNetCore.Components.Web.MouseEventArgs args)
private async Task ContextMenuClosedAsync(Microsoft.AspNetCore.Components.Web.MouseEventArgs args)
{
await CloseContextMenuAsync(closeMenu: true);
}

private async Task ContextMenuOpenChangedAsync(bool open)
{
if (open)
{
_contextMenuOpen = true;
return;
}

await CloseContextMenuAsync(closeMenu: false);
}

private async Task CloseContextMenuAsync(bool closeMenu)
{
_contextMenuOpen = false;

if (_contextMenu is { } menu)
{
await menu.CloseAsync();
if (closeMenu)
{
await menu.CloseAsync();
}
}

CompleteContextMenuClosed();
}

private void CompleteContextMenuClosed()
{
_contextMenuClosedTcs?.TrySetResult();
_contextMenuClosedTcs = null;
}
Expand Down
12 changes: 12 additions & 0 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@
margin-left: calc(var(--design-unit) * 3px);
}

::deep .resources-tab-header {
max-width: 100%;
min-width: 0;
}

/* FluentTabs keeps its horizontal tab list at max-content width. Constrain the host so
focus can scroll the selected tab into view when space is limited above ultra-low width. */
::deep .resources-tab-header[orientation="horizontal"] {
overflow-x: auto;
overflow-y: hidden;
}

::deep .resources-grid-container {
overflow: auto;
grid-area: resources-tab-content;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
Href="@GetResourceErrorStructuredLogsUrl()"
Appearance="Appearance.Stealth"
Class="unread-logs-errors-link"
@onclick:stopPropagation="true">
@onclick:stopPropagation="true"
@onkeydown:stopPropagation="true">
<FluentBadge
Appearance="Appearance.Accent"
Circular="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ else if (DisplayedUrls.Count > 1)
var renderedUrls = DisplayedUrls.Take(maxRenderedUrls).ToList();
var preOverflowedCount = DisplayedUrls.Count - renderedUrls.Count;

<div class="url-container" @onclick:stopPropagation="true">
<div class="url-container" @onclick:stopPropagation="true" @onkeydown:stopPropagation="true">
<FluentOverflow Class="url-overflow">
<ChildContent>
@for (var i = 0; i < renderedUrls.Count; i++)
Expand All @@ -34,7 +34,7 @@ else if (DisplayedUrls.Count > 1)
}
</ChildContent>
<MoreButtonTemplate Context="overflow">
<div @onclick:stopPropagation="true" style="display:inherit;">
<div @onclick:stopPropagation="true" @onkeydown:stopPropagation="true" style="display:inherit;">
<FluentButton Appearance="Appearance.Accent" OnClick="() => _popoverVisible = !_popoverVisible" Class="url-button">
@($"+{overflow.ItemsOverflow.Count() + preOverflowedCount}")
</FluentButton>
Expand All @@ -44,7 +44,7 @@ else if (DisplayedUrls.Count > 1)
@{
var items = overflow.ItemsOverflow.Select(i => (DisplayedUrl)i.Data!).Concat(DisplayedUrls.Skip(maxRenderedUrls)).ToList();
}
<div @onclick:stopPropagation="true">
<div @onclick:stopPropagation="true" @onkeydown:stopPropagation="true">
@* Each item is approximately 36px tall, plus 48px for the header and container padding. The popover body has a CSS max-height of 300px. *@
<FluentPopover AnchorId="@overflow.IdMoreButton" @bind-Open="_popoverVisible" VerticalThreshold="@(Math.Min(items.Count * 36 + 48, 300))" AutoFocus="false">
<Header>
Expand Down Expand Up @@ -77,7 +77,7 @@ else if (DisplayedUrls.Count > 1)
{
if (displayedUrl.Url != null)
{
return @<a href="@displayedUrl.Url" target="_blank" title="@GetTooltipText(displayedUrl)" @onclick:stopPropagation="true">@displayedUrl.Text</a>;
return @<a href="@displayedUrl.Url" target="_blank" title="@GetTooltipText(displayedUrl)" @onclick:stopPropagation="true" @onkeydown:stopPropagation="true">@displayedUrl.Text</a>;
}
else
{
Expand Down
Loading
Loading