- @RenderHeaderContent(item)
+ @RenderHeaderItemContent(item)
@if (TabStyle == TabStyle.Chrome)
{
@@ -136,10 +151,10 @@ else
}
;
- RenderFragment RenderHeader(TabItem item) =>
+ RenderFragment RenderHeaderItem(TabItem item) =>
@
@if (!string.IsNullOrEmpty(item.Icon))
{
diff --git a/src/BootstrapBlazor/Components/Tab/Tab.razor.cs b/src/BootstrapBlazor/Components/Tab/Tab.razor.cs
index 231e641607c..187adc45325 100644
--- a/src/BootstrapBlazor/Components/Tab/Tab.razor.cs
+++ b/src/BootstrapBlazor/Components/Tab/Tab.razor.cs
@@ -10,22 +10,18 @@
namespace BootstrapBlazor.Components;
///
-/// Tab 组件
+/// Tab component
///
public partial class Tab : IHandlerException
{
private bool FirstRender { get; set; } = true;
- private static string? GetContentClassString(TabItem item) => CssBuilder.Default("tabs-body-content")
- .AddClass("d-none", !item.IsActive)
- .Build();
-
private string? WrapClassString => CssBuilder.Default("tabs-nav-wrap")
.AddClass("extend", ShouldShowExtendButtons())
.Build();
private static string? GetItemWrapClassString(TabItem item) => CssBuilder.Default("tabs-item-wrap")
- .AddClass("active", item.IsActive && !item.IsDisabled)
+ .AddClass("active", item is { IsActive: true, IsDisabled: false })
.Build();
private string? GetClassString(TabItem item) => CssBuilder.Default("tabs-item")
@@ -58,25 +54,25 @@ public partial class Tab : IHandlerException
private readonly List
_draggedItems = new(50);
///
- /// 获得/设置 TabItem 集合
+ /// Gets the collection of tab items.
///
public IEnumerable Items => TabItems;
private List TabItems => _dragged ? _draggedItems : _items;
///
- /// 获得/设置 是否为排除地址 默认 false
+ /// Gets or sets the excluded link. Default is false.
///
private bool Excluded { get; set; }
///
- /// 获得/设置 是否为卡片样式 默认 false
+ /// Gets or sets whether card style. Default is false.
///
[Parameter]
public bool IsCard { get; set; }
///
- /// 获得/设置 是否为带边框卡片样式 默认 false
+ /// Gets or sets whether border card style. Default is false.
///
[Parameter]
public bool IsBorderCard { get; set; }
@@ -294,6 +290,30 @@ public partial class Tab : IHandlerException
[Parameter]
public TabStyle TabStyle { get; set; }
+ ///
+ /// Gets or sets whether show the toolbar. Default is false.
+ ///
+ [Parameter]
+ public bool ShowToolbar { get; set; }
+
+ ///
+ /// Gets or sets whether show the full screen button. Default is true.
+ ///
+ [Parameter]
+ public bool ShowFullscreenToolbarButton { get; set; } = true;
+
+ ///
+ /// Gets or sets whether show the full screen button. Default is true.
+ ///
+ [Parameter]
+ public bool ShowRefreshToolbarButton { get; set; } = true;
+
+ ///
+ /// Gets or sets the refresh toolbar button icon string. Default is null.
+ ///
+ [Parameter]
+ public string? RefreshToolbarButtonIcon { get; set; }
+
[CascadingParameter]
private Layout? Layout { get; set; }
@@ -330,6 +350,8 @@ public partial class Tab : IHandlerException
private string? DraggableString => AllowDrag ? "true" : null;
+ private readonly ConcurrentDictionary _cache = [];
+
///
///
///
@@ -360,8 +382,16 @@ protected override void OnParametersSet()
NextIcon ??= IconTheme.GetIconByKey(ComponentIcons.TabNextIcon);
DropdownIcon ??= IconTheme.GetIconByKey(ComponentIcons.TabDropdownIcon);
CloseIcon ??= IconTheme.GetIconByKey(ComponentIcons.TabCloseIcon);
+ RefreshToolbarButtonIcon ??= IconTheme.GetIconByKey(ComponentIcons.TabRefreshButtonIcon);
- AdditionalAssemblies ??= new[] { Assembly.GetEntryAssembly()! };
+ if (AdditionalAssemblies is null)
+ {
+ var entryAssembly = Assembly.GetEntryAssembly();
+ if (entryAssembly is not null)
+ {
+ AdditionalAssemblies = [entryAssembly];
+ }
+ }
if (Placement != Placement.Top && TabStyle == TabStyle.Chrome)
{
@@ -805,7 +835,7 @@ public void SetDisabledItem(TabItem item, bool disabled)
}
if (TabItems.Any(i => i.IsActive) == false)
{
- TabItems.Where(i => !i.IsDisabled).FirstOrDefault()?.SetActive(true);
+ TabItems.FirstOrDefault(i => !i.IsDisabled)?.SetActive(true);
}
StateHasChanged();
}
@@ -819,7 +849,7 @@ private RenderFragment RenderTabItemContent(TabItem item) => builder =>
if (item.IsActive)
{
- builder.AddContent(0, item.ChildContent);
+ builder.AddContent(0, item.RenderContent(_cache));
if (IsLazyLoadTabItem)
{
LazyTabCache.AddOrUpdate(item, _ => true, (_, _) => true);
@@ -827,7 +857,7 @@ private RenderFragment RenderTabItemContent(TabItem item) => builder =>
}
else if (!IsLazyLoadTabItem || item.AlwaysLoad || LazyTabCache.TryGetValue(item, out var init) && init)
{
- builder.AddContent(0, item.ChildContent);
+ builder.AddContent(0, item.RenderContent(_cache));
}
};
@@ -876,7 +906,15 @@ public async Task DragItemCallback(int originIndex, int currentIndex)
}
}
- private string? GetIdByTabItem(TabItem item) => (ShowFullScreen && item.ShowFullScreen) ? ComponentIdGenerator.Generate(item) : null;
+ private string? GetIdByTabItem(TabItem item) => ComponentIdGenerator.Generate(item);
+
+ private Task OnRefreshAsync()
+ {
+ // refresh the active tab item
+ var item = TabItems.FirstOrDefault(i => i.IsActive);
+ item.Refresh(_cache);
+ return Task.CompletedTask;
+ }
///
///
diff --git a/src/BootstrapBlazor/Components/Tab/Tab.razor.scss b/src/BootstrapBlazor/Components/Tab/Tab.razor.scss
index 35acdabb21a..82fa992a942 100644
--- a/src/BootstrapBlazor/Components/Tab/Tab.razor.scss
+++ b/src/BootstrapBlazor/Components/Tab/Tab.razor.scss
@@ -197,6 +197,7 @@
}
.tabs .tabs-body {
+ background-color: var(--bs-body-bg);
padding: var(--bb-tabs-body-padding);
flex: 1;
}
@@ -232,6 +233,8 @@
.tabs .tabs-item-fix {
height: 100%;
flex: 1;
+ width: 1%;
+ min-width: 0;
border: 1px solid var(--bs-border-color);
border-width: 0 0 1px 0;
}
@@ -643,3 +646,47 @@
}
}
}
+
+.tabs {
+ &:not(.tabs-vertical) > .tabs-header .tabs-nav-toolbar {
+ display: flex;
+ }
+
+ &.tabs-bottom > .tabs-header {
+ .tabs-nav-toolbar {
+ border-top: 1px solid var(--bs-border-color);
+ }
+ }
+
+ &:not(.tabs-bottom) > .tabs-header {
+ .tabs-nav-toolbar {
+ border-bottom: 1px solid var(--bs-border-color);
+ }
+ }
+
+ > .tabs-header {
+ .tabs-nav-toolbar {
+ display: none;
+ align-items: center;
+ height: 100%;
+ padding: 3px 0.5rem;
+
+ .tabs-nav-toolbar-button {
+ cursor: pointer;
+ padding: 0 .75rem;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ border-radius: var(--bs-border-radius);
+
+ &:not(.disabled):not(:disabled):hover {
+ background-color: var(--bb-tabs-item-hover-bg-color);
+ }
+
+ .btn {
+ padding: 0;
+ }
+ }
+ }
+ }
+}
diff --git a/src/BootstrapBlazor/Components/Tab/TabItemContent.cs b/src/BootstrapBlazor/Components/Tab/TabItemContent.cs
new file mode 100644
index 00000000000..249867bc926
--- /dev/null
+++ b/src/BootstrapBlazor/Components/Tab/TabItemContent.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace BootstrapBlazor.Components;
+
+internal class TabItemContent : IComponent
+{
+ ///
+ /// Gets or sets the component content. Default is null
+ ///
+ [Parameter, NotNull]
+ public TabItem? Item { get; set; }
+
+ ///
+ /// Gets instrance
+ ///
+ [Inject]
+ [NotNull]
+ private IComponentIdGenerator? ComponentIdGenerator { get; set; }
+
+ private RenderHandle _renderHandle;
+
+ void IComponent.Attach(RenderHandle renderHandle)
+ {
+ _renderHandle = renderHandle;
+ }
+
+ Task IComponent.SetParametersAsync(ParameterView parameters)
+ {
+ parameters.SetParameterProperties(this);
+
+ RenderContent();
+ return Task.CompletedTask;
+ }
+
+ private void RenderContent()
+ {
+ _renderHandle.Render(BuildRenderTree);
+ }
+
+ private object _key = new();
+
+ private void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenElement(0, "div");
+ builder.SetKey(_key);
+ builder.AddAttribute(5, "class", ClassString);
+ builder.AddAttribute(6, "id", ComponentIdGenerator.Generate(Item));
+ builder.AddContent(10, Item.ChildContent);
+ builder.CloseElement();
+ }
+
+ private string? ClassString => CssBuilder.Default("tabs-body-content")
+ .AddClass("d-none", !Item.IsActive)
+ .Build();
+
+ ///
+ /// Render method
+ ///
+ public void Render()
+ {
+ _key = new object();
+ RenderContent();
+ }
+}
diff --git a/src/BootstrapBlazor/Components/Tab/TabToolbarRefreshButton.razor b/src/BootstrapBlazor/Components/Tab/TabToolbarRefreshButton.razor
new file mode 100644
index 00000000000..69e9e9577d9
--- /dev/null
+++ b/src/BootstrapBlazor/Components/Tab/TabToolbarRefreshButton.razor
@@ -0,0 +1,3 @@
+@namespace BootstrapBlazor.Components
+
+
diff --git a/src/BootstrapBlazor/Components/Tab/TabToolbarRefreshButton.razor.cs b/src/BootstrapBlazor/Components/Tab/TabToolbarRefreshButton.razor.cs
new file mode 100644
index 00000000000..a77d56e80e1
--- /dev/null
+++ b/src/BootstrapBlazor/Components/Tab/TabToolbarRefreshButton.razor.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+namespace BootstrapBlazor.Components;
+
+///
+/// TabToolbarRefreshButton component
+///
+public partial class TabToolbarRefreshButton
+{
+ ///
+ /// Gets or sets the button icon string. Default is null.
+ ///
+ [Parameter]
+ public string? Icon { get; set; }
+
+ ///
+ /// Gets or sets the button click event handler. Default is null.
+ ///
+ [Parameter]
+ public Func? OnClickAsync { get; set; }
+
+ private async Task OnClick()
+ {
+ if (OnClickAsync != null)
+ {
+ await OnClickAsync();
+ }
+ }
+}
diff --git a/src/BootstrapBlazor/Enums/ComponentIcons.cs b/src/BootstrapBlazor/Enums/ComponentIcons.cs
index 9ccde6692d8..98aaa444683 100644
--- a/src/BootstrapBlazor/Enums/ComponentIcons.cs
+++ b/src/BootstrapBlazor/Enums/ComponentIcons.cs
@@ -705,6 +705,11 @@ public enum ComponentIcons
///
TabCloseIcon,
+ ///
+ /// Tab 组件 RefreshToolbarButtonIcon 属性图标
+ ///
+ TabRefreshButtonIcon,
+
///
/// Timer 组件 Icon 属性图标
///
diff --git a/src/BootstrapBlazor/Extensions/TabItemExtensions.cs b/src/BootstrapBlazor/Extensions/TabItemExtensions.cs
new file mode 100644
index 00000000000..75abc1a3391
--- /dev/null
+++ b/src/BootstrapBlazor/Extensions/TabItemExtensions.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the Apache 2.0 License
+// See the LICENSE file in the project root for more information.
+// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
+
+using System.Collections.Concurrent;
+
+namespace BootstrapBlazor.Components;
+
+///
+/// TabItem Extension
+///
+internal static class TabItemExtensions
+{
+ public static RenderFragment RenderContent(this TabItem item, ConcurrentDictionary cache) => builder =>
+ {
+ builder.OpenComponent(0);
+ builder.AddAttribute(10, nameof(TabItemContent.Item), item);
+ builder.AddComponentReferenceCapture(20, content =>
+ {
+ var tabItemContent = (TabItemContent)content;
+ cache.AddOrUpdate(item, tabItemContent, (_, _) => tabItemContent);
+ });
+ builder.CloseComponent();
+ };
+
+ public static void Refresh(this TabItem? item, ConcurrentDictionary cache)
+ {
+ if (item is not null && cache.TryGetValue(item, out var content))
+ {
+ content.Render();
+ }
+ }
+}
diff --git a/src/BootstrapBlazor/Icons/BootstrapIcons.cs b/src/BootstrapBlazor/Icons/BootstrapIcons.cs
index 8a4817671ee..51cf458b9f6 100644
--- a/src/BootstrapBlazor/Icons/BootstrapIcons.cs
+++ b/src/BootstrapBlazor/Icons/BootstrapIcons.cs
@@ -136,6 +136,7 @@ internal static class BootstrapIcons
{ ComponentIcons.TabNextIcon, "bi bi-chevron-right" },
{ ComponentIcons.TabDropdownIcon, "bi bi-chevron-down" },
{ ComponentIcons.TabCloseIcon, "bi bi-x" },
+ { ComponentIcons.TabRefreshButtonIcon, "bi bi-arrow-clockwise" },
{ ComponentIcons.TableColumnToolboxIcon, "bi bi-gear" },
diff --git a/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs b/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs
index 3fb9db7a467..6d36a912bd7 100644
--- a/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs
+++ b/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs
@@ -134,6 +134,7 @@ internal static class FontAwesomeIcons
{ ComponentIcons.TabNextIcon, "fa-solid fa-chevron-right" },
{ ComponentIcons.TabDropdownIcon, "fa-solid fa-chevron-down" },
{ ComponentIcons.TabCloseIcon, "fa-solid fa-xmark" },
+ { ComponentIcons.TabRefreshButtonIcon, "fa-solid fa-rotate-right" },
{ ComponentIcons.TableColumnToolboxIcon, "fa-solid fa-gear" },
diff --git a/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs b/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs
index d724caf5efb..7792c063da5 100644
--- a/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs
+++ b/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs
@@ -77,8 +77,8 @@ internal static class MaterialDesignIcons
{ ComponentIcons.ImagePreviewNextIcon, "mdi mdi-chevron-right" },
{ ComponentIcons.ImagePreviewMinusIcon, "mdi mdi-magnify-minus-outline" },
{ ComponentIcons.ImagePreviewPlusIcon, "mdi mdi-magnify-plus-outline" },
- { ComponentIcons.ImagePreviewRotateLeftIcon, "mdi mdi-file-rotate-left-outline" },
- { ComponentIcons.ImagePreviewRotateRightIcon, "mdi mdi-file-rotate-right-outline" },
+ { ComponentIcons.ImagePreviewRotateLeftIcon, "mdi mdi-restore" },
+ { ComponentIcons.ImagePreviewRotateRightIcon, "mdi mdi-reload" },
{ ComponentIcons.ImageViewerFileIcon, "mdi mdi-file-image-outline" },
@@ -136,6 +136,7 @@ internal static class MaterialDesignIcons
{ ComponentIcons.TabNextIcon, "mdi mdi-chevron-right" },
{ ComponentIcons.TabDropdownIcon, "mdi mdi-chevron-down" },
{ ComponentIcons.TabCloseIcon, "mdi mdi-close" },
+ { ComponentIcons.TabRefreshButtonIcon, "mdi mdi-reload" },
{ ComponentIcons.TableColumnToolboxIcon, "mdi mdi-cog" },
diff --git a/test/UnitTest/Components/LayoutTest.cs b/test/UnitTest/Components/LayoutTest.cs
index 1b915fa9702..179c8e2699a 100644
--- a/test/UnitTest/Components/LayoutTest.cs
+++ b/test/UnitTest/Components/LayoutTest.cs
@@ -234,7 +234,7 @@ public void UseTabSet_Layout()
});
var nav = cut.Services.GetRequiredService();
nav.NavigateTo("/Binder");
- cut.WaitForAssertion(() => cut.Contains("Binder
"));
+ cut.Contains("Binder");
}
[Fact]
@@ -268,7 +268,7 @@ public void UseTabSet_Menus()
});
var nav = cut.Services.GetRequiredService();
nav.NavigateTo("/Binder");
- cut.WaitForAssertion(() => cut.Contains("Binder
"));
+ cut.Contains("Binder");
}
[Fact]
diff --git a/test/UnitTest/Components/TabTest.cs b/test/UnitTest/Components/TabTest.cs
index e259b50552e..1e6085e55a9 100644
--- a/test/UnitTest/Components/TabTest.cs
+++ b/test/UnitTest/Components/TabTest.cs
@@ -353,7 +353,7 @@ public void Menus_Ok()
});
var nav = cut.Services.GetRequiredService();
nav.NavigateTo("/Binder");
- cut.Contains("Binder
");
+ cut.Contains("Binder");
var items = cut.Instance.Items;
Assert.Equal(2, items.Count());
@@ -542,10 +542,9 @@ public void IsOnlyRenderActiveTab_True()
});
Assert.Contains("Tab1-Content", cut.Markup);
Assert.DoesNotContain("Tab2-Content", cut.Markup);
- Assert.DoesNotContain("tabs-body-content", cut.Markup);
// 提高代码覆盖率
- cut.InvokeAsync(() => cut.Instance.CloseOtherTabs());
+ cut.InvokeAsync(cut.Instance.CloseOtherTabs);
}
[Fact]
@@ -942,6 +941,13 @@ public async Task FullScreen_Ok()
var button = cut.Find(".btn-fs");
await cut.InvokeAsync(() => button.Click());
+
+ var tab = cut.FindComponent();
+ tab.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.ShowFullScreen, false);
+ });
+ cut.DoesNotContain("btn btn-fs");
}
[Fact]
@@ -965,6 +971,57 @@ public void BeforeNavigatorTemplate_Ok()
cut.Contains("after-navigator-template");
}
+ [Fact]
+ public async Task ShowToolbar_Ok()
+ {
+ var cut = Context.RenderComponent(pb =>
+ {
+ pb.AddChildContent(pb =>
+ {
+ pb.Add(a => a.ShowToolbar, false);
+ pb.AddChildContent(pb =>
+ {
+ pb.Add(a => a.ShowFullScreen, true);
+ pb.Add(a => a.Text, "Text1");
+ pb.Add(a => a.ChildContent, builder => builder.AddContent(0, "Test1"));
+ });
+ });
+ });
+ cut.DoesNotContain("tabs-nav-toolbar");
+
+ var tab = cut.FindComponent();
+ tab.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.ShowToolbar, true);
+ });
+ cut.Contains("tabs-nav-toolbar");
+ cut.Contains("tabs-nav-toolbar-refresh");
+ cut.Contains("tabs-nav-toolbar-fs");
+
+ // 点击刷新按钮
+ var button = cut.Find(".tabs-nav-toolbar-refresh > i");
+ await cut.InvokeAsync(() => button.Click());
+
+ tab.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.ShowRefreshToolbarButton, false);
+ });
+ cut.DoesNotContain("tabs-nav-toolbar-refresh");
+
+ tab.SetParametersAndRender(pb =>
+ {
+ pb.Add(a => a.ShowFullscreenToolbarButton, false);
+ });
+ cut.DoesNotContain("tabs-nav-toolbar-fs");
+
+ // 利用反射提高代码覆盖率
+ var type = Type.GetType("BootstrapBlazor.Components.TabItemExtensions, BootstrapBlazor");
+ Assert.NotNull(type);
+ var mi = type.GetMethod("Refresh", BindingFlags.Static | BindingFlags.Public);
+ Assert.NotNull(mi);
+ mi.Invoke(null, [null, null]);
+ }
+
class DisableTabItemButton : ComponentBase
{
[CascadingParameter, NotNull]