feat(ui): add ExpandAll, CollapseAll, RevealNode to TreeView (R12, R13)
This commit is contained in:
@@ -163,4 +163,95 @@ else
|
|||||||
await SelectedKeyChanged.InvokeAsync(key);
|
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");
|
var labels = cut.FindAll(".node-label");
|
||||||
Assert.DoesNotContain(labels, l => l.TextContent == "Alpha-1");
|
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