@* Reusable hierarchical tree view with expand/collapse, ARIA roles, and guide lines *@
@typeparam TItem
@inject IJSRuntime JSRuntime
@if (_items is null || _items.Count == 0)
{
if (EmptyContent != null)
{
@EmptyContent
}
}
else
{
@foreach (var item in _items)
{
RenderNode(item, 0);
}
}
@if (_showContextMenu && _contextMenuItem != null && ContextMenu != null)
{
}
@{ void RenderNode(TItem item, int depth)
{
var key = KeySelector(item);
var children = ChildrenSelector(item);
var isBranch = HasChildrenSelector(item);
var isExpanded = _expandedKeys.Contains(KeyStr(key));
var isSelected = Selectable && SelectedKey != null && SelectedKey.Equals(key);
var rowClasses = "tv-row" + (isSelected ? " tv-selected " + SelectedCssClass : "");
// Checkbox-mode tri-state computed for this node (folder = aggregate of
// descendant leaves; leaf = present-in-SelectedKeys).
var checkState = SelectionMode == TreeViewSelectionMode.Checkbox
? ComputeCheckState(item)
: CheckState.Unchecked;
OnContextMenu(e, item)" @oncontextmenu:preventDefault="@(ContextMenu != null)" @oncontextmenu:stopPropagation="@(ContextMenu != null)">
@if (isBranch)
{
ToggleExpand(key)" @onclick:stopPropagation>
}
else
{
}
@if (SelectionMode == TreeViewSelectionMode.Checkbox)
{
OnCheckboxToggle(item)"
@onclick:stopPropagation />
}
OnContentClick(key)" @onclick:stopPropagation>
@NodeContent(item)
@if (isBranch && isExpanded && children is { Count: > 0 })
{
@foreach (var child in children)
{
RenderNode(child, depth + 1);
}
}
}
}
@code {
private IReadOnlyList? _items;
private HashSet _expandedKeys = new();
/// Normalize any key object to a string for consistent comparison with sessionStorage.
private string KeyStr(object key) => key.ToString()!;
private bool _initialExpansionApplied;
private bool _storageLoaded;
private TItem? _contextMenuItem;
private double _contextMenuX;
private double _contextMenuY;
private bool _showContextMenu;
private bool _contextMenuNeedsFocus;
private ElementReference _contextMenuRef;
[Parameter, EditorRequired] public IReadOnlyList Items { get; set; } = [];
[Parameter, EditorRequired] public Func> ChildrenSelector { get; set; } = default!;
[Parameter, EditorRequired] public Func HasChildrenSelector { get; set; } = default!;
[Parameter, EditorRequired] public Func KeySelector { get; set; } = default!;
[Parameter, EditorRequired] public RenderFragment NodeContent { get; set; } = default!;
[Parameter] public RenderFragment? EmptyContent { get; set; }
[Parameter] public RenderFragment? ContextMenu { get; set; }
[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