feat(ui): add ExpandAll, CollapseAll, RevealNode to TreeView (R12, R13)
This commit is contained in:
@@ -163,4 +163,95 @@ else
|
||||
await SelectedKeyChanged.InvokeAsync(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Expand every branch node in the tree.</summary>
|
||||
public void ExpandAll()
|
||||
{
|
||||
if (_items is { Count: > 0 })
|
||||
{
|
||||
ExpandAllRecursive(_items);
|
||||
}
|
||||
|
||||
PersistExpandedState();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ExpandAllRecursive(IReadOnlyList<TItem> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (HasChildrenSelector(item))
|
||||
{
|
||||
_expandedKeys.Add(KeySelector(item));
|
||||
}
|
||||
|
||||
var children = ChildrenSelector(item);
|
||||
if (children is { Count: > 0 })
|
||||
{
|
||||
ExpandAllRecursive(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Collapse every node in the tree.</summary>
|
||||
public void CollapseAll()
|
||||
{
|
||||
_expandedKeys.Clear();
|
||||
PersistExpandedState();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expand all ancestors of the given key so it becomes visible.
|
||||
/// Optionally select the node.
|
||||
/// </summary>
|
||||
public async Task RevealNode(object key, bool select = false)
|
||||
{
|
||||
var parentLookup = BuildParentLookup();
|
||||
|
||||
// If key is not in the tree at all, no-op
|
||||
if (!parentLookup.ContainsKey(key))
|
||||
return;
|
||||
|
||||
// Walk up through ancestors
|
||||
var current = key;
|
||||
while (parentLookup.TryGetValue(current, out var parentKey) && parentKey != null)
|
||||
{
|
||||
_expandedKeys.Add(parentKey);
|
||||
current = parentKey;
|
||||
}
|
||||
|
||||
if (select && Selectable)
|
||||
{
|
||||
await SelectedKeyChanged.InvokeAsync(key);
|
||||
}
|
||||
|
||||
PersistExpandedState();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private Dictionary<object, object?> BuildParentLookup()
|
||||
{
|
||||
var lookup = new Dictionary<object, object?>();
|
||||
if (_items is { Count: > 0 })
|
||||
{
|
||||
BuildParentLookupRecursive(_items, null, lookup);
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
|
||||
private void BuildParentLookupRecursive(IReadOnlyList<TItem> items, object? parentKey, Dictionary<object, object?> lookup)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
var key = KeySelector(item);
|
||||
lookup[key] = parentKey;
|
||||
|
||||
var children = ChildrenSelector(item);
|
||||
if (children is { Count: > 0 })
|
||||
{
|
||||
BuildParentLookupRecursive(children, key, lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,4 +383,77 @@ public class TreeViewTests : BunitContext
|
||||
var labels = cut.FindAll(".node-label");
|
||||
Assert.DoesNotContain(labels, l => l.TextContent == "Alpha-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpandAll_ExpandsAllBranches()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
// Everything collapsed initially
|
||||
Assert.Equal(2, cut.FindAll(".node-label").Count);
|
||||
|
||||
cut.InvokeAsync(() => cut.Instance.ExpandAll());
|
||||
|
||||
var labels = cut.FindAll(".node-label");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-1");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-2");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-2-X");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollapseAll_CollapsesAllBranches()
|
||||
{
|
||||
var cut = RenderTreeView(initiallyExpanded: _ => true);
|
||||
|
||||
// Verify deep content is visible
|
||||
var labels = cut.FindAll(".node-label");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-2-X");
|
||||
|
||||
cut.InvokeAsync(() => cut.Instance.CollapseAll());
|
||||
|
||||
// Only roots should be visible
|
||||
labels = cut.FindAll(".node-label");
|
||||
Assert.Equal(2, labels.Count);
|
||||
Assert.DoesNotContain(labels, l => l.TextContent == "Alpha-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RevealNode_ExpandsAncestors()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
// Everything collapsed initially
|
||||
Assert.Equal(2, cut.FindAll(".node-label").Count);
|
||||
|
||||
cut.InvokeAsync(() => cut.Instance.RevealNode("a2x"));
|
||||
|
||||
// Alpha-2-X should now be visible (Alpha and Alpha-2 expanded)
|
||||
var labels = cut.FindAll(".node-label");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-2-X");
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-1"); // sibling also visible since Alpha is expanded
|
||||
Assert.Contains(labels, l => l.TextContent == "Alpha-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RevealNode_WithSelect_SelectsNode()
|
||||
{
|
||||
object? selected = null;
|
||||
var cut = RenderTreeView(selectable: true, onSelectedKeyChanged: k => selected = k);
|
||||
|
||||
cut.InvokeAsync(() => cut.Instance.RevealNode("a2x", select: true));
|
||||
|
||||
Assert.Equal("a2x", selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RevealNode_UnknownKey_NoOp()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
cut.InvokeAsync(() => cut.Instance.RevealNode("nonexistent"));
|
||||
|
||||
// Alpha should still be collapsed
|
||||
var labels = cut.FindAll(".node-label");
|
||||
Assert.Equal(2, labels.Count);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user