feat(ui): add TreeView<TItem> component with core rendering, expand/collapse, ARIA (R1-R4, R14)
This commit is contained in:
106
src/ScadaLink.CentralUI/Components/Shared/TreeView.razor
Normal file
106
src/ScadaLink.CentralUI/Components/Shared/TreeView.razor
Normal file
@@ -0,0 +1,106 @@
|
||||
@* Reusable hierarchical tree view with expand/collapse, ARIA roles, and guide lines *@
|
||||
@typeparam TItem
|
||||
|
||||
@if (_items is null || _items.Count == 0)
|
||||
{
|
||||
if (EmptyContent != null)
|
||||
{
|
||||
@EmptyContent
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul role="tree" class="tv-root @(ShowGuideLines ? "tv-guides" : "")">
|
||||
@foreach (var item in _items)
|
||||
{
|
||||
RenderNode(item, 0);
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@{ void RenderNode(TItem item, int depth)
|
||||
{
|
||||
var key = KeySelector(item);
|
||||
var children = ChildrenSelector(item);
|
||||
var isBranch = HasChildrenSelector(item);
|
||||
var isExpanded = _expandedKeys.Contains(key);
|
||||
|
||||
<li role="treeitem" @key="key"
|
||||
aria-expanded="@(isBranch ? (isExpanded ? "true" : "false") : null)">
|
||||
<div class="tv-row" style="padding-left: @(depth * IndentPx)px">
|
||||
@if (isBranch)
|
||||
{
|
||||
<span class="tv-toggle" @onclick="() => ToggleExpand(key)">@(isExpanded ? "\u2212" : "+")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="tv-spacer"></span>
|
||||
}
|
||||
<span class="tv-content">
|
||||
@NodeContent(item)
|
||||
</span>
|
||||
</div>
|
||||
@if (isBranch && isExpanded && children is { Count: > 0 })
|
||||
{
|
||||
<ul role="group">
|
||||
@foreach (var child in children)
|
||||
{
|
||||
RenderNode(child, depth + 1);
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
private IReadOnlyList<TItem>? _items;
|
||||
private HashSet<object> _expandedKeys = new();
|
||||
private bool _initialExpansionApplied;
|
||||
|
||||
[Parameter, EditorRequired] public IReadOnlyList<TItem> Items { get; set; } = [];
|
||||
[Parameter, EditorRequired] public Func<TItem, IReadOnlyList<TItem>> ChildrenSelector { get; set; } = default!;
|
||||
[Parameter, EditorRequired] public Func<TItem, bool> HasChildrenSelector { get; set; } = default!;
|
||||
[Parameter, EditorRequired] public Func<TItem, object> KeySelector { get; set; } = default!;
|
||||
[Parameter, EditorRequired] public RenderFragment<TItem> NodeContent { get; set; } = default!;
|
||||
[Parameter] public RenderFragment? EmptyContent { get; set; }
|
||||
[Parameter] public int IndentPx { get; set; } = 24;
|
||||
[Parameter] public bool ShowGuideLines { get; set; } = true;
|
||||
[Parameter] public Func<TItem, bool>? InitiallyExpanded { get; set; }
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
_items = Items;
|
||||
|
||||
if (!_initialExpansionApplied && InitiallyExpanded != null && _items is { Count: > 0 })
|
||||
{
|
||||
_initialExpansionApplied = true;
|
||||
ApplyInitialExpansion(_items);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyInitialExpansion(IReadOnlyList<TItem> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (InitiallyExpanded!(item))
|
||||
{
|
||||
_expandedKeys.Add(KeySelector(item));
|
||||
}
|
||||
|
||||
var children = ChildrenSelector(item);
|
||||
if (children is { Count: > 0 })
|
||||
{
|
||||
ApplyInitialExpansion(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleExpand(object key)
|
||||
{
|
||||
if (!_expandedKeys.Remove(key))
|
||||
{
|
||||
_expandedKeys.Add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user