+ @RenderTab
+}
+
+@code {
+ RenderFragment RenderTab =>
+ @
-}
-
-@code {
+ ;
+
RenderFragment RenderTabItem => item =>
@
@RenderTabItemContent(item)
;
- RenderFragment RenderDisabledHeaderItem(TabItem item) =>
+ RenderFragment RenderDisabledHeaderItem(TabItem item) =>
@
@if (BeforeNavigatorTemplate != null)
@@ -155,18 +181,17 @@ else
}
-
-
+
+ /// Gets or sets whether enable tab context menu. Default is false.
+ ///
+ [Parameter]
+ public bool ShowContextMenu { get; set; }
+
+ ///
+ /// Gets or sets the template of before context menu. Default is null.
+ ///
+ [Parameter]
+ public RenderFragment? BeforeContextMenuTemplate { get; set; }
+
+ ///
+ /// Gets or sets the template of context menu. Default is null.
+ ///
+ [Parameter]
+ public RenderFragment? ContextMenuTemplate { get; set; }
+
+ ///
+ /// Gets or sets the icon of tab item context menu refresh button. Default is null.
+ ///
+ [Parameter]
+ public string? ContextMenuRefreshIcon { get; set; }
+
+ ///
+ /// Gets or sets the icon of tab item context menu close button. Default is null.
+ ///
+ [Parameter]
+ public string? ContextMenuCloseIcon { get; set; }
+
+ ///
+ /// Gets or sets the icon of tab item context menu close other button. Default is null.
+ ///
+ [Parameter]
+ public string? ContextMenuCloseOtherIcon { get; set; }
+
+ ///
+ /// Gets or sets the icon of tab item context menu close all button. Default is null.
+ ///
+ [Parameter]
+ public string? ContextMenuCloseAllIcon { get; set; }
+
+ ///
+ /// Gets or sets before popup context menu callback. Default is null.
+ ///
+ [Parameter]
+ public Func>? OnBeforeShowContextMenu { get; set; }
+
[CascadingParameter]
private Layout? Layout { get; set; }
@@ -385,8 +433,7 @@ public partial class Tab : IHandlerException
[Inject, NotNull]
private DialogService? DialogService { get; set; }
- [CascadingParameter]
- private ContextMenuZone? ContextMenuZone { get; set; }
+ private ContextMenuZone? _contextMenuZone;
private ConcurrentDictionary LazyTabCache { get; } = new();
@@ -400,7 +447,7 @@ public partial class Tab : IHandlerException
private readonly ConcurrentDictionary _cache = [];
- private bool IsPreventDefault => ContextMenuZone != null;
+ private bool IsPreventDefault => _contextMenuZone != null;
///
///
@@ -439,6 +486,11 @@ protected override void OnParametersSet()
CloseIcon ??= IconTheme.GetIconByKey(ComponentIcons.TabCloseIcon);
RefreshToolbarButtonIcon ??= IconTheme.GetIconByKey(ComponentIcons.TabRefreshButtonIcon);
+ ContextMenuRefreshIcon ??= IconTheme.GetIconByKey(ComponentIcons.TabContextMenuRefreshIcon);
+ ContextMenuCloseIcon ??= IconTheme.GetIconByKey(ComponentIcons.TabContextMenuCloseIcon);
+ ContextMenuCloseOtherIcon ??= IconTheme.GetIconByKey(ComponentIcons.TabContextMenuCloseOtherIcon);
+ ContextMenuCloseAllIcon ??= IconTheme.GetIconByKey(ComponentIcons.TabContextMenuCloseAllIcon);
+
if (AdditionalAssemblies is null)
{
var entryAssembly = Assembly.GetEntryAssembly();
@@ -890,9 +942,10 @@ public void SetDisabledItem(TabItem item, bool disabled)
{
item.SetActive(false);
}
- if (TabItems.Any(i => i.IsActive) == false)
+ if (TabItems.Find(i => i.IsActive) == null)
{
- TabItems.FirstOrDefault(i => !i.IsDisabled)?.SetActive(true);
+ var tabItem = TabItems.Find(i => !i.IsDisabled);
+ tabItem?.SetActive(true);
}
StateHasChanged();
}
@@ -990,6 +1043,55 @@ public async Task Refresh(TabItem item)
}
}
+ private async Task OnRefresh(ContextMenuItem item, object? context)
+ {
+ if (context is TabItem tabItem)
+ {
+ await Refresh(tabItem);
+ }
+ }
+
+ private async Task OnClose(ContextMenuItem item, object? context)
+ {
+ if (context is TabItem tabItem)
+ {
+ await RemoveTab(tabItem);
+ }
+ }
+
+ private Task OnCloseOther(ContextMenuItem item, object? context)
+ {
+ if (context is TabItem tabItem)
+ {
+ ActiveTab(tabItem);
+ }
+ CloseOtherTabs();
+ return Task.CompletedTask;
+ }
+
+ private Task OnCloseAll(ContextMenuItem item, object? context)
+ {
+ CloseAllTabs();
+ return Task.CompletedTask;
+ }
+
+ private async Task OnContextMenu(MouseEventArgs e, TabItem item)
+ {
+ if (_contextMenuZone != null)
+ {
+ var show = true;
+ if (OnBeforeShowContextMenu != null)
+ {
+ show = await OnBeforeShowContextMenu(item);
+ }
+
+ if (show)
+ {
+ await _contextMenuZone.OnContextMenu(e, item);
+ }
+ }
+ }
+
///
///
///
@@ -1003,12 +1105,4 @@ protected override async ValueTask DisposeAsync(bool disposing)
ErrorLogger?.UnRegister(this);
}
}
-
- private async Task OnContextMenu(MouseEventArgs e, TabItem item)
- {
- if (ContextMenuZone != null)
- {
- await ContextMenuZone.OnContextMenu(e, item);
- }
- }
}
diff --git a/src/BootstrapBlazor/Components/Tab/Tab.razor.scss b/src/BootstrapBlazor/Components/Tab/Tab.razor.scss
index 0de01177aad..8865bff1506 100644
--- a/src/BootstrapBlazor/Components/Tab/Tab.razor.scss
+++ b/src/BootstrapBlazor/Components/Tab/Tab.razor.scss
@@ -700,6 +700,7 @@
display: flex;
align-items: center;
border-radius: var(--bs-border-radius);
+ user-select: none;
&:not(.disabled):not(:disabled):hover {
background-color: var(--bb-tabs-item-hover-bg-color);
diff --git a/src/BootstrapBlazor/Locales/en.json b/src/BootstrapBlazor/Locales/en.json
index 67168d6fa7f..847e2e1a738 100644
--- a/src/BootstrapBlazor/Locales/en.json
+++ b/src/BootstrapBlazor/Locales/en.json
@@ -105,11 +105,7 @@
"TooltipText": "Go top"
},
"BootstrapBlazor.Components.Layout": {
- "TooltipText": "Click to Expand/Collapse sidebar",
- "ContextRefresh": "Refresh",
- "ContextClose": "Close",
- "ContextCloseOther": "Close Other Tabs",
- "ContextCloseAll": "Close All Tabs"
+ "TooltipText": "Click to Expand/Collapse sidebar"
},
"BootstrapBlazor.Components.Logout": {
"PrefixDisplayNameText": "Welcome",
@@ -119,7 +115,7 @@
"Text": "Logout"
},
"BootstrapBlazor.Components.Menu": {
- "InvalidOperationExceptionMessage": "Sidemenu component cannot be used independently. Please use Menu component to set IsVertical = true"
+ "InvalidOperationExceptionMessage": "SideMenu component cannot be used independently. Please use Menu component to set IsVertical = true"
},
"BootstrapBlazor.Components.ModalDialog": {
"CloseButtonText": "Close",
@@ -182,7 +178,11 @@
"FullscreenToolbarTooltipText": "Fullscreen",
"PrevTabNavLinkTooltipText": "Prev Tab",
"NextTabNavLinkTooltipText": "Next Tab",
- "CloseTabNavLinkTooltipText": "Close"
+ "CloseTabNavLinkTooltipText": "Close",
+ "ContextRefresh": "Refresh",
+ "ContextClose": "Close",
+ "ContextCloseOther": "Close Other Tabs",
+ "ContextCloseAll": "Close All Tabs"
},
"BootstrapBlazor.Components.MultiFilter": {
"MultiFilterSearchPlaceHolderText": "Please enter ...",
diff --git a/src/BootstrapBlazor/Locales/zh.json b/src/BootstrapBlazor/Locales/zh.json
index 6dceb50ef15..22af6c45ff4 100644
--- a/src/BootstrapBlazor/Locales/zh.json
+++ b/src/BootstrapBlazor/Locales/zh.json
@@ -105,11 +105,7 @@
"TooltipText": "返回顶端"
},
"BootstrapBlazor.Components.Layout": {
- "TooltipText": "点击展开收缩左侧菜单",
- "ContextRefresh": "刷新",
- "ContextClose": "关闭",
- "ContextCloseOther": "关闭其他",
- "ContextCloseAll": "关闭全部"
+ "TooltipText": "点击展开收缩左侧菜单"
},
"BootstrapBlazor.Components.Logout": {
"PrefixDisplayNameText": "欢迎",
@@ -182,7 +178,11 @@
"FullscreenToolbarTooltipText": "全屏",
"PrevTabNavLinkTooltipText": "上一个标签",
"NextTabNavLinkTooltipText": "下一个标签",
- "CloseTabNavLinkTooltipText": "关闭"
+ "CloseTabNavLinkTooltipText": "关闭",
+ "ContextRefresh": "刷新",
+ "ContextClose": "关闭",
+ "ContextCloseOther": "关闭其他",
+ "ContextCloseAll": "关闭全部"
},
"BootstrapBlazor.Components.MultiFilter": {
"MultiFilterSearchPlaceHolderText": "请输入 ...",
diff --git a/test/UnitTest/Components/LayoutTest.cs b/test/UnitTest/Components/LayoutTest.cs
index e71b325142a..0d129c591a8 100644
--- a/test/UnitTest/Components/LayoutTest.cs
+++ b/test/UnitTest/Components/LayoutTest.cs
@@ -57,6 +57,14 @@ public async Task TabStyle_Ok()
cut.SetParametersAndRender(pb => pb.Add(a => a.TabContextMenuTemplate, tab => b => b.AddContent(0, "test-tab-context-menu")));
cut.Contains("test-tab-context-menu");
+ cut.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.TabContextMenuRefreshIcon, "test-tab-refresh-icon");
+ pb.Add(a => a.TabContextMenuCloseIcon, "test-tab-close-icon");
+ pb.Add(a => a.TabContextMenuCloseAllIcon, "test-tab-close-all-icon");
+ pb.Add(a => a.TabContextMenuCloseOtherIcon, "test-tab-close-other-icon");
+ });
+
// test context menu onclick event handler
var tab = cut.Find(".tabs-item");
await cut.InvokeAsync(() => tab.ContextMenu());
@@ -66,6 +74,22 @@ public async Task TabStyle_Ok()
{
await cut.InvokeAsync(() => button.Click());
}
+ cut.Contains("test-tab-refresh-icon");
+ cut.Contains("test-tab-close-icon");
+ cut.Contains("test-tab-close-all-icon");
+ cut.Contains("test-tab-close-other-icon");
+
+ var show = false;
+ cut.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.OnBeforeShowContextMenu, item =>
+ {
+ show = true;
+ return Task.FromResult(true);
+ });
+ });
+ await cut.InvokeAsync(() => tab.ContextMenu());
+ Assert.True(show);
}
[Fact]
diff --git a/test/UnitTest/Components/TabTest.cs b/test/UnitTest/Components/TabTest.cs
index b78809bb9ed..99df42b4fc2 100644
--- a/test/UnitTest/Components/TabTest.cs
+++ b/test/UnitTest/Components/TabTest.cs
@@ -28,13 +28,14 @@ protected override void ConfigureServices(IServiceCollection services)
[Fact]
public async Task ContextMenu_Ok()
{
- var clicked = false;
var cut = Context.RenderComponent(pb =>
{
pb.AddChildContent(pb =>
{
+ pb.Add(a => a.ShowContextMenu, true);
pb.AddChildContent(pb =>
{
+ pb.Add(a => a.IsDisabled, true);
pb.Add(a => a.Text, "Tab1");
pb.Add(a => a.Url, "/Index");
pb.Add(a => a.Closable, true);
@@ -42,30 +43,13 @@ public async Task ContextMenu_Ok()
pb.Add(a => a.ChildContent, "Tab1-Content");
});
});
- pb.AddChildContent(pb =>
- {
- pb.AddChildContent(pb =>
- {
- pb.Add(a => a.Text, "test-close");
- pb.Add(a => a.OnClick, (context, item) =>
- {
- clicked = true;
- if (item is TabItem tabItem)
- {
-
- }
- return Task.CompletedTask;
- });
- });
- });
});
var menuItem = cut.Find(".tabs-item");
await cut.InvokeAsync(() => menuItem.ContextMenu());
var item = cut.Find(".dropdown-menu .dropdown-item");
- await cut.InvokeAsync(() => item.Click());
- Assert.True(clicked);
+ Assert.NotNull(item);
}
[Fact]
@@ -496,13 +480,16 @@ public async Task IsDisabled_Ok()
pb.AddChildContent();
});
});
- Assert.Contains("", cut.Markup);
+ var tabItems = cut.FindAll(".tabs-item");
+ Assert.Contains("tabs-item disabled", tabItems[0].OuterHtml);
+ Assert.DoesNotContain("tabs-item disabled", tabItems[1].OuterHtml);
var button = cut.FindComponent();
Assert.NotNull(button);
await cut.InvokeAsync(() => button.Instance.OnDisabledTabItem());
- Assert.Contains("", cut.Markup);
+ tabItems = cut.FindAll(".tabs-item");
+ Assert.Contains("tabs-item disabled", tabItems[1].OuterHtml);
}
[Fact]
@@ -545,7 +532,6 @@ public async Task TabStyle_Chrome_Ok()
var button = cut.FindComponent();
Assert.NotNull(button);
await cut.InvokeAsync(() => button.Instance.OnDisabledTabItem());
- cut.Contains("");
// trigger click
var link = cut.Find("a");
OnContextMenu(e, item)" @oncontextmenu:preventDefault="IsPreventDefault">
@RenderHeaderItemContent(item)
@if (TabStyle == TabStyle.Chrome)
diff --git a/src/BootstrapBlazor/Components/Tab/Tab.razor.cs b/src/BootstrapBlazor/Components/Tab/Tab.razor.cs
index 8e9443e82d8..21610165ab0 100644
--- a/src/BootstrapBlazor/Components/Tab/Tab.razor.cs
+++ b/src/BootstrapBlazor/Components/Tab/Tab.razor.cs
@@ -359,6 +359,54 @@ public partial class Tab : IHandlerException
[Parameter]
public string? CloseTabNavLinkTooltipText { get; set; }
+ /// Text1
Text2
Text2