Skip to content

Commit d95dca4

Browse files
authored
feat(Tab): support context menu (#5697)
* feat: 增加右键支持 * doc: 更新示例 * doc: 更新示例 * test: 更新单元测试 * test: 更新单元测试 * chore: bump version 9.5.0-beta12
1 parent b5fbcfd commit d95dca4

File tree

8 files changed

+125
-4
lines changed

8 files changed

+125
-4
lines changed

src/BootstrapBlazor.Server/Components/Samples/Tabs.razor

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,30 @@ private void Navigation()
496496
</Tab>
497497
</DemoBlock>
498498

499+
<DemoBlock Title="@Localizer["TabsContextMenuTitle"]" Introduction="@Localizer["TabsContextMenuIntro"]" Name="ContextMenu">
500+
<ContextMenuZone>
501+
<Tab IsCard="true" ShowClose="true" TabStyle="TabStyle.Chrome" ShowToolbar="true" @ref="_tab">
502+
<TabItem Text="@Localizer["TabItem1Text"]" Icon="fa-solid fa-user">
503+
<div>@Localizer["TabItem1Content"]</div>
504+
</TabItem>
505+
<TabItem Text="@Localizer["TabItem2Text"]" Icon="fa-solid fa-gauge-high">
506+
<div>@Localizer["TabItem2Content"]</div>
507+
</TabItem>
508+
<TabItem Text="@Localizer["TabItem3Text"]" Icon="fa-solid fa-sitemap">
509+
<div>@Localizer["TabItem3Content"]</div>
510+
</TabItem>
511+
<TabItem Text="@Localizer["TabItem4Text"]" Icon="fa-solid fa-building-columns">
512+
<div>@Localizer["TabItem4Content"]</div>
513+
</TabItem>
514+
</Tab>
515+
<ContextMenu>
516+
<ContextMenuItem Icon="fa-solid fa-xmark" Text="@Localizer["ContextClose"]" OnClick="OnClose"></ContextMenuItem>
517+
<ContextMenuItem Icon="fa-solid fa-left-right" Text="@Localizer["ContextCloseOther"]" OnClick="OnCloseOther"></ContextMenuItem>
518+
<ContextMenuItem Icon="fa-solid fa-arrows-left-right-to-line" Text="@Localizer["ContextCloseAll"]" OnClick="OnCloseAll"></ContextMenuItem>
519+
</ContextMenu>
520+
</ContextMenuZone>
521+
</DemoBlock>
522+
499523
<AttributeTable Items="@GetAttributes()" Title="@Localizer["AttTitle"]" />
500524

501525
<MethodTable Items="@GetMethods()" Title="@Localizer["MethodTitle"]" />

src/BootstrapBlazor.Server/Components/Samples/Tabs.razor.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,33 @@ private Task OnSetTitle(string text)
171171
return Task.CompletedTask;
172172
}
173173

174+
[NotNull]
175+
private Tab? _tab = null;
176+
177+
private async Task OnClose(ContextMenuItem item, object? context)
178+
{
179+
if (context is TabItem tabItem)
180+
{
181+
await _tab.RemoveTab(tabItem);
182+
}
183+
}
184+
185+
private Task OnCloseOther(ContextMenuItem item, object? context)
186+
{
187+
if (context is TabItem tabItem)
188+
{
189+
_tab.ActiveTab(tabItem);
190+
}
191+
_tab.CloseOtherTabs();
192+
return Task.CompletedTask;
193+
}
194+
195+
private Task OnCloseAll(ContextMenuItem item, object? context)
196+
{
197+
_tab.CloseAllTabs();
198+
return Task.CompletedTask;
199+
}
200+
174201
/// <summary>
175202
/// 获得属性方法
176203
/// </summary>

