diff --git a/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj b/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj index ec6cce29417..acf0224f659 100644 --- a/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj +++ b/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/BootstrapBlazor.Server/Components/Layout/ComponentLayout.razor b/src/BootstrapBlazor.Server/Components/Layout/ComponentLayout.razor index 3fa93db5737..b8f3cb9c85e 100644 --- a/src/BootstrapBlazor.Server/Components/Layout/ComponentLayout.razor +++ b/src/BootstrapBlazor.Server/Components/Layout/ComponentLayout.razor @@ -30,7 +30,7 @@
- + @Body diff --git a/src/BootstrapBlazor.Server/Components/Samples/Tabs.razor b/src/BootstrapBlazor.Server/Components/Samples/Tabs.razor index be0c0544c1e..9076971d8ed 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Tabs.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/Tabs.razor @@ -423,7 +423,21 @@ private void Navigation() - + + +
@Localizer["TabItem1Content"]
+
+ +
@Localizer["TabItem2Content"]
+
+ +
@Localizer["TabItem3Content"]
+
+
+
+ + +
@Localizer["TabItem1Content"]
@@ -437,7 +451,7 @@ private void Navigation()
- +
@Localizer["TabItem1Content"]
@@ -454,9 +468,10 @@ private void Navigation()
- +
@Localizer["TabItem1Content"]
+
@Localizer["TabItem2Content"]
diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index 40666cb0b8f..9e81c4b15b8 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@  - 9.5.0-beta10 + 9.5.0-beta11 diff --git a/src/BootstrapBlazor/Components/Tab/Tab.razor b/src/BootstrapBlazor/Components/Tab/Tab.razor index 8d35f24b3a6..c5ddbba0d20 100644 --- a/src/BootstrapBlazor/Components/Tab/Tab.razor +++ b/src/BootstrapBlazor/Components/Tab/Tab.razor @@ -51,11 +51,11 @@ else } else if (item.IsDisabled) { - @RenderDisabledHeader(item) + @RenderDisabledHeaderItem(item) } else { - @RenderHeader(item) + @RenderHeaderItem(item) } } @if (IsCard || IsBorderCard) @@ -73,6 +73,23 @@ else { @ButtonTemplate } + @if (ShowToolbar) + { +
+ @if (ShowRefreshToolbarButton) + { +
+ +
+ } + @if (ShowFullscreenToolbarButton) + { +
+ +
+ } +
+ } @if (ShowNavigatorButtons) { @@ -108,9 +125,7 @@ else { foreach (var item in Items) { -
- @RenderTabItem(item) -
+ @RenderTabItem(item) } }
@@ -124,10 +139,10 @@ else @RenderTabItemContent(item) ; - RenderFragment RenderDisabledHeader(TabItem item) => + RenderFragment RenderDisabledHeaderItem(TabItem item) => @
@if (TabStyle == TabStyle.Chrome) { @@ -136,10 +151,10 @@ else }
; - RenderFragment RenderHeader(TabItem item) => + RenderFragment RenderHeaderItem(TabItem item) => @
- @RenderHeaderContent(item) + @RenderHeaderItemContent(item) @if (TabStyle == TabStyle.Chrome) { @@ -148,7 +163,7 @@ else }
; - RenderFragment RenderHeaderContent(TabItem item) => + RenderFragment RenderHeaderItemContent(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]