diff --git a/src/ScadaLink.CentralUI/Components/Shared/TreeView.razor b/src/ScadaLink.CentralUI/Components/Shared/TreeView.razor index cddb90f..de894d5 100644 --- a/src/ScadaLink.CentralUI/Components/Shared/TreeView.razor +++ b/src/ScadaLink.CentralUI/Components/Shared/TreeView.razor @@ -19,6 +19,14 @@ else } +@if (_showContextMenu && _contextMenuItem != null && ContextMenu != null) +{ +
+ +} + @{ void RenderNode(TItem item, int depth) { var key = KeySelector(item); @@ -29,7 +37,8 @@ else
  • -
    +
    @if (isBranch) { @(isExpanded ? "\u2212" : "+") @@ -60,6 +69,10 @@ else private HashSet _expandedKeys = new(); private bool _initialExpansionApplied; private bool _storageLoaded; + private TItem? _contextMenuItem; + private double _contextMenuX; + private double _contextMenuY; + private bool _showContextMenu; [Parameter, EditorRequired] public IReadOnlyList Items { get; set; } = []; [Parameter, EditorRequired] public Func> ChildrenSelector { get; set; } = default!; @@ -67,6 +80,7 @@ else [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; } @@ -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; + } + /// Expand every branch node in the tree. public void ExpandAll() { diff --git a/tests/ScadaLink.CentralUI.Tests/TreeViewTests.cs b/tests/ScadaLink.CentralUI.Tests/TreeViewTests.cs index 544a12e..4096b93 100644 --- a/tests/ScadaLink.CentralUI.Tests/TreeViewTests.cs +++ b/tests/ScadaLink.CentralUI.Tests/TreeViewTests.cs @@ -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? onSelectedKeyChanged = null, string? selectedCssClass = null, - string? storageKey = null) + string? storageKey = null, + RenderFragment? contextMenu = null) { return Render>(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(() => cut.Find(".dropdown-menu")); + } + + [Fact] + public void ContextMenu_RightClickShowsMenu() + { + var cut = RenderTreeView(contextMenu: node => builder => + { + builder.AddMarkupContent(0, $""); + }); + + // 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, $""); + }); + + // 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); + } }