diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TreeView.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TreeView.razor
index 0d1be6fd..c0f4be8e 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TreeView.razor
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TreeView.razor
@@ -51,7 +51,15 @@ else
@oncontextmenu="(e) => OnContextMenu(e, item)" @oncontextmenu:preventDefault="@(ContextMenu != null)" @oncontextmenu:stopPropagation="@(ContextMenu != null)">
@if (isBranch)
{
- ToggleExpand(key)" @onclick:stopPropagation>
+ ToggleExpand(key)"
+ @onclick:stopPropagation
+ @onkeydown="(e) => OnToggleKey(e, key)"
+ @onkeydown:preventDefault>
}
else
{
@@ -276,6 +284,14 @@ else
PersistExpandedState();
}
+ private void OnToggleKey(KeyboardEventArgs e, object key)
+ {
+ if (e.Key == "Enter" || e.Key == " ")
+ {
+ ToggleExpand(key);
+ }
+ }
+
private void PersistExpandedState()
{
if (StorageKey != null)
diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/AccessibilityTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/AccessibilityTests.cs
new file mode 100644
index 00000000..69e814d0
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/AccessibilityTests.cs
@@ -0,0 +1,106 @@
+using Bunit;
+using Microsoft.AspNetCore.Components;
+using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
+
+///
+/// Accessibility regression tests.
+/// Locks the a11y attributes on TreeView chevron toggles and the
+/// ToastNotification live-region container against future regressions.
+///
+public class AccessibilityTests : BunitContext
+{
+ // ── TreeView harness (mirrors TreeViewTests setup) ─────────────────────────
+
+ private record TestNode(string Key, string Label, List Children);
+
+ private static List BranchRoots() => new()
+ {
+ new("root-1", "Root One", new()
+ {
+ new("child-1", "Child One", new()),
+ }),
+ new("leaf-1", "Leaf One", new()),
+ };
+
+ private IRenderedComponent> RenderTreeView(List? items = null)
+ {
+ return Render>(parameters =>
+ {
+ parameters
+ .Add(p => p.Items, items ?? BranchRoots())
+ .Add(p => p.ChildrenSelector, n => n.Children)
+ .Add(p => p.HasChildrenSelector, n => n.Children.Count > 0)
+ .Add(p => p.KeySelector, n => n.Key)
+ .Add(p => p.NodeContent, node => builder =>
+ {
+ builder.AddMarkupContent(0, $"{node.Label}");
+ });
+ });
+ }
+
+ // ── Test 1: TreeView chevron toggle a11y attributes ────────────────────────
+
+ [Fact]
+ public void TreeViewToggle_HasAccessibleNameAndRole()
+ {
+ // BranchRoots has one branch node ("root-1") and one leaf node ("leaf-1").
+ // The branch node's toggle span is the only .tv-toggle rendered.
+ var cut = RenderTreeView();
+
+ var toggle = cut.Find(".tv-toggle");
+
+ // role="button" makes it announced as a button by screen readers.
+ Assert.Equal("button", toggle.GetAttribute("role"));
+
+ // tabindex="0" puts it in the natural tab order.
+ Assert.Equal("0", toggle.GetAttribute("tabindex"));
+
+ // aria-label must be non-empty (e.g. "Expand root-1").
+ var label = toggle.GetAttribute("aria-label");
+ Assert.NotNull(label);
+ Assert.NotEmpty(label!);
+
+ // aria-expanded reflects the collapsed state ("false" when collapsed).
+ Assert.Equal("false", toggle.GetAttribute("aria-expanded"));
+ }
+
+ [Fact]
+ public void TreeViewToggle_AriaExpanded_ReflectsExpandedState()
+ {
+ var cut = RenderTreeView();
+
+ // Collapsed initially.
+ var toggle = cut.Find(".tv-toggle");
+ Assert.Equal("false", toggle.GetAttribute("aria-expanded"));
+
+ // Click to expand.
+ toggle.Click();
+
+ // aria-expanded must now be "true".
+ var expandedToggle = cut.Find(".tv-toggle");
+ Assert.Equal("true", expandedToggle.GetAttribute("aria-expanded"));
+
+ // aria-label should also flip to "Collapse …".
+ var labelAfterExpand = expandedToggle.GetAttribute("aria-label");
+ Assert.NotNull(labelAfterExpand);
+ Assert.StartsWith("Collapse", labelAfterExpand!);
+ }
+
+ // ── Test 2: ToastNotification live-region regression lock ─────────────────
+
+ [Fact]
+ public void ToastNotification_Container_IsAriaLivePolite()
+ {
+ // Render ToastNotification — it has no required services/cascading params.
+ var cut = Render();
+
+ // The outermost div is the live-region container.
+ // Find the element that carries aria-live (may be the root or a child div).
+ var liveRegion = cut.Find("[aria-live]");
+
+ Assert.Equal("polite", liveRegion.GetAttribute("aria-live"));
+ Assert.Equal("true", liveRegion.GetAttribute("aria-atomic"));
+ }
+}