diff --git a/src/BootstrapBlazor/Components/Tab/Tab.razor.cs b/src/BootstrapBlazor/Components/Tab/Tab.razor.cs index 98dc9c060bc9bd961d444fc1364231170ce16b1b..00183e36755b850b08b3d4e78d43d78c9b64b6b6 100644 --- a/src/BootstrapBlazor/Components/Tab/Tab.razor.cs +++ b/src/BootstrapBlazor/Components/Tab/Tab.razor.cs @@ -170,13 +170,6 @@ public partial class Tab [NotNull] public string? CloseCurrentTabText { get; set; } - /// - /// 获得/设置 空白 Tab 显示文字 - /// - [Parameter] - [NotNull] - public string? NullTabText { get; set; } - /// /// 获得/设置 关闭所有 TabItem 菜单文本 /// @@ -256,44 +249,24 @@ public partial class Tab } } - private bool CheckUrl(string url) - { - var ret = false; - foreach (var rule in ExcludeUrls ?? Enumerable.Empty()) - { - var checkUrl = rule.TrimStart('/'); - var startIndex = checkUrl.IndexOf("?"); - if (startIndex > 0) - { - checkUrl = checkUrl[..startIndex]; - if (url.StartsWith(checkUrl, StringComparison.OrdinalIgnoreCase)) - { - ret = true; - break; - } - } - else - { - if (url.Equals(checkUrl, StringComparison.OrdinalIgnoreCase)) - { - ret = true; - break; - } - } - } - return ret; - } - private void AddTabByUrl() { var requestUrl = Navigator.ToBaseRelativePath(Navigator.Uri); // 判断是否排除 - Excluded = CheckUrl(requestUrl); - + var urls = ExcludeUrls ?? Enumerable.Empty(); + if (requestUrl == "") + { + Excluded = urls.Any(u => u == "" || u == "/"); + } + else + { + Excluded = urls.Any(u => u != "/" && requestUrl.StartsWith(u.TrimStart('/'), StringComparison.OrdinalIgnoreCase)); + } if (!Excluded) { - var tab = Items.FirstOrDefault(tab => tab.Url?.Equals(requestUrl, StringComparison.OrdinalIgnoreCase) ?? false); + // 地址相同参数不同需要重新渲染 TabItem + var tab = Items.FirstOrDefault(tab => tab.Url.TrimStart('/').Equals(requestUrl, StringComparison.OrdinalIgnoreCase)); if (tab != null) { ActiveTabItem(tab); @@ -366,7 +339,7 @@ public partial class Tab item = Items.ElementAt(index); if (ClickTabToNavigation) { - Navigator.NavigateTo(item.Url!); + Navigator.NavigateTo(item.Url); } else { @@ -404,7 +377,7 @@ public partial class Tab if (ClickTabToNavigation) { - Navigator.NavigateTo(item.Url!); + Navigator.NavigateTo(item.Url); } else { @@ -523,29 +496,16 @@ public partial class Tab { var text = Options.Text; var icon = Options.Icon ?? string.Empty; - var active = Options.IsActive ?? true; - var closable = Options.Closable ?? true; + var active = Options.IsActive; + var closable = Options.Closable; Options.Reset(); parameters.Add(nameof(TabItem.Url), url); parameters.Add(nameof(TabItem.Icon), icon); parameters.Add(nameof(TabItem.Closable), closable); parameters.Add(nameof(TabItem.IsActive), active); - parameters.Add(nameof(TabItem.Text), GetTabText(text, context.Segments)); - } - } - - private string GetTabText(string? text, string[]? segments) - { - if (NullTabText == null) - { - var t = Localizer[nameof(NullTabText)]; - if (!t.ResourceNotFound) - { - NullTabText = t.Value; - } + parameters.Add(nameof(TabItem.Text), text); } - return text ?? NullTabText ?? segments?.FirstOrDefault() ?? ""; } /// @@ -596,7 +556,7 @@ public partial class Tab { if (ClickTabToNavigation) { - Navigator.NavigateTo(activeItem.Url!); + Navigator.NavigateTo(activeItem.Url); } else { diff --git a/src/BootstrapBlazor/Components/Tab/TabItem.cs b/src/BootstrapBlazor/Components/Tab/TabItem.cs index 3617c51b5b935015d363fc78d1bea1e5e2de3d40..726ab95beff5efe7ec9e8f8a3358dc7adebc84cf 100644 --- a/src/BootstrapBlazor/Components/Tab/TabItem.cs +++ b/src/BootstrapBlazor/Components/Tab/TabItem.cs @@ -21,6 +21,7 @@ public class TabItem : ComponentBase /// 获得/设置 请求地址 /// [Parameter] + [NotNull] public string? Url { get; set; } /// @@ -72,6 +73,7 @@ public class TabItem : ComponentBase { base.OnInitialized(); + Url ??= ""; TabSet?.AddItem(this); } diff --git a/src/BootstrapBlazor/Extensions/NavigationManagerExtensions.cs b/src/BootstrapBlazor/Extensions/NavigationManagerExtensions.cs index 860be33770e921da34dc784eb407d62e16b15a18..3760720bd4adac1431983c518314070472c98ad3 100644 --- a/src/BootstrapBlazor/Extensions/NavigationManagerExtensions.cs +++ b/src/BootstrapBlazor/Extensions/NavigationManagerExtensions.cs @@ -21,13 +21,12 @@ public static class NavigationManagerExtensions /// /// /// - public static void NavigateTo(this NavigationManager navigation, IServiceProvider provider, string url, string text, string? icon = null, bool? closable = null) + public static void NavigateTo(this NavigationManager navigation, IServiceProvider provider, string url, string text, string? icon = null, bool closable = true) { var option = provider.GetRequiredService(); option.Text = text; option.Icon = icon; - option.IsActive = true; - option.Closable = closable ?? true; + option.Closable = closable; navigation.NavigateTo(url); } } diff --git a/src/BootstrapBlazor/Options/TabItemTextOptions.cs b/src/BootstrapBlazor/Options/TabItemTextOptions.cs index 3edbaec6dd189f5051c3d5322c24446574c070bc..6b2a693eb6d81deeb1fb7962151443b6687a011b 100644 --- a/src/BootstrapBlazor/Options/TabItemTextOptions.cs +++ b/src/BootstrapBlazor/Options/TabItemTextOptions.cs @@ -20,29 +20,29 @@ internal class TabItemTextOptions public string? Icon { get; set; } /// - /// 获得/设置 是否激活 默认为 null + /// 获得/设置 是否激活 默认为 true /// /// - public bool? IsActive { get; set; } + public bool IsActive { get; set; } = true; /// - /// 获得/设置 当前 TabItem 是否可关闭 默认为 null + /// 获得/设置 当前 TabItem 是否可关闭 默认为 true /// - public bool? Closable { get; set; } + public bool Closable { get; set; } = true; /// - /// + /// 重置方法 /// public void Reset() { Text = null; Icon = null; - IsActive = null; - Closable = null; + IsActive = true; + Closable = true; } /// - /// + /// 是否可用方法 /// /// public bool Valid() => Text != null; diff --git a/test/UnitTest/Components/LayoutTest.cs b/test/UnitTest/Components/LayoutTest.cs index 28cbd1e9c7e949fc1cfb2540bceb176e5c5c60ad..ad080c188f19ae812f890410db14ed909ace8fbe 100644 --- a/test/UnitTest/Components/LayoutTest.cs +++ b/test/UnitTest/Components/LayoutTest.cs @@ -211,6 +211,10 @@ public class LayoutTest : BootstrapBlazorTestBase }); navMan.NavigateTo("/"); Assert.Equal("http://localhost/Test", navMan.Uri); + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.OnAuthorizing, null); + }); } [Fact] diff --git a/test/UnitTest/Components/TabLinkTest.cs b/test/UnitTest/Components/TabLinkTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..302ac0bef7d53f5e14c27c05f60c22f9cfb9b1a0 --- /dev/null +++ b/test/UnitTest/Components/TabLinkTest.cs @@ -0,0 +1,38 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace UnitTest.Components; + +public class TabLinkTest : BootstrapBlazorTestBase +{ + [Fact] + public void Click_Ok() + { + var clicked = false; + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.Url, "/Cat"); + pb.Add(a => a.Text, "Cat"); + pb.Add(a => a.Icon, "fa fa-fa"); + pb.Add(a => a.Closable, false); + pb.Add(a => a.OnClick, () => + { + clicked = true; + return Task.CompletedTask; + }); + }); + cut.Find("a").Click(); + Assert.True(clicked); + } + + [Fact] + public void ChildContent_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.ChildContent, builder => builder.AddContent(0, "Body")); + }); + Assert.Contains("Body", cut.Markup); + } +} diff --git a/test/UnitTest/Components/TabTest.cs b/test/UnitTest/Components/TabTest.cs index aaad053fdf711f6e22f1966e612fcfc00e7b73e2..2d63cf417c4a932508e4282bb8dbe96c8930835d 100644 --- a/test/UnitTest/Components/TabTest.cs +++ b/test/UnitTest/Components/TabTest.cs @@ -5,6 +5,7 @@ using Bunit.TestDoubles; using Microsoft.Extensions.DependencyInjection; using System.Reflection; +using UnitTest.Misc; namespace UnitTest.Components; @@ -27,6 +28,20 @@ public class TabTest : BootstrapBlazorTestBase }); }); Assert.Contains("Tab1-Content", cut.Markup); + Assert.Equal("TabItem-Key", cut.FindComponent().Instance.Key); + } + + [Fact] + public void TabItemCreate_Ok() + { + TabItem.Create(new Dictionary() + { + ["Url"] = null + }); + TabItem.Create(new Dictionary() + { + ["Url"] = new NullString() + }); } [Fact] @@ -34,7 +49,6 @@ public class TabTest : BootstrapBlazorTestBase { var cut = Context.RenderComponent(pb => { - pb.Add(a => a.ShowExtendButtons, true); pb.Add(a => a.Placement, Placement.Left); pb.Add(a => a.Height, 100); }); @@ -68,6 +82,9 @@ public class TabTest : BootstrapBlazorTestBase var clicked = false; var cut = Context.RenderComponent(pb => { + pb.Add(a => a.ShowExtendButtons, true); + pb.Add(a => a.Placement, Placement.Bottom); + pb.Add(a => a.ShowClose, true); pb.Add(a => a.OnClickTab, item => { clicked = true; @@ -92,13 +109,56 @@ public class TabTest : BootstrapBlazorTestBase cut.Find(".tabs-item").Click(); Assert.True(clicked); - var tab = cut.Instance; - cut.InvokeAsync(() => tab.ClickNextTab()); + // Click Prev + var button = cut.Find(".nav-link-bar.left"); + button.Click(); + button.Click(); + button.Click(); Assert.Equal("Tab1-Content", cut.Find(".tabs-body .d-none").InnerHtml); - cut.InvokeAsync(() => tab.ClickPrevTab()); - cut.InvokeAsync(() => tab.CloseCurrentTab()); - cut.InvokeAsync(() => tab.CloseAllTabs()); + // Click Next + button = cut.Find(".nav-link-bar.right"); + button.Click(); + button.Click(); + button.Click(); + Assert.Equal("Tab2-Content", cut.Find(".tabs-body .d-none").InnerHtml); + + // Close + button = cut.Find(".tabs-item-close"); + button.Click(); + } + + [Fact] + public void ClickTabToNavigation_True() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.AdditionalAssemblies, new Assembly[] { GetType().Assembly }); + pb.Add(a => a.ShowExtendButtons, true); + pb.Add(a => a.Placement, Placement.Top); + pb.Add(a => a.ClickTabToNavigation, true); + pb.Add(a => a.ShowClose, true); + pb.Add(a => a.DefaultUrl, "/"); + }); + cut.InvokeAsync(() => cut.Instance.AddTab("/", "Index")); + cut.InvokeAsync(() => cut.Instance.AddTab("/Cat", null!)); + + // Click Prev + var button = cut.Find(".nav-link-bar.left"); + button.Click(); + + // Click Next + button = cut.Find(".nav-link-bar.right"); + button.Click(); + + button = cut.Find(".tabs-item-close"); + button.Click(); + + // Close Current + cut.InvokeAsync(() => cut.Instance.CloseAllTabs()); + + button = cut.Find(".dropdown-item"); + button.Click(); } [Fact] @@ -132,33 +192,144 @@ public class TabTest : BootstrapBlazorTestBase public void AddTabByUrl_Ok() { var navMan = Context.Services.GetRequiredService(); + navMan.NavigateTo("/"); var cut = Context.RenderComponent(pb => { pb.Add(a => a.AdditionalAssemblies, new Assembly[] { GetType().Assembly }); pb.Add(a => a.ClickTabToNavigation, true); }); + + navMan.NavigateTo("/"); + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ExcludeUrls, new String[] { "/" }); + }); + + navMan.NavigateTo("/"); + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ExcludeUrls, new String[] { "" }); + }); + + navMan.NavigateTo("/Cat"); + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ExcludeUrls, new String[] { "/", "Cat" }); + }); + navMan.NavigateTo("/"); + cut.InvokeAsync(() => cut.Instance.AddTab(new Dictionary + { + ["Text"] = "Cat", + ["Url"] = "Cat" + })); + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ExcludeUrls, new String[] { "/Test" }); + }); + cut.InvokeAsync(() => cut.Instance.CloseCurrentTab()); + + // AddTab + cut.InvokeAsync(() => cut.Instance.AddTab(new Dictionary + { + ["Text"] = "Cat", + ["Url"] = null, + ["IsActive"] = true + })); + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ExcludeUrls, new String[] { "/Test" }); + }); + + // Remove Tab + var item = cut.Instance.GetActiveTab(); + Assert.NotNull(item); + cut.InvokeAsync(() => cut.Instance.RemoveTab(item!)); + item = cut.Instance.GetActiveTab(); + Assert.Null(item); } [Fact] - public void ExcludeUrls_Ok() + public void DefaultUrl_Ok() { - var navMan = Context.Services.GetRequiredService(); var cut = Context.RenderComponent(pb => { pb.Add(a => a.AdditionalAssemblies, new Assembly[] { GetType().Assembly }); - pb.Add(a => a.ClickTabToNavigation, true); - pb.Add(a => a.ExcludeUrls, new String[] { "/Cat" }); + pb.Add(a => a.DefaultUrl, "/"); }); - navMan.NavigateTo("/Cat"); + var item = cut.Instance.GetActiveTab(); + Assert.Contains("Index", cut.Markup); } [Fact] - public void DefaultUrl_Ok() + public void IsOnlyRenderActiveTab_Ok() { var cut = Context.RenderComponent(pb => { + pb.Add(a => a.AdditionalAssemblies, new Assembly[] { GetType().Assembly }); + pb.Add(a => a.IsOnlyRenderActiveTab, true); + pb.AddChildContent(pb => + { + pb.Add(a => a.Text, "Tab1"); + pb.Add(a => a.Url, "/Cat"); + pb.Add(a => a.ChildContent, "Tab1-Content"); + }); + pb.AddChildContent(pb => + { + pb.Add(a => a.Text, "Tab2"); + pb.Add(a => a.Url, "/"); + pb.Add(a => a.Closable, false); + pb.Add(a => a.ChildContent, "Tab2-Content"); + }); + }); + Assert.Equal(1, cut.FindAll(".tabs-body-content").Count); + + // 提高代码覆盖率 + cut.InvokeAsync(() => cut.Instance.CloseOtherTabs()); + } + + [Fact] + public void ActiveTab_Ok() + { + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.AdditionalAssemblies, new Assembly[] { GetType().Assembly }); pb.Add(a => a.DefaultUrl, "/"); }); + cut.InvokeAsync(() => cut.Instance.ActiveTab(0)); + var item = cut.Instance.GetActiveTab(); + Assert.NotNull(item); + cut.InvokeAsync(() => + { + if (item != null) + { + cut.Instance.ActiveTab(item); + } + }); + cut.InvokeAsync(() => cut.Instance.RemoveTab(item!)); + item = cut.Instance.GetActiveTab(); + Assert.Null(item); + } + + [Fact] + public void NavigationActiveTab_Ok() + { + var navMan = Context.Services.GetRequiredService(); + navMan.NavigateTo("/"); + var cut = Context.RenderComponent(pb => + { + pb.Add(a => a.AdditionalAssemblies, new Assembly[] { GetType().Assembly }); + pb.Add(a => a.ClickTabToNavigation, true); + pb.AddChildContent(pb => + { + pb.Add(a => a.Text, "Tab1"); + pb.Add(a => a.Url, "/Cat"); + }); + pb.AddChildContent(pb => + { + pb.Add(a => a.Text, "Tab2"); + pb.Add(a => a.Url, "/"); + }); + }); } } diff --git a/test/UnitTest/Misc/NullString.cs b/test/UnitTest/Misc/NullString.cs new file mode 100644 index 0000000000000000000000000000000000000000..e78647570dc0a1a372d4d284c0ce4f7ea7086fbf --- /dev/null +++ b/test/UnitTest/Misc/NullString.cs @@ -0,0 +1,10 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace UnitTest.Misc; + +internal class NullString +{ + public override string? ToString() => null; +} diff --git a/test/UnitTest/Pages/Cat.cs b/test/UnitTest/Pages/Cat.cs index 2fd2d34428aceff57fa1707bbd9baaf45039f1e7..f28487d4640dc30ba7518f2583d880805325879e 100644 --- a/test/UnitTest/Pages/Cat.cs +++ b/test/UnitTest/Pages/Cat.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components.Rendering; namespace UnitTest.Pages; [Route("/Cat")] +[TabItemOption(Icon = "fa fa-fa", Closable = true, Text = "Cat")] public class Cat : ComponentBase { protected override void BuildRenderTree(RenderTreeBuilder builder)