From da4f29f6eeab64655dc4eb70d47cce54369c84ba Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 23 Mar 2026 02:26:09 -0400 Subject: [PATCH] feat(ui): add selection support to TreeView (R5) --- .../Components/Shared/TreeView.razor | 21 +++- .../TreeViewTests.cs | 103 ++++++++++++++++-- 2 files changed, 108 insertions(+), 16 deletions(-) diff --git a/src/ScadaLink.CentralUI/Components/Shared/TreeView.razor b/src/ScadaLink.CentralUI/Components/Shared/TreeView.razor index fcc9981..92d286f 100644 --- a/src/ScadaLink.CentralUI/Components/Shared/TreeView.razor +++ b/src/ScadaLink.CentralUI/Components/Shared/TreeView.razor @@ -26,17 +26,18 @@ else var isExpanded = _expandedKeys.Contains(key);
  • -
    + aria-expanded="@(isBranch ? (isExpanded ? "true" : "false") : null)" + aria-selected="@(Selectable && SelectedKey != null && SelectedKey.Equals(key) ? "true" : null)"> +
    @if (isBranch) { - @(isExpanded ? "\u2212" : "+") + @(isExpanded ? "\u2212" : "+") } else { } - + @NodeContent(item)
    @@ -67,6 +68,10 @@ else [Parameter] public int IndentPx { get; set; } = 24; [Parameter] public bool ShowGuideLines { get; set; } = true; [Parameter] public Func? InitiallyExpanded { get; set; } + [Parameter] public bool Selectable { get; set; } + [Parameter] public object? SelectedKey { get; set; } + [Parameter] public EventCallback SelectedKeyChanged { get; set; } + [Parameter] public string SelectedCssClass { get; set; } = "bg-primary bg-opacity-10"; protected override void OnParametersSet() { @@ -103,4 +108,12 @@ else _expandedKeys.Add(key); } } + + private async Task OnContentClick(object key) + { + if (Selectable) + { + await SelectedKeyChanged.InvokeAsync(key); + } + } } diff --git a/tests/ScadaLink.CentralUI.Tests/TreeViewTests.cs b/tests/ScadaLink.CentralUI.Tests/TreeViewTests.cs index a17cf2c..c618344 100644 --- a/tests/ScadaLink.CentralUI.Tests/TreeViewTests.cs +++ b/tests/ScadaLink.CentralUI.Tests/TreeViewTests.cs @@ -29,20 +29,39 @@ public class TreeViewTests : BunitContext List? items = null, RenderFragment? emptyContent = null, int indentPx = 24, - Func? initiallyExpanded = null) + Func? initiallyExpanded = null, + bool selectable = false, + object? selectedKey = null, + Action? onSelectedKeyChanged = null, + string? selectedCssClass = null) { - return Render>(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 => + return Render>(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, $"{node.Label}"); + }) + .Add(p => p.IndentPx, indentPx) + .Add(p => p.EmptyContent, emptyContent) + .Add(p => p.InitiallyExpanded, initiallyExpanded) + .Add(p => p.Selectable, selectable) + .Add(p => p.SelectedKey, selectedKey); + + if (onSelectedKeyChanged != null) { - builder.AddMarkupContent(0, $"{node.Label}"); - }) - .Add(p => p.IndentPx, indentPx) - .Add(p => p.EmptyContent, emptyContent) - .Add(p => p.InitiallyExpanded, initiallyExpanded)); + parameters.Add(p => p.SelectedKeyChanged, onSelectedKeyChanged); + } + + if (selectedCssClass != null) + { + parameters.Add(p => p.SelectedCssClass, selectedCssClass); + } + }); } [Fact] @@ -238,4 +257,64 @@ public class TreeViewTests : BunitContext var alpha2xRow = rows[3]; Assert.Contains("padding-left: 60px", alpha2xRow.GetAttribute("style")); } + + [Fact] + public void Selection_Disabled_ClickDoesNotFireCallback() + { + object? selected = null; + var cut = RenderTreeView(selectable: false, onSelectedKeyChanged: k => selected = k); + + cut.Find(".tv-content").Click(); + + Assert.Null(selected); + } + + [Fact] + public void Selection_Enabled_ClickContentFiresCallback() + { + object? selected = null; + var cut = RenderTreeView(selectable: true, onSelectedKeyChanged: k => selected = k); + + cut.Find(".tv-content").Click(); + + Assert.Equal("a", selected); + } + + [Fact] + public void Selection_ClickToggle_DoesNotChangeSelection() + { + object? selected = null; + var cut = RenderTreeView(selectable: true, onSelectedKeyChanged: k => selected = k); + + cut.Find(".tv-toggle").Click(); + + Assert.Null(selected); + } + + [Fact] + public void Selection_SelectedNode_HasCssClass() + { + var cut = RenderTreeView(selectable: true, selectedKey: "a"); + + var alphaRow = cut.FindAll(".tv-row")[0]; + Assert.Contains("bg-primary", alphaRow.GetAttribute("class")); + } + + [Fact] + public void Selection_CustomCssClass_Applied() + { + var cut = RenderTreeView(selectable: true, selectedKey: "a", selectedCssClass: "my-highlight"); + + var alphaRow = cut.FindAll(".tv-row")[0]; + Assert.Contains("my-highlight", alphaRow.GetAttribute("class")); + } + + [Fact] + public void Selection_AriaSelected_SetOnSelectedNode() + { + var cut = RenderTreeView(selectable: true, selectedKey: "a"); + + var alphaLi = cut.FindAll("li[role='treeitem']")[0]; + Assert.Equal("true", alphaLi.GetAttribute("aria-selected")); + } }