src/BootstrapBlazor.Server/Locales/en-US.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2132,7 +2132,12 @@
21322132
"AttributeRefreshToolbarButtonIcon": "Toolbar refresh button icon",
21332133
"AttributeFullscreenToolbarButtonIcon": "Toolbar full screen button icon",
21342134
"TabsToolbarDesc": "After clicking the button, the counter value increases, and clicking the <b>Refresh</b> button on the toolbar will reset the counter.",
2135-
"AttributeOnToolbarRefreshCallback": "Click the toolbar refresh button callback method"
2135+
"AttributeOnToolbarRefreshCallback": "Click the toolbar refresh button callback method",
2136+
"ContextClose": "Close",
2137+
"ContextCloseOther": "Close Other Tabs",
2138+
"ContextCloseAll": "Close All Tabs",
2139+
"TabsContextMenuTitle": "TabItem Context Menu",
2140+
"TabsContextMenuIntro": "Use the built-in <code>ContextMenuZone</code> component to pop up a custom context menu when you right-click a tab item"
21362141
},
21372142
"BootstrapBlazor.Server.Components.Components.DemoTabItem": {
21382143
"Info": "Reset the title of this <code>TabItem</code> by click the button",

src/BootstrapBlazor.Server/Locales/zh-CN.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2132,7 +2132,12 @@
21322132
"AttributeRefreshToolbarButtonIcon": "工具栏刷新按钮图标",
21332133
"AttributeFullscreenToolbarButtonIcon": "工具栏全屏按钮图标",
21342134
"TabsToolbarDesc": "点击按钮计数器数值增加后,点击工具栏的 <b>刷新</b> 按钮计数器清零",
2135-
"AttributeOnToolbarRefreshCallback": "点击工具栏刷新按钮回调方法"
2135+
"AttributeOnToolbarRefreshCallback": "点击工具栏刷新按钮回调方法",
2136+
"ContextClose": "关闭",
2137+
"ContextCloseOther": "关闭其他",
2138+
"ContextCloseAll": "关闭全部",
2139+
"TabsContextMenuTitle": "右键菜单",
2140+
"TabsContextMenuIntro": "通过内置 <code>ContextMenuZone</code> 组件,点击标签页右键时弹窗自定义右键菜单"
21362141
},
21372142
"BootstrapBlazor.Server.Components.Components.DemoTabItem": {
21382143
"Info": "点击下方按钮,本 <code>TabItem</code> 标题更改为当前分钟与秒",

src/BootstrapBlazor/BootstrapBlazor.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
4-
<Version>9.4.11</Version>
4+
<Version>9.5.0-beta12</Version>
55
</PropertyGroup>
66

77
<ItemGroup>

src/BootstrapBlazor/Components/Tab/Tab.razor

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ else
166166

167167
RenderFragment RenderHeaderItem(TabItem item) =>
168168
@<div @key="@item" class="@GetItemWrapClassString(item)" draggable="@DraggableString">
169-
<a href="@item.Url" role="tab" tabindex="-1" class="@GetClassString(item)" @onclick="@(() => OnClickTabItem(item))" @onclick:preventDefault="@(!ClickTabToNavigation)">
169+
<a href="@item.Url" role="tab" tabindex="-1" class="@GetClassString(item)"
170+
@oncontextmenu="e => OnContextMenu(e, item)" @oncontextmenu:preventDefault="IsPreventDefault"
171+
@onclick="@(() => OnClickTabItem(item))" @onclick:preventDefault="@(!ClickTabToNavigation)">
170172
@RenderHeaderItemContent(item)
171173
</a>
172174
@if (TabStyle == TabStyle.Chrome)

src/BootstrapBlazor/Components/Tab/Tab.razor.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
55

6+
using Microsoft.AspNetCore.Components.Web;
67
using Microsoft.Extensions.Localization;
78
using System.Collections.Concurrent;
89
using System.Reflection;
@@ -370,6 +371,9 @@ public partial class Tab : IHandlerException
370371
[Inject, NotNull]
371372
private DialogService? DialogService { get; set; }
372373

374+
[CascadingParameter]
375+
private ContextMenuZone? ContextMenuZone { get; set; }
376+
373377
private ConcurrentDictionary<TabItem, bool> LazyTabCache { get; } = new();
374378

375379
private bool HandlerNavigation { get; set; }
@@ -382,6 +386,8 @@ public partial class Tab : IHandlerException
382386

383387
private readonly ConcurrentDictionary<TabItem, TabItemContent> _cache = [];
384388

389+
private bool IsPreventDefault => ContextMenuZone != null;
390+
385391
/// <summary>
386392
/// <inheritdoc/>
387393
/// </summary>
@@ -965,4 +971,12 @@ protected override async ValueTask DisposeAsync(bool disposing)
965971
ErrorLogger?.UnRegister(this);
966972
}
967973
}
974+
975+
private async Task OnContextMenu(MouseEventArgs e, TabItem item)
976+
{
977+
if (ContextMenuZone != null)
978+
{
979+
await ContextMenuZone.OnContextMenu(e, item);
980+
}
981+
}
968982
}

test/UnitTest/Components/TabTest.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Bunit.TestDoubles;
88
using Microsoft.AspNetCore.Components.Rendering;
99
using System.Reflection;
10+
using System.Threading.Tasks;
1011
using UnitTest.Misc;
1112

1213
namespace UnitTest.Components;
@@ -25,6 +26,49 @@ protected override void ConfigureServices(IServiceCollection services)
2526
});
2627
}
2728

29+
[Fact]
30+
public async Task ContextMenu_Ok()
31+
{
32+
var clicked = false;
33+
var cut = Context.RenderComponent<ContextMenuZone>(pb =>
34+
{
35+
pb.AddChildContent<Tab>(pb =>
36+
{
37+
pb.AddChildContent<TabItem>(pb =>
38+
{
39+
pb.Add(a => a.Text, "Tab1");
40+
pb.Add(a => a.Url, "/Index");
41+
pb.Add(a => a.Closable, true);
42+
pb.Add(a => a.Icon, "fa-solid fa-font-awesome");
43+
pb.Add(a => a.ChildContent, "Tab1-Content");
44+
});
45+
});
46+
pb.AddChildContent<ContextMenu>(pb =>
47+
{
48+
pb.AddChildContent<ContextMenuItem>(pb =>
49+
{
50+
pb.Add(a => a.Text, "test-close");
51+
pb.Add(a => a.OnClick, (context, item) =>
52+
{
53+
clicked = true;
54+
if (item is TabItem tabItem)
55+
{
56+
57+
}
58+
return Task.CompletedTask;
59+
});
60+
});
61+
});
62+
});
63+
64+
var menuItem = cut.Find(".tabs-item");
65+
await cut.InvokeAsync(() => menuItem.ContextMenu());
66+
67+
var item = cut.Find(".dropdown-menu .dropdown-item");
68+
await cut.InvokeAsync(() => item.Click());
69+
Assert.True(clicked);
70+
}
71+
2872
[Fact]
2973
public void ToolbarTemplate_Ok()
3074
{

0 commit comments

Comments
 (0)