feat(ui): add right-click context menu to TreeView (R15)
This commit is contained in:
@@ -19,6 +19,14 @@ else
|
||||
</ul>
|
||||
}
|
||||
|
||||
@if (_showContextMenu && _contextMenuItem != null && ContextMenu != null)
|
||||
{
|
||||
<div class="tv-ctx-overlay" @onclick="DismissContextMenu" style="position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1049;background:transparent;"></div>
|
||||
<div class="dropdown-menu show" style="position:fixed;top:@(_contextMenuY)px;left:@(_contextMenuX)px;z-index:1050;">
|
||||
@ContextMenu(_contextMenuItem)
|
||||
</div>
|
||||
}
|
||||
|
||||
@{ void RenderNode(TItem item, int depth)
|
||||
{
|
||||
var key = KeySelector(item);
|
||||
@@ -29,7 +37,8 @@ else
|
||||
<li role="treeitem" @key="key"
|
||||
aria-expanded="@(isBranch ? (isExpanded ? "true" : "false") : null)"
|
||||
aria-selected="@(Selectable && SelectedKey != null && SelectedKey.Equals(key) ? "true" : null)">
|
||||
<div class="tv-row @(Selectable && SelectedKey != null && SelectedKey.Equals(key) ? SelectedCssClass : "")" style="padding-left: @(depth * IndentPx)px">
|
||||
<div class="tv-row @(Selectable && SelectedKey != null && SelectedKey.Equals(key) ? SelectedCssClass : "")" style="padding-left: @(depth * IndentPx)px"
|
||||
@oncontextmenu="(e) => OnContextMenu(e, item)" @oncontextmenu:preventDefault="@(ContextMenu != null)">
|
||||
@if (isBranch)
|
||||
{
|
||||
<span class="tv-toggle" @onclick="() => ToggleExpand(key)" @onclick:stopPropagation>@(isExpanded ? "\u2212" : "+")</span>
|
||||
@@ -60,6 +69,10 @@ else
|
||||
private HashSet<object> _expandedKeys = new();
|
||||
private bool _initialExpansionApplied;
|
||||
private bool _storageLoaded;
|
||||
private TItem? _contextMenuItem;
|
||||
private double _contextMenuX;
|
||||
private double _contextMenuY;
|
||||
private bool _showContextMenu;
|
||||
|
||||
[Parameter, EditorRequired] public IReadOnlyList<TItem> Items { get; set; } = [];
|
||||
[Parameter, EditorRequired] public Func<TItem, IReadOnlyList<TItem>> ChildrenSelector { get; set; } = default!;
|
||||
@@ -67,6 +80,7 @@ else
|
||||
[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 RenderFragment<TItem>? ContextMenu { get; set; }
|
||||
[Parameter] public int IndentPx { get; set; } = 24;
|
||||
[Parameter] public bool ShowGuideLines { get; set; } = true;
|
||||
[Parameter] public Func<TItem, bool>? InitiallyExpanded { get; set; }
|
||||
@@ -164,6 +178,22 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void OnContextMenu(MouseEventArgs e, TItem item)
|
||||
{
|
||||
if (ContextMenu == null) return;
|
||||
|
||||
_contextMenuItem = item;
|
||||
_contextMenuX = e.ClientX;
|
||||
_contextMenuY = e.ClientY;
|
||||
_showContextMenu = true;
|
||||
}
|
||||
|
||||
private void DismissContextMenu()
|
||||
{
|
||||
_showContextMenu = false;
|
||||
_contextMenuItem = default;
|
||||
}
|
||||
|
||||
/// <summary>Expand every branch node in the tree.</summary>
|
||||
public void ExpandAll()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using ScadaLink.CentralUI.Components.Shared;
|
||||
|
||||
namespace ScadaLink.CentralUI.Tests;
|
||||
@@ -34,7 +35,8 @@ public class TreeViewTests : BunitContext
|
||||
object? selectedKey = null,
|
||||
Action<object?>? onSelectedKeyChanged = null,
|
||||
string? selectedCssClass = null,
|
||||
string? storageKey = null)
|
||||
string? storageKey = null,
|
||||
RenderFragment<TestNode>? contextMenu = null)
|
||||
{
|
||||
return Render<TreeView<TestNode>>(parameters =>
|
||||
{
|
||||
@@ -67,6 +69,11 @@ public class TreeViewTests : BunitContext
|
||||
{
|
||||
parameters.Add(p => p.StorageKey, storageKey);
|
||||
}
|
||||
|
||||
if (contextMenu != null)
|
||||
{
|
||||
parameters.Add(p => p.ContextMenu, contextMenu);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -456,4 +463,63 @@ public class TreeViewTests : BunitContext
|
||||
var labels = cut.FindAll(".node-label");
|
||||
Assert.Equal(2, labels.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextMenu_Null_NoMenuRendered()
|
||||
{
|
||||
var cut = RenderTreeView();
|
||||
|
||||
// Right-click Alpha
|
||||
var row = cut.Find(".tv-row");
|
||||
row.TriggerEvent("oncontextmenu", new MouseEventArgs { ClientX = 100, ClientY = 200 });
|
||||
|
||||
// No dropdown-menu should appear
|
||||
Assert.Throws<Bunit.ElementNotFoundException>(() => cut.Find(".dropdown-menu"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextMenu_RightClickShowsMenu()
|
||||
{
|
||||
var cut = RenderTreeView(contextMenu: node => builder =>
|
||||
{
|
||||
builder.AddMarkupContent(0, $"<button class=\"ctx-btn\">{node.Label}</button>");
|
||||
});
|
||||
|
||||
// Right-click Alpha
|
||||
var row = cut.Find(".tv-row");
|
||||
row.TriggerEvent("oncontextmenu", new MouseEventArgs { ClientX = 100, ClientY = 200 });
|
||||
|
||||
// Dropdown menu should contain the button for Alpha
|
||||
var menu = cut.Find(".dropdown-menu");
|
||||
Assert.NotNull(menu);
|
||||
var btn = menu.QuerySelector(".ctx-btn");
|
||||
Assert.NotNull(btn);
|
||||
Assert.Equal("Alpha", btn!.TextContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContextMenu_RightClickDifferentNode_ReplacesMenu()
|
||||
{
|
||||
var cut = RenderTreeView(
|
||||
initiallyExpanded: n => n.Key == "a",
|
||||
contextMenu: node => builder =>
|
||||
{
|
||||
builder.AddMarkupContent(0, $"<button class=\"ctx-btn\">{node.Label}</button>");
|
||||
});
|
||||
|
||||
// Right-click Alpha
|
||||
var rows = cut.FindAll(".tv-row");
|
||||
rows[0].TriggerEvent("oncontextmenu", new MouseEventArgs { ClientX = 100, ClientY = 200 });
|
||||
|
||||
// Now right-click Alpha-1
|
||||
rows = cut.FindAll(".tv-row");
|
||||
rows[1].TriggerEvent("oncontextmenu", new MouseEventArgs { ClientX = 150, ClientY = 250 });
|
||||
|
||||
// Should be only one dropdown-menu, showing Alpha-1
|
||||
var menus = cut.FindAll(".dropdown-menu");
|
||||
Assert.Single(menus);
|
||||
var btn = menus[0].QuerySelector(".ctx-btn");
|
||||
Assert.NotNull(btn);
|
||||
Assert.Equal("Alpha-1", btn!.TextContent);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user