fdfd5e1b27
TreeView<TItem> handled only Enter/Space on the chevron; the tree itself was unreachable by keyboard. Implements the WAI-ARIA tree pattern: - Roving tabindex on li[role=treeitem] — exactly one node is tabbable, so the whole tree is a single Tab stop. Target resolved per render: last-focused node while still visible -> SelectedKey -> first visible node. Browser focus follows via a per-node ElementReference + FocusAsync in OnAfterRenderAsync, guarded by the same JSException/JSDisconnectedException/InvalidOperationException triple the context-menu focus already used (a no-op under bUnit). - ArrowDown/ArrowUp move between VISIBLE nodes; ArrowRight expands a collapsed branch else moves to first child; ArrowLeft collapses an expanded branch else moves to parent; Home/End jump to first/last; Enter/Space activate through the SAME path a click takes (OnContentClick / OnCheckboxToggle) so selection semantics never diverge between input modes. - ARIA: aria-level, aria-posinset, aria-setsize added; aria-selected now renders true/false on selectable+checkbox trees and stays absent on non-selectable ones. - Event scoping: @onkeydown:stopPropagation on the li, chevron, checkbox and content slot, so nested nodes do not double-handle and consumer controls inside NodeContent keep their own key handling. Browser scroll-on-Space/Arrow is suppressed by a minimal NATIVE inline onkeydown on the root ul, targeted at treeitems only — Blazor's preventDefault directive is all-or-nothing per element, so on the li it would trap Tab and cancel Enter/Space on consumer buttons. No CSP is configured, so the inline handler runs. BuildVisibleNodes() mirrors RenderNode's visibility rules and must stay in step. Tests: 31 new bUnit tests in TreeViewKeyboardNavigationTests; the 47 existing TreeView tests are unchanged and still green. Docs: new keyboard/a11y section in docs/components/TreeView.md.
543 lines
19 KiB
C#
543 lines
19 KiB
C#
using Bunit;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.Components.Web;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
|
|
|
|
/// <summary>
|
|
/// bUnit tests for <c>TreeView</c>'s WAI-ARIA tree keyboard navigation
|
|
/// (https://www.w3.org/WAI/ARIA/apg/patterns/treeview/). Covers the roving
|
|
/// tabindex (exactly one tabbable treeitem), Arrow/Home/End movement over the
|
|
/// flattened <em>visible</em> node order, ArrowRight/ArrowLeft expand-collapse
|
|
/// semantics, Enter/Space activation in both selection modes, and the
|
|
/// <c>aria-level</c> / <c>aria-posinset</c> / <c>aria-setsize</c> structure.
|
|
///
|
|
/// bUnit moves no real browser focus, so "focus" is asserted through the rendered
|
|
/// <c>tabindex</c> attribute (the roving-tabindex target) and through the
|
|
/// selection callbacks, not through a focused element.
|
|
/// </summary>
|
|
public class TreeViewKeyboardNavigationTests : BunitContext
|
|
{
|
|
private record TestNode(string Key, string Label, List<TestNode> Children);
|
|
|
|
// Alpha (branch)
|
|
// ├── Alpha-1 (leaf)
|
|
// └── Alpha-2 (branch)
|
|
// └── Alpha-2-X (leaf)
|
|
// Beta (leaf)
|
|
private static List<TestNode> SimpleRoots() => new()
|
|
{
|
|
new("a", "Alpha", new()
|
|
{
|
|
new("a1", "Alpha-1", new()),
|
|
new("a2", "Alpha-2", new()
|
|
{
|
|
new("a2x", "Alpha-2-X", new())
|
|
})
|
|
}),
|
|
new("b", "Beta", new()),
|
|
};
|
|
|
|
private IRenderedComponent<TreeView<TestNode>> RenderTreeView(
|
|
List<TestNode>? items = null,
|
|
Func<TestNode, bool>? initiallyExpanded = null,
|
|
bool selectable = false,
|
|
object? selectedKey = null,
|
|
Action<object?>? onSelectedKeyChanged = null,
|
|
TreeViewSelectionMode selectionMode = TreeViewSelectionMode.Single,
|
|
HashSet<object>? selectedKeys = null,
|
|
Action<HashSet<object>>? onSelectedKeysChanged = null)
|
|
{
|
|
if (selectionMode == TreeViewSelectionMode.Checkbox)
|
|
{
|
|
// Checkbox mode pushes the tri-state via JS interop after every render;
|
|
// stub it so bUnit's strict mode doesn't blow up on the unmocked call.
|
|
JSInterop.SetupVoid("treeviewStorage.setIndeterminate", _ => true);
|
|
}
|
|
|
|
return Render<TreeView<TestNode>>(parameters =>
|
|
{
|
|
parameters
|
|
.Add(p => p.Items, items ?? SimpleRoots())
|
|
.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, $"<span class=\"node-label\">{node.Label}</span>");
|
|
})
|
|
.Add(p => p.InitiallyExpanded, initiallyExpanded)
|
|
.Add(p => p.Selectable, selectable)
|
|
.Add(p => p.SelectedKey, selectedKey)
|
|
.Add(p => p.SelectionMode, selectionMode)
|
|
.Add(p => p.SelectedKeys, selectedKeys);
|
|
|
|
if (onSelectedKeyChanged != null)
|
|
{
|
|
parameters.Add(p => p.SelectedKeyChanged, onSelectedKeyChanged);
|
|
}
|
|
|
|
if (onSelectedKeysChanged != null)
|
|
{
|
|
parameters.Add(p => p.SelectedKeysChanged, onSelectedKeysChanged);
|
|
}
|
|
});
|
|
}
|
|
|
|
// `li[role=treeitem]` elements come back in document order, which for a nested
|
|
// <ul>/<li> tree is exactly the flattened visible order the arrow keys walk.
|
|
// So an index into this list is an index into the visible node sequence.
|
|
private static IReadOnlyList<AngleSharp.Dom.IElement> TreeItems(
|
|
IRenderedComponent<TreeView<TestNode>> cut)
|
|
=> cut.FindAll("li[role='treeitem']").ToList();
|
|
|
|
/// <summary>Index of the single tabbable node; also asserts that exactly one exists.</summary>
|
|
private static int TabbableIndex(IRenderedComponent<TreeView<TestNode>> cut)
|
|
{
|
|
var items = TreeItems(cut);
|
|
var found = -1;
|
|
for (var i = 0; i < items.Count; i++)
|
|
{
|
|
var tabindex = items[i].GetAttribute("tabindex");
|
|
if (tabindex == "0")
|
|
{
|
|
Assert.Equal(-1, found);
|
|
found = i;
|
|
}
|
|
else
|
|
{
|
|
// Every other node must be explicitly removed from the tab order.
|
|
Assert.Equal("-1", tabindex);
|
|
}
|
|
}
|
|
|
|
Assert.NotEqual(-1, found);
|
|
return found;
|
|
}
|
|
|
|
private static void PressKey(
|
|
IRenderedComponent<TreeView<TestNode>> cut, int nodeIndex, string key)
|
|
=> TreeItems(cut)[nodeIndex].KeyDown(new KeyboardEventArgs { Key = key });
|
|
|
|
// ── Roving tabindex ────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void InitialRender_ExactlyOneNodeIsTabbable_AndItIsTheFirst()
|
|
{
|
|
var cut = RenderTreeView();
|
|
|
|
// Two collapsed roots; Alpha (index 0) owns the tab stop.
|
|
Assert.Equal(2, TreeItems(cut).Count);
|
|
Assert.Equal(0, TabbableIndex(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void InitialRender_SelectedNode_OwnsTheTabStop()
|
|
{
|
|
// With a selection present the tab stop defaults to it, not to the first node.
|
|
var cut = RenderTreeView(selectable: true, selectedKey: "b");
|
|
|
|
Assert.Equal(1, TabbableIndex(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void TabStop_FallsBackToFirstNode_WhenFocusedNodeIsCollapsedAway()
|
|
{
|
|
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");
|
|
|
|
// Visible: Alpha, Alpha-1, Alpha-2, Beta. Move onto Alpha-1.
|
|
PressKey(cut, 0, "ArrowDown");
|
|
Assert.Equal(1, TabbableIndex(cut));
|
|
|
|
// Collapsing Alpha removes Alpha-1 from the visible set; the tab stop must
|
|
// not vanish with it.
|
|
cut.InvokeAsync(() => cut.Instance.CollapseAll());
|
|
Assert.Equal(0, TabbableIndex(cut));
|
|
}
|
|
|
|
// ── ArrowDown / ArrowUp ────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ArrowDown_MovesToNextVisibleNode()
|
|
{
|
|
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");
|
|
|
|
// Visible order: Alpha(0), Alpha-1(1), Alpha-2(2), Beta(3).
|
|
Assert.Equal(4, TreeItems(cut).Count);
|
|
|
|
PressKey(cut, 0, "ArrowDown");
|
|
Assert.Equal(1, TabbableIndex(cut));
|
|
|
|
PressKey(cut, 1, "ArrowDown");
|
|
Assert.Equal(2, TabbableIndex(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void ArrowDown_SkipsChildrenOfCollapsedBranch()
|
|
{
|
|
var cut = RenderTreeView();
|
|
|
|
// Alpha is collapsed, so the next visible node is Beta — not Alpha-1.
|
|
PressKey(cut, 0, "ArrowDown");
|
|
|
|
Assert.Equal(2, TreeItems(cut).Count);
|
|
Assert.Equal(1, TabbableIndex(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void ArrowDown_AtLastVisibleNode_IsNoOp()
|
|
{
|
|
var cut = RenderTreeView();
|
|
|
|
// Walk onto Beta (the last visible node) first, then try to go past it.
|
|
PressKey(cut, 0, "ArrowDown");
|
|
PressKey(cut, 1, "ArrowDown");
|
|
|
|
Assert.Equal(1, TabbableIndex(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void ArrowUp_MovesToPreviousVisibleNode()
|
|
{
|
|
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");
|
|
|
|
PressKey(cut, 3, "ArrowUp");
|
|
Assert.Equal(2, TabbableIndex(cut));
|
|
|
|
PressKey(cut, 2, "ArrowUp");
|
|
Assert.Equal(1, TabbableIndex(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void ArrowUp_AtFirstVisibleNode_IsNoOp()
|
|
{
|
|
var cut = RenderTreeView();
|
|
|
|
PressKey(cut, 0, "ArrowUp");
|
|
|
|
Assert.Equal(0, TabbableIndex(cut));
|
|
}
|
|
|
|
// ── ArrowRight ─────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ArrowRight_OnCollapsedBranch_Expands_WithoutMovingFocus()
|
|
{
|
|
var cut = RenderTreeView();
|
|
|
|
PressKey(cut, 0, "ArrowRight");
|
|
|
|
var items = TreeItems(cut);
|
|
Assert.Equal("true", items[0].GetAttribute("aria-expanded"));
|
|
Assert.Equal(4, items.Count); // Alpha, Alpha-1, Alpha-2, Beta
|
|
Assert.Equal(0, TabbableIndex(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void ArrowRight_OnExpandedBranch_MovesToFirstChild()
|
|
{
|
|
var cut = RenderTreeView();
|
|
|
|
PressKey(cut, 0, "ArrowRight"); // expand
|
|
PressKey(cut, 0, "ArrowRight"); // move onto Alpha-1
|
|
|
|
Assert.Equal(1, TabbableIndex(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void ArrowRight_OnLeaf_IsNoOp()
|
|
{
|
|
var cut = RenderTreeView();
|
|
|
|
// Beta is a leaf: no expansion, no movement, no new nodes.
|
|
PressKey(cut, 0, "ArrowDown");
|
|
PressKey(cut, 1, "ArrowRight");
|
|
|
|
var items = TreeItems(cut);
|
|
Assert.Equal(2, items.Count);
|
|
Assert.Null(items[1].GetAttribute("aria-expanded"));
|
|
Assert.Equal(1, TabbableIndex(cut));
|
|
}
|
|
|
|
// ── ArrowLeft ──────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ArrowLeft_OnExpandedBranch_Collapses_WithoutMovingFocus()
|
|
{
|
|
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");
|
|
|
|
PressKey(cut, 0, "ArrowLeft");
|
|
|
|
var items = TreeItems(cut);
|
|
Assert.Equal("false", items[0].GetAttribute("aria-expanded"));
|
|
Assert.Equal(2, items.Count);
|
|
Assert.Equal(0, TabbableIndex(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void ArrowLeft_OnChildNode_MovesToParent()
|
|
{
|
|
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");
|
|
|
|
// Alpha-1 (index 1) is a collapsed leaf → ArrowLeft goes up to Alpha.
|
|
PressKey(cut, 1, "ArrowLeft");
|
|
|
|
Assert.Equal(0, TabbableIndex(cut));
|
|
// The parent must NOT have collapsed as a side effect.
|
|
Assert.Equal("true", TreeItems(cut)[0].GetAttribute("aria-expanded"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ArrowLeft_OnCollapsedBranchChild_MovesToParent()
|
|
{
|
|
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");
|
|
|
|
// Alpha-2 (index 2) is a branch but collapsed → move to its parent Alpha.
|
|
PressKey(cut, 2, "ArrowLeft");
|
|
|
|
Assert.Equal(0, TabbableIndex(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void ArrowLeft_OnRootLeaf_IsNoOp()
|
|
{
|
|
var cut = RenderTreeView();
|
|
|
|
PressKey(cut, 0, "ArrowDown");
|
|
PressKey(cut, 1, "ArrowLeft"); // Beta — root level, nothing to collapse
|
|
|
|
Assert.Equal(2, TreeItems(cut).Count);
|
|
Assert.Equal(1, TabbableIndex(cut));
|
|
}
|
|
|
|
// ── Home / End ─────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Home_MovesToFirstVisibleNode()
|
|
{
|
|
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");
|
|
|
|
PressKey(cut, 3, "Home");
|
|
|
|
Assert.Equal(0, TabbableIndex(cut));
|
|
}
|
|
|
|
[Fact]
|
|
public void End_MovesToLastVisibleNode()
|
|
{
|
|
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a" || n.Key == "a2");
|
|
|
|
// Visible: Alpha(0), Alpha-1(1), Alpha-2(2), Alpha-2-X(3), Beta(4).
|
|
Assert.Equal(5, TreeItems(cut).Count);
|
|
|
|
PressKey(cut, 0, "End");
|
|
|
|
Assert.Equal(4, TabbableIndex(cut));
|
|
}
|
|
|
|
// ── Enter / Space activation ───────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Enter_SelectsNode_InSingleMode()
|
|
{
|
|
object? selected = null;
|
|
var cut = RenderTreeView(
|
|
initiallyExpanded: n => n.Key == "a",
|
|
selectable: true,
|
|
onSelectedKeyChanged: k => selected = k);
|
|
|
|
PressKey(cut, 1, "Enter"); // Alpha-1
|
|
|
|
Assert.Equal("a1", selected);
|
|
}
|
|
|
|
[Fact]
|
|
public void Space_SelectsNode_InSingleMode()
|
|
{
|
|
object? selected = null;
|
|
var cut = RenderTreeView(
|
|
selectable: true,
|
|
onSelectedKeyChanged: k => selected = k);
|
|
|
|
PressKey(cut, 1, " "); // Beta
|
|
|
|
Assert.Equal("b", selected);
|
|
}
|
|
|
|
[Fact]
|
|
public void Enter_WhenNotSelectable_RaisesNoCallback()
|
|
{
|
|
object? selected = null;
|
|
var cut = RenderTreeView(selectable: false, onSelectedKeyChanged: k => selected = k);
|
|
|
|
PressKey(cut, 0, "Enter");
|
|
|
|
Assert.Null(selected);
|
|
}
|
|
|
|
[Fact]
|
|
public void Space_InCheckboxMode_TogglesTheNode()
|
|
{
|
|
HashSet<object>? captured = null;
|
|
var cut = RenderTreeView(
|
|
initiallyExpanded: _ => true,
|
|
selectionMode: TreeViewSelectionMode.Checkbox,
|
|
selectedKeys: new HashSet<object>(),
|
|
onSelectedKeysChanged: keys => captured = keys);
|
|
|
|
// Visible: Alpha(0), Alpha-1(1), Alpha-2(2), Alpha-2-X(3), Beta(4).
|
|
// Space on Alpha-1 selects that one leaf.
|
|
PressKey(cut, 1, " ");
|
|
|
|
Assert.NotNull(captured);
|
|
Assert.Equal(new HashSet<object> { "a1" }, captured);
|
|
}
|
|
|
|
[Fact]
|
|
public void Enter_InCheckboxMode_OnFolder_CascadesToDescendantLeaves()
|
|
{
|
|
HashSet<object>? captured = null;
|
|
var cut = RenderTreeView(
|
|
initiallyExpanded: _ => true,
|
|
selectionMode: TreeViewSelectionMode.Checkbox,
|
|
selectedKeys: new HashSet<object>(),
|
|
onSelectedKeysChanged: keys => captured = keys);
|
|
|
|
PressKey(cut, 0, "Enter"); // Alpha → leaves a1 + a2x
|
|
|
|
Assert.NotNull(captured);
|
|
Assert.Equal(2, captured!.Count);
|
|
Assert.Contains((object)"a1", captured);
|
|
Assert.Contains((object)"a2x", captured);
|
|
}
|
|
|
|
[Fact]
|
|
public void Keydown_OnNestedNode_DoesNotAlsoActivateItsAncestor()
|
|
{
|
|
// The treeitem stops keydown propagation, so a key pressed on a child must
|
|
// not be handled a second time by every enclosing <li>.
|
|
object? selected = null;
|
|
var cut = RenderTreeView(
|
|
initiallyExpanded: n => n.Key == "a",
|
|
selectable: true,
|
|
onSelectedKeyChanged: k => selected = k);
|
|
|
|
PressKey(cut, 1, "Enter"); // Alpha-1, nested inside Alpha
|
|
|
|
Assert.Equal("a1", selected);
|
|
}
|
|
|
|
// ── Existing chevron behaviour (regression) ────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ChevronToggle_EnterKey_StillExpands_AndDoesNotSelect()
|
|
{
|
|
object? selected = null;
|
|
var cut = RenderTreeView(selectable: true, onSelectedKeyChanged: k => selected = k);
|
|
|
|
cut.Find(".tv-toggle").KeyDown(new KeyboardEventArgs { Key = "Enter" });
|
|
|
|
Assert.Equal("true", TreeItems(cut)[0].GetAttribute("aria-expanded"));
|
|
// The chevron stops propagation, so the treeitem never sees the Enter.
|
|
Assert.Null(selected);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChevronToggle_SpaceKey_StillExpands()
|
|
{
|
|
var cut = RenderTreeView();
|
|
|
|
cut.Find(".tv-toggle").KeyDown(new KeyboardEventArgs { Key = " " });
|
|
|
|
Assert.Equal("true", TreeItems(cut)[0].GetAttribute("aria-expanded"));
|
|
}
|
|
|
|
// ── ARIA structure ─────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void TreeItems_CarryOneBasedAriaLevel()
|
|
{
|
|
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a" || n.Key == "a2");
|
|
|
|
var items = TreeItems(cut);
|
|
// Alpha, Alpha-1, Alpha-2, Alpha-2-X, Beta
|
|
Assert.Equal("1", items[0].GetAttribute("aria-level"));
|
|
Assert.Equal("2", items[1].GetAttribute("aria-level"));
|
|
Assert.Equal("2", items[2].GetAttribute("aria-level"));
|
|
Assert.Equal("3", items[3].GetAttribute("aria-level"));
|
|
Assert.Equal("1", items[4].GetAttribute("aria-level"));
|
|
}
|
|
|
|
[Fact]
|
|
public void TreeItems_CarryAriaPosInSetAndSetSize()
|
|
{
|
|
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");
|
|
|
|
var items = TreeItems(cut);
|
|
|
|
// Two roots.
|
|
Assert.Equal("1", items[0].GetAttribute("aria-posinset"));
|
|
Assert.Equal("2", items[0].GetAttribute("aria-setsize"));
|
|
|
|
// Alpha's two children.
|
|
Assert.Equal("1", items[1].GetAttribute("aria-posinset"));
|
|
Assert.Equal("2", items[1].GetAttribute("aria-setsize"));
|
|
Assert.Equal("2", items[2].GetAttribute("aria-posinset"));
|
|
Assert.Equal("2", items[2].GetAttribute("aria-setsize"));
|
|
|
|
// Beta — second of the two roots.
|
|
Assert.Equal("2", items[3].GetAttribute("aria-posinset"));
|
|
Assert.Equal("2", items[3].GetAttribute("aria-setsize"));
|
|
}
|
|
|
|
[Fact]
|
|
public void TreeStructure_UsesRoleTree_RoleGroup_AndRoleTreeitem()
|
|
{
|
|
var cut = RenderTreeView(initiallyExpanded: n => n.Key == "a");
|
|
|
|
Assert.NotNull(cut.Find("ul[role='tree']"));
|
|
Assert.Single(cut.FindAll("ul[role='group']"));
|
|
Assert.Equal(4, TreeItems(cut).Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void AriaSelected_IsPresentOnSelectableNodes_AndTracksSelection()
|
|
{
|
|
var cut = RenderTreeView(selectable: true, selectedKey: "a");
|
|
|
|
var items = TreeItems(cut);
|
|
Assert.Equal("true", items[0].GetAttribute("aria-selected"));
|
|
Assert.Equal("false", items[1].GetAttribute("aria-selected"));
|
|
}
|
|
|
|
[Fact]
|
|
public void RootTree_SuppressesBrowserScrollDefaults_ForNavigationKeys()
|
|
{
|
|
// Space/Arrow/Home/End would otherwise scroll the page while a treeitem holds
|
|
// focus. bUnit runs no scripts, so this locks the native guard's presence and
|
|
// the key set it covers — not its runtime effect.
|
|
var cut = RenderTreeView();
|
|
|
|
var handler = cut.Find("ul[role='tree']").GetAttribute("onkeydown");
|
|
|
|
Assert.NotNull(handler);
|
|
Assert.Contains("preventDefault", handler!);
|
|
// Scoped to the treeitem itself so a consumer's buttons inside NodeContent keep
|
|
// their Enter/Space activation.
|
|
Assert.Contains("'treeitem'", handler);
|
|
foreach (var key in new[] { "' '", "'ArrowUp'", "'ArrowDown'", "'ArrowLeft'", "'ArrowRight'", "'Home'", "'End'" })
|
|
{
|
|
Assert.Contains(key, handler);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AriaSelected_IsAbsent_WhenTreeIsNotSelectable()
|
|
{
|
|
var cut = RenderTreeView(selectable: false);
|
|
|
|
Assert.Null(TreeItems(cut)[0].GetAttribute("aria-selected"));
|
|
}
|
|
}
|