From 675000a640653b5401b925b07498b4a36167d586 Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 15:09:22 +0800 Subject: [PATCH 01/13] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=E7=BA=A7?= =?UTF-8?q?=E8=81=94=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/SelectGeneric/SelectGeneric.razor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs index 7b544789455..c5ade71f620 100644 --- a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs +++ b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs @@ -12,6 +12,7 @@ namespace BootstrapBlazor.Components; /// Select 组件实现类 /// /// +[CascadingTypeParameter(nameof(TValue))] public partial class SelectGeneric : ISelectGeneric, IModelEqualityComparer { [Inject] From e9afe19f9225ecba88ae8831dd4acbfe6103299d Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 15:09:41 +0800 Subject: [PATCH 02/13] =?UTF-8?q?doc:=20=E6=9B=B4=E6=96=B0=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/Selects.razor | 146 ++++++++---------- .../Components/Samples/Selects.razor.cs | 80 +++++----- 2 files changed, 106 insertions(+), 120 deletions(-) diff --git a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor index 093dd481024..e3ba2e53821 100644 --- a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor +++ b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor @@ -13,28 +13,28 @@

@((MarkupString)Localizer["SelectsNormalDescription"].Value)

- +
- +
- +
- +
- +
- +
- +
- +
@@ -45,31 +45,31 @@ Name="Disable">
- +
- +
- +
- +
- +
- +
- +

@((MarkupString)Localizer["SelectsDisableOption"].Value)

- +
@@ -92,23 +92,10 @@ Name="IsClearable">
- +
- -
-
- - - -
-
- -
-
- +
@@ -118,10 +105,10 @@ Name="Cascading">
- +
@@ -156,8 +143,7 @@ Name="Group">
- +
@@ -167,7 +153,7 @@ Name="Guid">
- +
- +
@@ -206,13 +192,13 @@ Name="Static">
- +
@@ -224,16 +210,16 @@

@((MarkupString)Localizer["SelectsEnumDescription2"].Value)

- + +
- + +
- + +
@SelectedEnumItem
@@ -244,11 +230,10 @@ -

@((MarkupString)Localizer["SelectsNullableDescription"].Value)

+
@((MarkupString)Localizer["SelectsNullableDescription"].Value)
- +
@GetSelectedIntItemString()
@@ -259,12 +244,13 @@ -

@((MarkupString)Localizer["SelectsNullableBooleanDescription1"].Value)

-

@((MarkupString)Localizer["SelectsNullableBooleanDescription2"].Value)

+
+

@((MarkupString)Localizer["SelectsNullableBooleanDescription1"].Value)

+
@((MarkupString)Localizer["SelectsNullableBooleanDescription2"].Value)
+
- +
@GetSelectedBoolItemString()
@@ -277,10 +263,10 @@ Name="CustomTemplate">
- +
- +
@@ -329,10 +315,10 @@
- +
@@ -342,8 +328,8 @@ Name="ConfirmSelect">
- + - @context?.Text + @context.Text - +
@@ -368,7 +354,7 @@ Name="TimeZone">
- +
@@ -390,7 +376,7 @@
@((MarkupString)Localizer["SelectsIsEditableDesc"].Value)
- +
@@ -418,20 +404,20 @@

1. 使用 OnQueryAsync 作为数据源

- +
- +

2. 使用 Items 作为数据源

- +
- +
@@ -445,7 +431,7 @@
- +
diff --git a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs index 6bb60dc47fb..0ed38b7881d 100644 --- a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs +++ b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs @@ -15,24 +15,24 @@ public sealed partial class Selects private Foo Model { get; set; } = new Foo(); - private IEnumerable Items { get; set; } = new[] + private IEnumerable> Items { get; set; } = new List>() { - new SelectedItem ("Beijing", "北京"), - new SelectedItem ("Shanghai", "上海") { Active = true }, + new("Beijing", "北京"), + new("Shanghai", "上海") { Active = true }, }; - private IEnumerable ClearableItems { get; set; } = new[] + private IEnumerable> ClearableItems { get; set; } = new List>() { - new SelectedItem ("", "未选择"), - new SelectedItem ("Beijing", "北京"), - new SelectedItem ("Shanghai", "上海") + new("", "未选择"), + new("Beijing", "北京"), + new("Shanghai", "上海") }; - private IEnumerable VirtualItems => Foos.Select(i => new SelectedItem(i.Name!, i.Name!)).ToList(); + private IEnumerable> VirtualItems => Foos.Select(i => new SelectedItem(i, i.Name!)); - private SelectedItem? VirtualItem1 { get; set; } + private Foo VirtualItem1 { get; set; } = new(); - private SelectedItem? VirtualItem2 { get; set; } + private Foo VirtualItem2 { get; set; } = new(); [NotNull] private List? Foos { get; set; } @@ -47,7 +47,7 @@ public sealed partial class Selects private bool _isClearable; - private string? _fooName; + private Foo _foo = new(); /// /// @@ -55,13 +55,13 @@ public sealed partial class Selects protected override void OnInitialized() { TimeZoneInfo.ClearCachedData(); - TimeZoneItems = TimeZoneInfo.GetSystemTimeZones().Select(i => new SelectedItem(i.Id, i.DisplayName)); + TimeZoneItems = TimeZoneInfo.GetSystemTimeZones().Select(i => new SelectedItem(i.Id, i.DisplayName)); TimeZoneId = TimeZoneInfo.Local.Id; TimeZoneValue = TimeZoneInfo.Local.BaseUtcOffset; Foos = Foo.GenerateFoo(LocalizerFoo); } - private async Task> OnQueryAsync(VirtualizeQueryOption option) + private async Task>> OnQueryAsync(VirtualizeQueryOption option) { await Task.Delay(200); var items = Foos; @@ -69,21 +69,21 @@ private async Task> OnQueryAsync(VirtualizeQueryOption o { items = Foos.Where(i => i.Name!.Contains(option.SearchText, StringComparison.OrdinalIgnoreCase)).ToList(); } - return new QueryData + return new QueryData> { - Items = items.Skip(option.StartIndex).Take(option.Count).Select(i => new SelectedItem(i.Name!, i.Name!)), + Items = items.Skip(option.StartIndex).Take(option.Count).Select(i => new SelectedItem(i, i.Name!)), TotalCount = items.Count }; } - private Task OnItemChanged(SelectedItem item) + private Task OnItemChanged(SelectedItem item) { Logger.Log($"SelectedItem Text: {item.Text} Value: {item.Value} Selected"); StateHasChanged(); return Task.CompletedTask; } - private readonly IEnumerable Items4 = new SelectedItem[] + private readonly IEnumerable> Items4 = new SelectedItem[] { new("Beijing", "北京") { IsDisabled = true}, new("Shanghai", "上海") { Active = true }, @@ -98,7 +98,7 @@ private Task OnItemChanged(SelectedItem item) private string ItemString => Item == null ? "" : $"{Item.Text} ({Item.Value})"; - private readonly IEnumerable Items3 = new SelectedItem[] + private readonly IEnumerable> Items3 = new SelectedItem[] { new("", "请选择 ..."), new("Beijing", "北京") { Active = true }, @@ -106,7 +106,7 @@ private Task OnItemChanged(SelectedItem item) new("Hangzhou", "杭州") }; - private IEnumerable? Items2 { get; set; } + private IEnumerable>? Items2 { get; set; } private Task OnShowDialog() => Dialog.Show(new DialogOption() { @@ -114,13 +114,13 @@ private Task OnShowDialog() => Dialog.Show(new DialogOption() Component = BootstrapDynamicComponent.CreateComponent() }); - private async Task OnCascadeBindSelectClick(SelectedItem item) + private async Task OnCascadeBindSelectClick(SelectedItem item) { // 模拟异步通讯切换线程 await Task.Delay(10); if (item.Value == "Beijing") { - Items2 = new SelectedItem[] + Items2 = new SelectedItem[] { new("1","朝阳区") { Active = true}, new("2","海淀区"), @@ -128,7 +128,7 @@ private async Task OnCascadeBindSelectClick(SelectedItem item) } else if (item.Value == "Shanghai") { - Items2 = new SelectedItem[] + Items2 = new SelectedItem[] { new("1","静安区"), new("2","黄浦区") { Active = true } , @@ -143,7 +143,7 @@ private async Task OnCascadeBindSelectClick(SelectedItem item) private Foo ValidateModel { get; set; } = new Foo() { Name = "" }; - private readonly IEnumerable GroupItems = new SelectedItem[] + private readonly IEnumerable> GroupItems = new SelectedItem[] { new("Jilin", "吉林") { GroupName = "东北"}, new("Liaoning", "辽宁") {GroupName = "东北", Active = true }, @@ -155,10 +155,10 @@ private async Task OnCascadeBindSelectClick(SelectedItem item) private Guid CurrentGuid { get; set; } - private readonly IEnumerable GuidItems = new SelectedItem[] + private readonly IEnumerable> GuidItems = new SelectedItem[] { - new(Guid.NewGuid().ToString(), "Guid1"), - new(Guid.NewGuid().ToString(), "Guid2") + new(Guid.NewGuid(), "Guid1"), + new(Guid.NewGuid(), "Guid2") }; private Foo LabelModel { get; set; } = new Foo(); @@ -174,7 +174,7 @@ private Task OnInputChangedCallback(string v) var item = Items.FirstOrDefault(i => i.Text.Equals(v, StringComparison.OrdinalIgnoreCase)); if (item == null) { - item = new SelectedItem() { Value = v, Text = v }; + item = new SelectedItem() { Value = v, Text = v }; var items = Items.ToList(); items.Insert(0, item); Items = items; @@ -187,11 +187,11 @@ private string GetSelectedIntItemString() return NullableSelectedIntItem.HasValue ? NullableSelectedIntItem.Value.ToString() : "null"; } - private IEnumerable NullableIntItems { get; set; } = new SelectedItem[] + private IEnumerable> NullableIntItems { get; set; } = new SelectedItem[] { - new() { Text = "Item 1", Value = "" }, - new() { Text = "Item 2", Value = "2" }, - new() { Text = "Item 3", Value = "3" } + new() { Text = "Item 1", Value = null }, + new() { Text = "Item 2", Value = 2 }, + new() { Text = "Item 3", Value = 3 } }; private bool? SelectedBoolItem { get; set; } @@ -201,14 +201,14 @@ private string GetSelectedBoolItemString() return SelectedBoolItem.HasValue ? SelectedBoolItem.Value.ToString() : "null"; } - private IEnumerable NullableBoolItems { get; set; } = new SelectedItem[] + private IEnumerable> NullableBoolItems { get; set; } = new SelectedItem[] { - new() { Text = "空值", Value = "" }, - new() { Text = "True 值", Value = "true" }, - new() { Text = "False 值", Value = "false" } + new() { Text = "空值", Value = null }, + new() { Text = "True 值", Value = true }, + new() { Text = "False 值", Value = false } }; - private readonly SelectedItem[] StringItems = + private readonly SelectedItem[] StringItems = [ new("1", "1"), new("12", "12"), @@ -221,13 +221,13 @@ private string GetSelectedBoolItemString() new("abcde", "abcde") ]; - private static Task OnBeforeSelectedItemChange(SelectedItem item) + private static Task OnBeforeSelectedItemChange(SelectedItem item) { return Task.FromResult(true); } [NotNull] - private IEnumerable? TimeZoneItems { get; set; } + private IEnumerable>? TimeZoneItems { get; set; } private string? TimeZoneId { get; set; } @@ -242,14 +242,14 @@ private Task OnTimeZoneValueChanged(string timeZoneId) return Task.CompletedTask; } - private readonly List> _genericItems = + private readonly List> _genericItems = [ new() { Text = "Foo1", Value = new Foo() { Id = 1, Address = "Address_F001" } }, new() { Text = "Foo2", Value = new Foo() { Id = 2, Address = "Address_F002" } }, new() { Text = "Foo3", Value = new Foo() { Id = 3, Address = "Address_F003" } } ]; - private Foo? _selectedFoo; + private Foo _selectedFoo = new(); /// /// 获得事件方法 From 3f5ea413df4318952b7fd4f9f7f3190bd0df66fe Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 15:10:37 +0800 Subject: [PATCH 03/13] =?UTF-8?q?doc:=20=E6=9B=B4=E6=94=B9=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=E6=96=87=E4=BB=B6=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/{Selects.razor => SelectGenerics.razor} | 0 .../Samples/{Selects.razor.cs => SelectGenerics.razor.cs} | 2 +- .../Samples/{Selects.razor.css => SelectGenerics.razor.css} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/BootstrapBlazor.Shared/Components/Samples/{Selects.razor => SelectGenerics.razor} (100%) rename src/BootstrapBlazor.Shared/Components/Samples/{Selects.razor.cs => SelectGenerics.razor.cs} (99%) rename src/BootstrapBlazor.Shared/Components/Samples/{Selects.razor.css => SelectGenerics.razor.css} (100%) diff --git a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor similarity index 100% rename from src/BootstrapBlazor.Shared/Components/Samples/Selects.razor rename to src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor diff --git a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor.cs similarity index 99% rename from src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs rename to src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor.cs index 0ed38b7881d..9aaa30f9d36 100644 --- a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs +++ b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor.cs @@ -8,7 +8,7 @@ namespace BootstrapBlazor.Shared.Components.Samples; /// /// 下拉框操作类 /// -public sealed partial class Selects +public sealed partial class SelectGenerics { [NotNull] private ConsoleLogger? Logger { get; set; } diff --git a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.css b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor.css similarity index 100% rename from src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.css rename to src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor.css From d1aa171f48b399c122aaf413175aebe437821048 Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 15:19:28 +0800 Subject: [PATCH 04/13] =?UTF-8?q?doc:=20=E5=A2=9E=E5=8A=A0=E5=8E=9F=20Sele?= =?UTF-8?q?ct=20=E7=BB=84=E4=BB=B6=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Samples/SelectGenerics.razor | 2 +- .../Components/Samples/Selects.razor | 441 ++++++++++++++++++ .../Components/Samples/Selects.razor.cs | 396 ++++++++++++++++ .../Components/Samples/Selects.razor.css | 62 +++ 4 files changed, 900 insertions(+), 1 deletion(-) create mode 100644 src/BootstrapBlazor.Shared/Components/Samples/Selects.razor create mode 100644 src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs create mode 100644 src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.css diff --git a/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor index e3ba2e53821..585d8f970d6 100644 --- a/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor +++ b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor @@ -1,4 +1,4 @@ -@page "/select" +@page "/select-generic" @inject DialogService Dialog @inject IStringLocalizer Localizer @inject IOptionsMonitor WebsiteOption diff --git a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor new file mode 100644 index 00000000000..e67f5be7f8f --- /dev/null +++ b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor @@ -0,0 +1,441 @@ +@page "/select" +@inject DialogService Dialog +@inject IStringLocalizer Localizer +@inject IOptionsMonitor WebsiteOption + +

@Localizer["SelectsTitle"]

+ +

@Localizer["SelectsDescription"]

+ + +

@((MarkupString)Localizer["SelectsNormalDescription"].Value)

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+

@((MarkupString)Localizer["SelectsDisableOption"].Value)

+
+
+ +
+
+ +
+
+
+ + +
+
+ +
+
+ +
+
+
+ + +
+
+ +
+
+ +
+
+
+ + +
+
+ +
+
+
+
+
+ + + +
+
+ +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+
+ + +
+
+ +
+
+
+ + +

@((MarkupString)Localizer["SelectsEnumDescription1"].Value)

+

@((MarkupString)Localizer["SelectsEnumDescription2"].Value)

+
+
+ +
+
+ +
+
+ +
+
+
@SelectedEnumItem
+
+
+
+ + +

@((MarkupString)Localizer["SelectsNullableDescription"].Value)

+
+
+ +
+
+
@GetSelectedIntItemString()
+
+
+
+ + +

@((MarkupString)Localizer["SelectsNullableBooleanDescription1"].Value)

+

@((MarkupString)Localizer["SelectsNullableBooleanDescription2"].Value)

+
+
+ +
+
+
@GetSelectedBoolItemString()
+
+
+
+ + +
+
+ +
+
+ +
+
+
+ + +
+
+
+ + + + +
+
+
+
+
+ +
+
+
+ + +
+
+ + + + @context?.Text + + +
+
+
+ + +
+
+ +
+
+
+ + +
@((MarkupString)Localizer["SelectsIsEditableDesc"].Value)
+
+
+ +
+
+
+ + +
@((MarkupString)Localizer["SelectsVirtualizeDescription"].Value)
+ +
+
+ + + + +
+
+ + + + +
+
+ +

1. 使用 OnQueryAsync 作为数据源

+
+
+ +
+
+ +
+
+ +

2. 使用 Items 作为数据源

+
+
+ +
+
+ +
+
+
+ + + + diff --git a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs new file mode 100644 index 00000000000..c07134ba8de --- /dev/null +++ b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs @@ -0,0 +1,396 @@ +// 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.Shared.Components.Samples; + +/// +/// 下拉框操作类 +/// +public sealed partial class Selects +{ + [NotNull] + private ConsoleLogger? Logger { get; set; } + + private Foo Model { get; set; } = new Foo(); + + private IEnumerable Items { get; set; } = new[] + { + new SelectedItem ("Beijing", "北京"), + new SelectedItem ("Shanghai", "上海") { Active = true }, + }; + + private IEnumerable ClearableItems { get; set; } = new[] + { + new SelectedItem ("", "未选择"), + new SelectedItem ("Beijing", "北京"), + new SelectedItem ("Shanghai", "上海") + }; + + private IEnumerable VirtualItems => Foos.Select(i => new SelectedItem(i.Name!, i.Name!)).ToList(); + + private SelectedItem? VirtualItem1 { get; set; } + + private SelectedItem? VirtualItem2 { get; set; } + + [NotNull] + private List? Foos { get; set; } + + [Inject] + [NotNull] + private IStringLocalizer? LocalizerFoo { get; set; } + + private bool _showSearch; + + private bool _isShowSearchClearable; + + private bool _isClearable; + + private string? _fooName; + + /// + /// + /// + protected override void OnInitialized() + { + TimeZoneInfo.ClearCachedData(); + TimeZoneItems = TimeZoneInfo.GetSystemTimeZones().Select(i => new SelectedItem(i.Id, i.DisplayName)); + TimeZoneId = TimeZoneInfo.Local.Id; + TimeZoneValue = TimeZoneInfo.Local.BaseUtcOffset; + Foos = Foo.GenerateFoo(LocalizerFoo); + } + + private async Task> OnQueryAsync(VirtualizeQueryOption option) + { + await Task.Delay(200); + var items = Foos; + if (!string.IsNullOrEmpty(option.SearchText)) + { + items = Foos.Where(i => i.Name!.Contains(option.SearchText, StringComparison.OrdinalIgnoreCase)).ToList(); + } + return new QueryData + { + Items = items.Skip(option.StartIndex).Take(option.Count).Select(i => new SelectedItem(i.Name!, i.Name!)), + TotalCount = items.Count + }; + } + + private Task OnItemChanged(SelectedItem item) + { + Logger.Log($"SelectedItem Text: {item.Text} Value: {item.Value} Selected"); + StateHasChanged(); + return Task.CompletedTask; + } + + private readonly IEnumerable Items4 = new SelectedItem[] + { + new("Beijing", "北京") { IsDisabled = true}, + new("Shanghai", "上海") { Active = true }, + new("Guangzhou", "广州") + }; + + private Foo BindingModel { get; set; } = new Foo(); + + private Foo ClearableModel { get; set; } = new Foo(); + + private SelectedItem? Item { get; set; } + + private string ItemString => Item == null ? "" : $"{Item.Text} ({Item.Value})"; + + private readonly IEnumerable Items3 = new SelectedItem[] + { + new("", "请选择 ..."), + new("Beijing", "北京") { Active = true }, + new("Shanghai", "上海"), + new("Hangzhou", "杭州") + }; + + private IEnumerable? Items2 { get; set; } + + private Task OnShowDialog() => Dialog.Show(new DialogOption() + { + Title = "弹窗中使用级联下拉框", + Component = BootstrapDynamicComponent.CreateComponent() + }); + + private async Task OnCascadeBindSelectClick(SelectedItem item) + { + // 模拟异步通讯切换线程 + await Task.Delay(10); + if (item.Value == "Beijing") + { + Items2 = new SelectedItem[] + { + new("1","朝阳区") { Active = true}, + new("2","海淀区"), + }; + } + else if (item.Value == "Shanghai") + { + Items2 = new SelectedItem[] + { + new("1","静安区"), + new("2","黄浦区") { Active = true } , + }; + } + else + { + Items2 = []; + } + StateHasChanged(); + } + + private Foo ValidateModel { get; set; } = new Foo() { Name = "" }; + + private readonly IEnumerable GroupItems = new SelectedItem[] + { + new("Jilin", "吉林") { GroupName = "东北"}, + new("Liaoning", "辽宁") {GroupName = "东北", Active = true }, + new("Beijing", "北京") { GroupName = "华中"}, + new("Shijiazhuang", "石家庄") { GroupName = "华中"}, + new("Shanghai", "上海") {GroupName = "华东", Active = true }, + new("Ningbo", "宁波") {GroupName = "华东", Active = true } + }; + + private Guid CurrentGuid { get; set; } + + private readonly IEnumerable GuidItems = new SelectedItem[] + { + new(Guid.NewGuid().ToString(), "Guid1"), + new(Guid.NewGuid().ToString(), "Guid2") + }; + + private Foo LabelModel { get; set; } = new Foo(); + + private EnumEducation SelectedEnumItem { get; set; } = EnumEducation.Primary; + + private EnumEducation? SelectedEnumItem1 { get; set; } + + private int? NullableSelectedIntItem { get; set; } + + private Task OnInputChangedCallback(string v) + { + var item = Items.FirstOrDefault(i => i.Text.Equals(v, StringComparison.OrdinalIgnoreCase)); + if (item == null) + { + item = new SelectedItem() { Value = v, Text = v }; + var items = Items.ToList(); + items.Insert(0, item); + Items = items; + } + return Task.CompletedTask; + } + + private string GetSelectedIntItemString() + { + return NullableSelectedIntItem.HasValue ? NullableSelectedIntItem.Value.ToString() : "null"; + } + + private IEnumerable NullableIntItems { get; set; } = new SelectedItem[] + { + new() { Text = "Item 1", Value = "" }, + new() { Text = "Item 2", Value = "2" }, + new() { Text = "Item 3", Value = "3" } + }; + + private bool? SelectedBoolItem { get; set; } + + private string GetSelectedBoolItemString() + { + return SelectedBoolItem.HasValue ? SelectedBoolItem.Value.ToString() : "null"; + } + + private IEnumerable NullableBoolItems { get; set; } = new SelectedItem[] + { + new() { Text = "空值", Value = "" }, + new() { Text = "True 值", Value = "true" }, + new() { Text = "False 值", Value = "false" } + }; + + private readonly SelectedItem[] StringItems = + [ + new("1", "1"), + new("12", "12"), + new("123", "123"), + new("1234", "1234"), + new("a", "a"), + new("ab", "ab"), + new("abc", "abc"), + new("abcd", "abcd"), + new("abcde", "abcde") + ]; + + private static Task OnBeforeSelectedItemChange(SelectedItem item) + { + return Task.FromResult(true); + } + + [NotNull] + private IEnumerable? TimeZoneItems { get; set; } + + private string? TimeZoneId { get; set; } + + [NotNull] + private TimeSpan TimeZoneValue { get; set; } + + private Task OnTimeZoneValueChanged(string timeZoneId) + { + TimeZoneId = timeZoneId; + TimeZoneValue = TimeZoneInfo.GetSystemTimeZones().First(i => i.Id == timeZoneId).BaseUtcOffset; + StateHasChanged(); + return Task.CompletedTask; + } + + /// + /// 获得事件方法 + /// + /// + private EventItem[] GetEvents() => + [ + new() + { + Name = "OnSelectedItemChanged", + Description = Localizer["SelectsOnSelectedItemChanged"], + Type = "Func" + }, + new() + { + Name = "OnBeforeSelectedItemChange", + Description = Localizer["SelectsOnBeforeSelectedItemChange"], + Type = "Func>" + }, + new() + { + Name = "OnInputChangedCallback", + Description = Localizer["SelectsOnInputChangedCallback"], + Type = "Func" + }, + new() + { + Name = "TextConvertToValueCallback", + Description = Localizer["SelectsTextConvertToValueCallback"], + Type = "Func>" + } + ]; + + /// + /// 获得属性方法 + /// + /// + private AttributeItem[] GetAttributes() => + [ + new() + { + Name = "ShowLabel", + Description = Localizer["SelectsShowLabel"], + Type = "bool", + ValueList = "true|false", + DefaultValue = "true" + }, + new() + { + Name = "ShowSearch", + Description = Localizer["SelectsShowSearch"], + Type = "bool", + ValueList = "true|false", + DefaultValue = "false" + }, + new() + { + Name = "DisplayText", + Description = Localizer["SelectsDisplayText"], + Type = "string", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "Class", + Description = Localizer["SelectsClass"], + Type = "string", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "Color", + Description = Localizer["SelectsColor"], + Type = "Color", + ValueList = "Primary / Secondary / Success / Danger / Warning / Info / Dark", + DefaultValue = "Primary" + }, + new() + { + Name = "IsEditable", + Description = Localizer["SelectsIsEditable"], + Type = "boolean", + ValueList = "true / false", + DefaultValue = "false" + }, + new() + { + Name = "IsDisabled", + Description = Localizer["SelectsIsDisabled"], + Type = "boolean", + ValueList = "true / false", + DefaultValue = "false" + }, + new() + { + Name = "Items", + Description = Localizer["SelectsItems"], + Type = "IEnumerable", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "SelectItems", + Description = Localizer["SelectItems"], + Type = "RenderFragment", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "ItemTemplate", + Description = Localizer["SelectsItemTemplate"], + Type = "RenderFragment", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "ChildContent", + Description = Localizer["SelectsChildContent"], + Type = "RenderFragment", + ValueList = " — ", + DefaultValue = " — " + }, + new() + { + Name = "Category", + Description = Localizer["SelectsCategory"], + Type = "SwalCategory", + ValueList = " — ", + DefaultValue = " SwalCategory.Information " + }, + new() + { + Name = "Content", + Description = Localizer["SelectsContent"], + Type = "string?", + ValueList = " — ", + DefaultValue = Localizer["SelectsContentDefaultValue"]! + }, + new() + { + Name = "DisableItemChangedWhenFirstRender", + Description = Localizer["SelectsDisableItemChangedWhenFirstRender"], + Type = "bool", + ValueList = "true|false", + DefaultValue = "false" + } + ]; +} diff --git a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.css b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.css new file mode 100644 index 00000000000..ac594ac0fde --- /dev/null +++ b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.css @@ -0,0 +1,62 @@ +.select-custom ::deep .dropdown-menu { + --bs-dropdown-link-active-bg: var(--bs-secondary); + --bs-dropdown-link-active-color: var(--bs-body-color); + --bb-dropdown-max-height: 540px; +} + +.select-custom ::deep .divider { + --bb-divider-margin: 1rem 0; + --bb-divider-bg: #c0c4cc; +} + +.dropdown-item-demo { + border-radius: var(--bs-dropdown-border-radius); + border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); + padding: .5rem; + flex-direction: column; +} + +.select-custom-header { + display: flex; + align-items: center; + margin-top: .5rem; +} + + .select-custom-header .id { + background-color: var(--bs-success); + padding: .25rem .5rem; + border-radius: var(--bs-dropdown-border-radius); + } + + .select-custom-header .name { + padding: .25rem .5rem; + margin: 0 1rem; + flex: 1; + font-weight: bold; + } + + .select-custom-header .status { + } + +.select-custom-body { + display: flex; +} + + .select-custom-body ::deep .progress { + height: 6px; + margin-bottom: .5rem; + } + + .select-custom-body .bb-avatar { + width: 102px; + border: 2px solid var(--bs-info); + } + +.select-custom-detail { + flex: 1; + margin-inline-start: 2rem; +} + + .select-custom-detail > div { + margin-bottom: .5rem; + } From 79776ee6f7b4895402cdfdf3ea957eb65094f58f Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 15:19:42 +0800 Subject: [PATCH 05/13] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=20SelectGeneri?= =?UTF-8?q?c=20=E7=BB=84=E4=BB=B6=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/SelectGenericTest.cs | 1000 +++++++++++++++++ 1 file changed, 1000 insertions(+) create mode 100644 test/UnitTest/Components/SelectGenericTest.cs diff --git a/test/UnitTest/Components/SelectGenericTest.cs b/test/UnitTest/Components/SelectGenericTest.cs new file mode 100644 index 00000000000..4977887105e --- /dev/null +++ b/test/UnitTest/Components/SelectGenericTest.cs @@ -0,0 +1,1000 @@ +// 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 AngleSharp.Dom; +using AngleSharp.Html.Dom; +using Microsoft.AspNetCore.Components.Web.Virtualization; +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace UnitTest.Components; + +public class SelectGenericTest : BootstrapBlazorTestBase +{ + [Fact] + public void SeletectedItem_Ok() + { + var item = new SelectedItem(null!, "Text"); + Assert.Equal(item.Value, string.Empty); + } + + [Fact] + public async Task OnSearchTextChanged_Null() + { + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.ShowSearch, true); + pb.Add(a => a.Items, new List>() + { + new("1", "Test1"), + new("2", "Test2") { IsDisabled = true } + }); + }); + }); + + var ctx = cut.FindComponent>(); + await ctx.InvokeAsync(async () => + { + await ctx.Instance.ConfirmSelectedItem(0); + + // 搜索 T + ctx.Find(".search-text").Input("T"); + await ctx.Instance.ConfirmSelectedItem(0); + }); + + ctx.SetParametersAndRender(pb => + { + pb.Add(a => a.OnBeforeSelectedItemChange, item => Task.FromResult(false)); + pb.Add(a => a.OnSelectedItemChanged, item => Task.CompletedTask); + }); + await ctx.InvokeAsync(() => ctx.Instance.ConfirmSelectedItem(0)); + + ctx.Instance.ClearSearchText(); + + ctx.SetParametersAndRender(pb => + { + pb.Add(a => a.OnBeforeSelectedItemChange, null); + pb.Add(a => a.OnSelectedItemChanged, null); + pb.Add(a => a.OnSearchTextChanged, text => + { + return new List>() + { + new("1", "Test1") + }; + }); + }); + + await ctx.InvokeAsync(() => + { + ctx.Find(".search-text").Input("T"); + }); + cut.DoesNotContain("Test2"); + } + + [Fact] + public void Options_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Options, builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Text", "Test-Select"); + builder.CloseComponent(); + + builder.OpenComponent(2); + builder.CloseComponent(); + }); + }); + Assert.Contains("Test-Select", cut.Markup); + } + + [Fact] + public void Disabled_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.IsDisabled, true); + pb.Add(a => a.Options, builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, nameof(SelectOptionGeneric.IsDisabled), true); + builder.CloseComponent(); + + builder.OpenComponent(2); + builder.CloseComponent(); + }); + }); + Assert.Contains("_input\" disabled=\"disabled\"", cut.Markup); + Assert.Contains("dropdown-item active disabled", cut.Markup); + } + + [Fact] + public void IsClearable_Ok() + { + var val = "Test2"; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.IsClearable, true); + pb.Add(a => a.Items, new List>() + { + new("", "请选择"), + new("2", "Test2"), + new("3", "Test3") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.OnValueChanged, v => + { + val = v; + return Task.CompletedTask; + }); + }); + var clearButton = cut.Find(".clear-icon"); + cut.InvokeAsync(() => clearButton.Click()); + Assert.Empty(val); + + // 提高代码覆盖率 + var select = cut; + select.SetParametersAndRender(pb => + { + pb.Add(a => a.Color, Color.Danger); + }); + + var validPi = typeof(SelectGeneric).GetProperty("IsValid", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; + validPi.SetValue(select.Instance, true); + + var pi = typeof(SelectGeneric).GetProperty("ClearClassString", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; + val = pi.GetValue(select.Instance, null)!.ToString(); + Assert.Contains("text-success", val); + + validPi.SetValue(select.Instance, false); + val = pi.GetValue(select.Instance, null)!.ToString(); + Assert.Contains("text-danger", val); + } + + [Fact] + public void SelectOption_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Text, "Test-SelectOption"); + pb.Add(a => a.GroupName, "Test-GroupName"); + pb.Add(a => a.IsDisabled, false); + pb.Add(a => a.Active, true); + pb.Add(a => a.Value, ""); + }); + } + + [Fact] + public void Enum_Ok() + { + var cut = Context.RenderComponent>(); + Assert.Equal(2, cut.FindAll(".dropdown-item").Count); + } + + [Fact] + public void NullableEnum_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.AdditionalAttributes, new Dictionary() + { + ["placeholder"] = "" + }); + }); + Assert.Equal(3, cut.FindAll(".dropdown-item").Count); + } + + [Fact] + public async Task OnSelectedItemChanged_OK() + { + var triggered = false; + + // 空值时,不触发 OnSelectedItemChanged 回调 + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("", "Test"), + new("1", "Test2") + }); + pb.Add(a => a.Value, ""); + pb.Add(a => a.OnSelectedItemChanged, item => + { + triggered = true; + return Task.CompletedTask; + }); + }); + Assert.True(triggered); + + // 切换候选项时触发 OnSelectedItemChanged 回调测试 + var items = cut.FindAll(".dropdown-item"); + var count = items.Count; + Assert.Equal(2, count); + + var item = items[1]; + await cut.InvokeAsync(() => { item.Click(); }); + Assert.True(triggered); + + // 切换回 空值 触发 OnSelectedItemChanged 回调测试 + triggered = false; + items = cut.FindAll(".dropdown-item"); + item = items[0]; + await cut.InvokeAsync(() => { item.Click(); }); + Assert.True(triggered); + + // 首次加载值不为空时触发 OnSelectedItemChanged 回调测试 + triggered = false; + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("", "Test"), + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + }); + Assert.True(triggered); + + // 切换回 空值 触发 OnSelectedItemChanged 回调测试 + triggered = false; + items = cut.FindAll(".dropdown-item"); + count = items.Count; + Assert.Equal(3, count); + item = items[0]; + await cut.InvokeAsync(() => { item.Click(); }); + Assert.True(triggered); + } + + [Fact] + public async Task OnSelectedItemChanged_Generic() + { + Foo? selectedValue = null; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new() { Value = new Foo() { Id = 1, Address = "Foo1" }, Text = "test1" }, + new() { Value = new Foo() { Id = 2, Address = "Foo2" }, Text = "test2" } + }); + pb.Add(a => a.Value, new Foo() { Id = 1, Address = "Foo1" }); + pb.Add(a => a.OnSelectedItemChanged, v => + { + if (v is SelectedItem d) + { + selectedValue = d.Value; + } + return Task.CompletedTask; + }); + pb.Add(a => a.CustomKeyAttribute, typeof(KeyAttribute)); + }); + + IModelEqualityComparer comparer = cut.Instance as IModelEqualityComparer; + Assert.NotNull(comparer); + comparer.ModelEqualityComparer = (x, y) => x.Id == y.Id; + + var items = cut.FindAll(".dropdown-item"); + await cut.InvokeAsync(() => items[1].Click()); + Assert.NotNull(selectedValue); + } + + [Fact] + public void DisableItemChangedWhenFirstRender_Ok() + { + var triggered = false; + + // 空值时,不触发 OnSelectedItemChanged 回调 + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test"), + new("2", "Test2") + }); + pb.Add(a => a.Value, ""); + pb.Add(a => a.OnSelectedItemChanged, item => + { + triggered = true; + return Task.CompletedTask; + }); + pb.Add(a => a.DisableItemChangedWhenFirstRender, true); + }); + Assert.False(triggered); + } + + [Fact] + public void Color_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Color, Color.Danger); + }); + Assert.Contains("border-danger", cut.Markup); + } + + [Fact] + public void Validate_Ok() + { + var valid = false; + var invalid = false; + var model = new Foo() { Name = "Test-Select1" }; + var cut = Context.RenderComponent(builder => + { + builder.Add(a => a.OnValidSubmit, context => + { + valid = true; + return Task.CompletedTask; + }); + builder.Add(a => a.OnInvalidSubmit, context => + { + invalid = true; + return Task.CompletedTask; + }); + builder.Add(a => a.Model, model); + builder.AddChildContent>(pb => + { + pb.Add(a => a.Value, model.Name); + pb.Add(a => a.OnValueChanged, v => + { + model.Name = v; + return Task.CompletedTask; + }); + pb.Add(a => a.ValueExpression, model.GenerateValueExpression()); + pb.Add(a => a.Items, new SelectedItem[] + { + new("", "Test"), + new("1", "Test1") { GroupName = "Test1" }, + new("2", "Test2") { GroupName = "Test2" } + }); + }); + }); + + cut.InvokeAsync(() => + { + var form = cut.Find("form"); + form.Submit(); + Assert.True(valid); + }); + + var ctx = cut.FindComponent>(); + ctx.InvokeAsync(async () => + { + await ctx.Instance.ConfirmSelectedItem(0); + var form = cut.Find("form"); + form.Submit(); + Assert.True(invalid); + }); + } + + [Fact] + public void ItemTemplate_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1") { GroupName = "Test1" }, + new("2", "Test2") { GroupName = "Test2" } + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.ItemTemplate, item => builder => + { + builder.OpenElement(0, "div"); + builder.AddContent(1, item.Text); + builder.CloseComponent(); + }); + }); + + cut.Find(".dropdown-item").Click(); + } + + [Fact] + public void GroupItemTemplate_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1") { GroupName = "Test1" }, + new("2", "Test2") { GroupName = "Test2" } + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.GroupItemTemplate, title => builder => + { + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "class", "group-key"); + builder.AddContent(2, title); + builder.CloseComponent(); + }); + }); + cut.Contains("
Test1
"); + cut.Contains("
Test2
"); + } + + [Fact] + public void NullItems_Ok() + { + var cut = Context.RenderComponent>(); + Assert.Contains("select", cut.Markup); + } + + [Fact] + public void NullBool_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new List> + { + new(true, "True"), + new(false, "False"), + }); + pb.Add(a => a.Value, null); + }); + + // 值为 null + // 候选项中无,导致默认选择第一个 Value 被更改为 true + Assert.True(cut.Instance.Value); + } + + [Fact] + public void SearchIcon_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.ShowSearch, true); + pb.Add(a => a.SearchIcon, "search-icon"); + }); + Assert.Contains("search-icon", cut.Markup); + } + + [Fact] + public void IsFixedSearch_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.ShowSearch, true); + pb.Add(a => a.IsFixedSearch, true); + }); + Assert.Contains("search is-fixed", cut.Markup); + Assert.Contains("class=\"icon", cut.Markup); + } + + [Fact] + public void CustomClass_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.CustomClass, "test-custom-class"); + }); + Assert.Contains("test-custom-class", cut.Markup); + } + + [Fact] + public void ShowShadow_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + }); + Assert.Contains("shadow", cut.Markup); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.ShowShadow, false); + }); + Assert.DoesNotContain("shadow", cut.Markup); + } + + [Fact] + public void DropdownIcon_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.DropdownIcon, "search-icon"); + }); + Assert.Contains("search-icon", cut.Markup); + } + + [Fact] + public void DisplayTemplate_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.DisplayTemplate, item => builder => + { + builder.AddContent(0, $"test-display-template-{item?.Text}"); + }); + }); + Assert.Contains("test-display-template-Test2", cut.Markup); + } + + [Fact] + public void IsPopover_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.IsPopover, true); + }); + Assert.DoesNotContain("dropdown-menu-arrow", cut.Markup); + Assert.DoesNotContain("data-bs-toggle=\"dropdown\"", cut.Markup); + } + + [Fact] + public void Offset_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.IsPopover, false); + pb.Add(a => a.Offset, "[0, 11]"); + }); + Assert.Contains("data-bs-offset=\"[0, 11]\"", cut.Markup); + } + + [Fact] + public void Placement_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.Placement, Placement.Top); + }); + cut.Contains($"data-bs-placement=\"{Placement.Top.ToDescriptionString()}\""); + + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.Placement, Placement.Auto); + }); + cut.DoesNotContain("data-bs-placement"); + } + + [Fact] + public void ItemClick_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.IsPopover, true); + }); + + cut.InvokeAsync(() => + { + var item = cut.Find(".dropdown-item"); + item.Click(); + Assert.True(item.ClassList.Contains("active")); + }); + } + + [Fact] + public void IsVirtualize_Items() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.IsVirtualize, true); + pb.Add(a => a.RowHeight, 33f); + pb.Add(a => a.OverscanCount, 4); + }); + + cut.SetParametersAndRender(pb => pb.Add(a => a.ShowSearch, true)); + cut.InvokeAsync(async () => + { + // 搜索 T + cut.Find(".search-text").Input("T"); + await cut.Instance.ConfirmSelectedItem(0); + }); + } + + [Fact] + public async Task IsVirtualize_Items_Clearable_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.IsVirtualize, true); + pb.Add(a => a.RowHeight, 33f); + pb.Add(a => a.OverscanCount, 4); + pb.Add(a => a.IsClearable, true); + pb.Add(a => a.ShowSearch, true); + }); + + // 覆盖有搜索条件时,点击清空按钮 + // 期望 UI 显示值为默认值 + // 期望 下拉框为全数据 + var input = cut.Find(".search-text"); + await cut.InvokeAsync(() => input.Input("2")); + + // 下拉框仅显示一个选项 Test2 + var items = cut.FindAll(".dropdown-item"); + Assert.Single(items); + + // UI 值为 Test2 + await cut.InvokeAsync(() => items[0].Click()); + var el = cut.Find(".form-select") as IHtmlInputElement; + Assert.NotNull(el); + Assert.Equal("Test2", el.Value); + Assert.Equal("2", cut.Instance.Value); + + // 点击 Clear 按钮 + var button = cut.Find(".clear-icon"); + await cut.InvokeAsync(() => button.Click()); + + // UI 恢复 Test1 + Assert.Equal("Test1", el.Value); + + // 下拉框显示所有选项 + items = cut.FindAll(".dropdown-item"); + Assert.Equal(2, items.Count); + } + + [Fact] + public async Task IsVirtualize_OnQueryAsync_Clearable_Ok() + { + var query = false; + var startIndex = 0; + var requestCount = 0; + var searchText = string.Empty; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.OnQueryAsync, option => + { + query = true; + startIndex = option.StartIndex; + requestCount = option.Count; + searchText = option.SearchText; + return Task.FromResult(new QueryData>() + { + Items = string.IsNullOrEmpty(searchText) + ? [new("", "All"), new("1", "Test1"), new("2", "Test2")] + : [new("2", "Test2")], + TotalCount = string.IsNullOrEmpty(searchText) ? 2 : 1 + }); + }); + pb.Add(a => a.Value, ""); + pb.Add(a => a.IsVirtualize, true); + pb.Add(a => a.IsClearable, true); + pb.Add(a => a.ShowSearch, true); + }); + + // 覆盖有搜索条件时,点击清空按钮 + // 期望 UI 显示值为默认值 + // 期望 下拉框为全数据 + var input = cut.Find(".search-text"); + await cut.InvokeAsync(() => input.Input("2")); + + // 下拉框仅显示一个选项 Test2 + var items = cut.FindAll(".dropdown-item"); + Assert.Single(items); + + // UI 值为 Test2 + await cut.InvokeAsync(() => items[0].Click()); + var el = cut.Find(".form-select") as IHtmlInputElement; + Assert.NotNull(el); + Assert.Equal("Test2", el.Value); + Assert.Equal("2", cut.Instance.Value); + + query = false; + // 点击 Clear 按钮 + var button = cut.Find(".clear-icon"); + await cut.InvokeAsync(() => button.Click()); + + // UI 恢复 Test1 + Assert.Equal("All", el.Value); + + // 下拉框显示所有选项 + Assert.True(query); + } + + [Fact] + public async Task IsVirtualize_BindValue() + { + var value = "3"; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Value, value); + pb.Add(a => a.IsVirtualize, true); + pb.Add(a => a.ValueChanged, EventCallback.Factory.Create(this, new Action(item => + { + value = item; + }))); + pb.Add(a => a.OnQueryAsync, option => + { + return Task.FromResult(new QueryData>() + { + Items = new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }, + TotalCount = 2 + }); + }); + }); + + var input = cut.Find(".form-select"); + Assert.Null(input.GetAttribute("value")); + + var select = cut.Instance; + Assert.Equal("3", select.Value); + + var item = cut.Find(".dropdown-item"); + await cut.InvokeAsync(() => { item.Click(); }); + Assert.Equal("1", value); + + input = cut.Find(".form-select"); + Assert.Equal("Test1", input.GetAttribute("value")); + } + + [Fact] + public void IsVirtualize_DefaultVirtualizeItemText() + { + string? value = "3"; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.IsVirtualize, true); + pb.Add(a => a.DefaultVirtualizeItemText, "Test 3"); + pb.Add(a => a.Value, value); + pb.Add(a => a.ValueChanged, EventCallback.Factory.Create(this, new Action(item => + { + value = item; + }))); + pb.Add(a => a.OnQueryAsync, option => + { + return Task.FromResult(new QueryData>() + { + Items = new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }, + TotalCount = 2 + }); + }); + }); + + cut.InvokeAsync(() => + { + var input = cut.Find(".form-select"); + Assert.Equal("Test 3", input.GetAttribute("value")); + }); + } + + [Fact] + public void LoadItems_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.OnQueryAsync, option => + { + return Task.FromResult(new QueryData>()); + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.IsVirtualize, true); + }); + var select = cut.Instance; + var mi = select.GetType().GetMethod("LoadItems", BindingFlags.NonPublic | BindingFlags.Instance); + mi?.Invoke(select, [new ItemsProviderRequest(0, 1, CancellationToken.None)]); + + var totalCountProperty = select.GetType().GetProperty("TotalCount", BindingFlags.NonPublic | BindingFlags.Instance); + totalCountProperty?.SetValue(select, 2); + mi?.Invoke(select, [new ItemsProviderRequest(0, 1, CancellationToken.None)]); + } + + [Fact] + public void IsMarkupString_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "
Test1
"), + new("2", "
Test2
") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.IsMarkupString, true); + }); + Assert.Contains("
Test1
", cut.Markup); + } + + [Fact] + public async Task IsEditable_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "
Test1
"), + new("2", "
Test2
") + }); + pb.Add(a => a.Value, "2"); + }); + var input = cut.Find(".form-select"); + Assert.True(input.IsReadOnly()); + + var updated = false; + cut.SetParametersAndRender(pb => + { + pb.Add(a => a.IsEditable, true); + pb.Add(a => a.OnInputChangedCallback, v => + { + updated = true; + return Task.CompletedTask; + }); + pb.Add(a => a.TextConvertToValueCallback, v => + { + return Task.FromResult(v); + }); + }); + Assert.False(input.IsReadOnly()); + + await cut.InvokeAsync(() => { input.Change("Test3"); }); + Assert.Equal("Test3", cut.Instance.Value); + Assert.True(updated); + } + + [Fact] + public async Task IsEditable_Generic() + { + var items = new List>() + { + new() { Value = new Foo() { Id = 1, Address = "Foo1" }, Text = "test1" }, + new() { Value = new Foo() { Id = 2, Address = "Foo2" }, Text = "test2" } + }; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, items); + pb.Add(a => a.Value, new Foo() { Id = 1, Address = "Foo1" }); + pb.Add(a => a.IsEditable, true); + pb.Add(a => a.TextConvertToValueCallback, v => + { + return Task.FromResult(new Foo() { Id = 3, Address = "Foo3" }); + }); + }); + + var input = cut.Find(".form-select"); + await cut.InvokeAsync(() => { input.Change("test2"); }); + Assert.Equal("Foo2", cut.Instance.Value.Address); + + await cut.InvokeAsync(() => { input.Change("test3"); }); + Assert.Equal("Foo3", cut.Instance.Value.Address); + } + + [Fact] + public async Task OnClearAsync_Ok() + { + var clear = false; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "
Test1
"), + new("2", "
Test2
") + }); + pb.Add(a => a.Value, "2"); + pb.Add(a => a.ShowSearch, true); + pb.Add(a => a.IsClearable, true); + pb.Add(a => a.OnClearAsync, () => + { + clear = true; + return Task.CompletedTask; + }); + }); + + var span = cut.Find(".clear-icon"); + Assert.NotNull(span); + + await cut.InvokeAsync(() => + { + span.Click(); + }); + Assert.True(clear); + } + + [Fact] + public async Task Toggle_Ok() + { + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }); + }); + await cut.Instance.Show(); + await cut.Instance.Hide(); + } + + [Fact] + public void GenericValue_Ok() + { + var items = new List>() + { + new() + { + Value = new Foo() { Id = 1, Name = "Foo1" }, + Text = "Foo1" + }, + new() + { + Value = new Foo() { Id = 2, Name = "Foo2" }, + Text = "Foo2" + } + }; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, items); + }); + } +} From 2967ecec3f282478759a9b884dc9e686fb80418d Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 15:27:55 +0800 Subject: [PATCH 06/13] =?UTF-8?q?revert:=20=E6=92=A4=E9=94=80=E5=BF=BD?= =?UTF-8?q?=E7=95=A5=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/BootstrapBlazor/Components/Select/Select.razor.cs | 1 - src/BootstrapBlazor/Components/Select/SelectOption.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/BootstrapBlazor/Components/Select/Select.razor.cs b/src/BootstrapBlazor/Components/Select/Select.razor.cs index 8ee49a0f29e..a0eebf44571 100644 --- a/src/BootstrapBlazor/Components/Select/Select.razor.cs +++ b/src/BootstrapBlazor/Components/Select/Select.razor.cs @@ -12,7 +12,6 @@ namespace BootstrapBlazor.Components; /// Select 组件实现类 ///
/// -[ExcludeFromCodeCoverage] public partial class Select : ISelect, IModelEqualityComparer { [Inject] diff --git a/src/BootstrapBlazor/Components/Select/SelectOption.cs b/src/BootstrapBlazor/Components/Select/SelectOption.cs index a293969f2da..4841fb76b4f 100644 --- a/src/BootstrapBlazor/Components/Select/SelectOption.cs +++ b/src/BootstrapBlazor/Components/Select/SelectOption.cs @@ -8,7 +8,6 @@ namespace BootstrapBlazor.Components; /// /// SelectOption 组件 /// -[ExcludeFromCodeCoverage] public class SelectOption : ComponentBase { /// From 7f320865aba673e8cf1d982985480ee5c6ccd806 Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 15:28:06 +0800 Subject: [PATCH 07/13] =?UTF-8?q?revert:=20=E6=92=A4=E9=94=80=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/SelectTest.cs | 362 ++++++++++++------------- 1 file changed, 168 insertions(+), 194 deletions(-) diff --git a/test/UnitTest/Components/SelectTest.cs b/test/UnitTest/Components/SelectTest.cs index 3d4680d1379..a757020d1cb 100644 --- a/test/UnitTest/Components/SelectTest.cs +++ b/test/UnitTest/Components/SelectTest.cs @@ -13,22 +13,15 @@ namespace UnitTest.Components; public class SelectTest : BootstrapBlazorTestBase { - [Fact] - public void SeletectedItem_Ok() - { - var item = new SelectedItem(null!, "Text"); - Assert.Equal(item.Value, string.Empty); - } - [Fact] public async Task OnSearchTextChanged_Null() { var cut = Context.RenderComponent(pb => { - pb.AddChildContent>(pb => + pb.AddChildContent>(pb => { pb.Add(a => a.ShowSearch, true); - pb.Add(a => a.Items, new List>() + pb.Add(a => a.Items, new List() { new("1", "Test1"), new("2", "Test2") { IsDisabled = true } @@ -36,7 +29,7 @@ public async Task OnSearchTextChanged_Null() }); }); - var ctx = cut.FindComponent>(); + var ctx = cut.FindComponent>(); await ctx.InvokeAsync(async () => { await ctx.Instance.ConfirmSelectedItem(0); @@ -61,7 +54,7 @@ await ctx.InvokeAsync(async () => pb.Add(a => a.OnSelectedItemChanged, null); pb.Add(a => a.OnSearchTextChanged, text => { - return new List>() + return new List() { new("1", "Test1") }; @@ -78,11 +71,11 @@ await ctx.InvokeAsync(() => [Fact] public void Options_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { pb.Add(a => a.Options, builder => { - builder.OpenComponent>(0); + builder.OpenComponent(0); builder.AddAttribute(1, "Text", "Test-Select"); builder.CloseComponent(); @@ -96,13 +89,13 @@ public void Options_Ok() [Fact] public void Disabled_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { pb.Add(a => a.IsDisabled, true); pb.Add(a => a.Options, builder => { - builder.OpenComponent>(0); - builder.AddAttribute(1, nameof(SelectOptionGeneric.IsDisabled), true); + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(SelectOption.IsDisabled), true); builder.CloseComponent(); builder.OpenComponent(2); @@ -117,10 +110,10 @@ public void Disabled_Ok() public void IsClearable_Ok() { var val = "Test2"; - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { pb.Add(a => a.IsClearable, true); - pb.Add(a => a.Items, new List>() + pb.Add(a => a.Items, new List() { new("", "请选择"), new("2", "Test2"), @@ -144,10 +137,10 @@ public void IsClearable_Ok() pb.Add(a => a.Color, Color.Danger); }); - var validPi = typeof(SelectGeneric).GetProperty("IsValid", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; + var validPi = typeof(Select).GetProperty("IsValid", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; validPi.SetValue(select.Instance, true); - var pi = typeof(SelectGeneric).GetProperty("ClearClassString", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; + var pi = typeof(Select).GetProperty("ClearClassString", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; val = pi.GetValue(select.Instance, null)!.ToString(); Assert.Contains("text-success", val); @@ -159,7 +152,7 @@ public void IsClearable_Ok() [Fact] public void SelectOption_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent(pb => { pb.Add(a => a.Text, "Test-SelectOption"); pb.Add(a => a.GroupName, "Test-GroupName"); @@ -172,14 +165,14 @@ public void SelectOption_Ok() [Fact] public void Enum_Ok() { - var cut = Context.RenderComponent>(); + var cut = Context.RenderComponent>(); Assert.Equal(2, cut.FindAll(".dropdown-item").Count); } [Fact] public void NullableEnum_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { pb.Add(a => a.AdditionalAttributes, new Dictionary() { @@ -195,9 +188,9 @@ public async Task OnSelectedItemChanged_OK() var triggered = false; // 空值时,不触发 OnSelectedItemChanged 回调 - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("", "Test"), new("1", "Test2") @@ -209,29 +202,35 @@ public async Task OnSelectedItemChanged_OK() return Task.CompletedTask; }); }); - Assert.True(triggered); + Assert.False(triggered); // 切换候选项时触发 OnSelectedItemChanged 回调测试 - var items = cut.FindAll(".dropdown-item"); - var count = items.Count; - Assert.Equal(2, count); + await cut.InvokeAsync(() => + { + var items = cut.FindAll(".dropdown-item"); + var count = items.Count; + Assert.Equal(2, count); - var item = items[1]; - await cut.InvokeAsync(() => { item.Click(); }); + var item = items[1]; + item.Click(); + }); Assert.True(triggered); // 切换回 空值 触发 OnSelectedItemChanged 回调测试 triggered = false; - items = cut.FindAll(".dropdown-item"); - item = items[0]; - await cut.InvokeAsync(() => { item.Click(); }); + await cut.InvokeAsync(() => + { + var items = cut.FindAll(".dropdown-item"); + var item = items[0]; + item.Click(); + }); Assert.True(triggered); // 首次加载值不为空时触发 OnSelectedItemChanged 回调测试 triggered = false; cut.SetParametersAndRender(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("", "Test"), new("1", "Test1"), @@ -243,44 +242,15 @@ public async Task OnSelectedItemChanged_OK() // 切换回 空值 触发 OnSelectedItemChanged 回调测试 triggered = false; - items = cut.FindAll(".dropdown-item"); - count = items.Count; - Assert.Equal(3, count); - item = items[0]; - await cut.InvokeAsync(() => { item.Click(); }); - Assert.True(triggered); - } - - [Fact] - public async Task OnSelectedItemChanged_Generic() - { - Foo? selectedValue = null; - var cut = Context.RenderComponent>(pb => + await cut.InvokeAsync(() => { - pb.Add(a => a.Items, new SelectedItem[] - { - new() { Value = new Foo() { Id = 1, Address = "Foo1" }, Text = "test1" }, - new() { Value = new Foo() { Id = 2, Address = "Foo2" }, Text = "test2" } - }); - pb.Add(a => a.Value, new Foo() { Id = 1, Address = "Foo1" }); - pb.Add(a => a.OnSelectedItemChanged, v => - { - if (v is SelectedItem d) - { - selectedValue = d.Value; - } - return Task.CompletedTask; - }); - pb.Add(a => a.CustomKeyAttribute, typeof(KeyAttribute)); + var items = cut.FindAll(".dropdown-item"); + var count = items.Count; + Assert.Equal(3, count); + var item = items[0]; + item.Click(); }); - - IModelEqualityComparer comparer = cut.Instance as IModelEqualityComparer; - Assert.NotNull(comparer); - comparer.ModelEqualityComparer = (x, y) => x.Id == y.Id; - - var items = cut.FindAll(".dropdown-item"); - await cut.InvokeAsync(() => items[1].Click()); - Assert.NotNull(selectedValue); + Assert.True(triggered); } [Fact] @@ -289,9 +259,9 @@ public void DisableItemChangedWhenFirstRender_Ok() var triggered = false; // 空值时,不触发 OnSelectedItemChanged 回调 - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test"), new("2", "Test2") @@ -310,7 +280,7 @@ public void DisableItemChangedWhenFirstRender_Ok() [Fact] public void Color_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { pb.Add(a => a.Color, Color.Danger); }); @@ -336,7 +306,7 @@ public void Validate_Ok() return Task.CompletedTask; }); builder.Add(a => a.Model, model); - builder.AddChildContent>(pb => + builder.AddChildContent>(pb => { pb.Add(a => a.Value, model.Name); pb.Add(a => a.OnValueChanged, v => @@ -345,7 +315,7 @@ public void Validate_Ok() return Task.CompletedTask; }); pb.Add(a => a.ValueExpression, model.GenerateValueExpression()); - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("", "Test"), new("1", "Test1") { GroupName = "Test1" }, @@ -361,7 +331,7 @@ public void Validate_Ok() Assert.True(valid); }); - var ctx = cut.FindComponent>(); + var ctx = cut.FindComponent>(); ctx.InvokeAsync(async () => { await ctx.Instance.ConfirmSelectedItem(0); @@ -374,9 +344,9 @@ public void Validate_Ok() [Fact] public void ItemTemplate_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1") { GroupName = "Test1" }, new("2", "Test2") { GroupName = "Test2" } @@ -396,9 +366,9 @@ public void ItemTemplate_Ok() [Fact] public void GroupItemTemplate_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1") { GroupName = "Test1" }, new("2", "Test2") { GroupName = "Test2" } @@ -419,19 +389,19 @@ public void GroupItemTemplate_Ok() [Fact] public void NullItems_Ok() { - var cut = Context.RenderComponent>(); + var cut = Context.RenderComponent>(); Assert.Contains("select", cut.Markup); } [Fact] public void NullBool_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new List> + pb.Add(a => a.Items, new List { - new(true, "True"), - new(false, "False"), + new("true", "True"), + new("false", "False"), }); pb.Add(a => a.Value, null); }); @@ -441,12 +411,32 @@ public void NullBool_Ok() Assert.True(cut.Instance.Value); } + [Fact] + public void SelectItem_Ok() + { + var v = new SelectedItem("2", "Text2"); + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, new List + { + new("1", "Text1"), + new("2", "Text2"), + }); + pb.Add(a => a.Value, v); + pb.Add(a => a.ValueChanged, EventCallback.Factory.Create(this, i => v = i)); + }); + Assert.Equal("2", cut.Instance.Value.Value); + + cut.InvokeAsync(() => cut.Find(".dropdown-item").Click()); + Assert.Equal("1", cut.Instance.Value.Value); + } + [Fact] public void SearchIcon_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -461,9 +451,9 @@ public void SearchIcon_Ok() [Fact] public void IsFixedSearch_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -479,9 +469,9 @@ public void IsFixedSearch_Ok() [Fact] public void CustomClass_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -495,9 +485,9 @@ public void CustomClass_Ok() [Fact] public void ShowShadow_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -516,9 +506,9 @@ public void ShowShadow_Ok() [Fact] public void DropdownIcon_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -532,9 +522,9 @@ public void DropdownIcon_Ok() [Fact] public void DisplayTemplate_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -551,9 +541,9 @@ public void DisplayTemplate_Ok() [Fact] public void IsPopover_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -568,9 +558,9 @@ public void IsPopover_Ok() [Fact] public void Offset_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -585,9 +575,9 @@ public void Offset_Ok() [Fact] public void Placement_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -607,9 +597,9 @@ public void Placement_Ok() [Fact] public void ItemClick_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -629,9 +619,9 @@ public void ItemClick_Ok() [Fact] public void IsVirtualize_Items() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -654,9 +644,9 @@ public void IsVirtualize_Items() [Fact] public async Task IsVirtualize_Items_Clearable_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -705,7 +695,7 @@ public async Task IsVirtualize_OnQueryAsync_Clearable_Ok() var startIndex = 0; var requestCount = 0; var searchText = string.Empty; - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { pb.Add(a => a.OnQueryAsync, option => { @@ -713,7 +703,7 @@ public async Task IsVirtualize_OnQueryAsync_Clearable_Ok() startIndex = option.StartIndex; requestCount = option.Count; searchText = option.SearchText; - return Task.FromResult(new QueryData>() + return Task.FromResult(new QueryData() { Items = string.IsNullOrEmpty(searchText) ? [new("", "All"), new("1", "Test1"), new("2", "Test2")] @@ -757,22 +747,22 @@ public async Task IsVirtualize_OnQueryAsync_Clearable_Ok() } [Fact] - public async Task IsVirtualize_BindValue() + public void IsVirtualize_BindValue() { - var value = "3"; - var cut = Context.RenderComponent>(pb => + var value = new SelectedItem("3", "Test 3"); + var cut = Context.RenderComponent>(pb => { pb.Add(a => a.Value, value); pb.Add(a => a.IsVirtualize, true); - pb.Add(a => a.ValueChanged, EventCallback.Factory.Create(this, new Action(item => + pb.Add(a => a.ValueChanged, EventCallback.Factory.Create(this, new Action(item => { value = item; }))); pb.Add(a => a.OnQueryAsync, option => { - return Task.FromResult(new QueryData>() + return Task.FromResult(new QueryData() { - Items = new SelectedItem[] + Items = new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -782,25 +772,31 @@ public async Task IsVirtualize_BindValue() }); }); - var input = cut.Find(".form-select"); - Assert.Null(input.GetAttribute("value")); - + cut.InvokeAsync(() => + { + var input = cut.Find(".form-select"); + Assert.Equal("Test 3", input.GetAttribute("value")); + }); + cut.Contains("Test 3"); var select = cut.Instance; - Assert.Equal("3", select.Value); + Assert.Equal("3", select.Value?.Value); - var item = cut.Find(".dropdown-item"); - await cut.InvokeAsync(() => { item.Click(); }); - Assert.Equal("1", value); + cut.InvokeAsync(() => + { + var item = cut.Find(".dropdown-item"); + item.Click(); + Assert.Equal("1", value.Value); - input = cut.Find(".form-select"); - Assert.Equal("Test1", input.GetAttribute("value")); + var input = cut.Find(".form-select"); + Assert.Equal("Test1", input.GetAttribute("value")); + }); } [Fact] public void IsVirtualize_DefaultVirtualizeItemText() { string? value = "3"; - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { pb.Add(a => a.IsVirtualize, true); pb.Add(a => a.DefaultVirtualizeItemText, "Test 3"); @@ -811,9 +807,9 @@ public void IsVirtualize_DefaultVirtualizeItemText() }))); pb.Add(a => a.OnQueryAsync, option => { - return Task.FromResult(new QueryData>() + return Task.FromResult(new QueryData() { - Items = new SelectedItem[] + Items = new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -833,11 +829,11 @@ public void IsVirtualize_DefaultVirtualizeItemText() [Fact] public void LoadItems_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { pb.Add(a => a.OnQueryAsync, option => { - return Task.FromResult(new QueryData>()); + return Task.FromResult(new QueryData()); }); pb.Add(a => a.Value, "2"); pb.Add(a => a.IsVirtualize, true); @@ -851,12 +847,40 @@ public void LoadItems_Ok() mi?.Invoke(select, [new ItemsProviderRequest(0, 1, CancellationToken.None)]); } + [Fact] + public void TryParseValueFromString_Ok() + { + var items = new SelectedItem[] + { + new("1", "Test1"), + new("2", "Test2") + }; + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.Items, items); + pb.Add(a => a.Value, new SelectedItem("1", "Test1")); + pb.Add(a => a.IsVirtualize, true); + }); + var select = cut.Instance; + var mi = select.GetType().GetMethod("TryParseSelectItem", BindingFlags.NonPublic | BindingFlags.Instance); + + string value = ""; + SelectedItem result = new(); + string? msg = null; + mi?.Invoke(select, [value, result, msg]); + + var p = select.GetType().GetProperty("VirtualItems", BindingFlags.NonPublic | BindingFlags.Instance); + p?.SetValue(select, items); + value = "1"; + mi?.Invoke(select, [value, result, msg]); + } + [Fact] public void IsMarkupString_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "
Test1
"), new("2", "
Test2
") @@ -870,9 +894,9 @@ public void IsMarkupString_Ok() [Fact] public async Task IsEditable_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "
Test1
"), new("2", "
Test2
") @@ -891,52 +915,24 @@ public async Task IsEditable_Ok() updated = true; return Task.CompletedTask; }); - pb.Add(a => a.TextConvertToValueCallback, v => - { - return Task.FromResult(v); - }); }); Assert.False(input.IsReadOnly()); - await cut.InvokeAsync(() => { input.Change("Test3"); }); - Assert.Equal("Test3", cut.Instance.Value); - Assert.True(updated); - } - - [Fact] - public async Task IsEditable_Generic() - { - var items = new List>() - { - new() { Value = new Foo() { Id = 1, Address = "Foo1" }, Text = "test1" }, - new() { Value = new Foo() { Id = 2, Address = "Foo2" }, Text = "test2" } - }; - var cut = Context.RenderComponent>(pb => + await cut.InvokeAsync(() => { - pb.Add(a => a.Items, items); - pb.Add(a => a.Value, new Foo() { Id = 1, Address = "Foo1" }); - pb.Add(a => a.IsEditable, true); - pb.Add(a => a.TextConvertToValueCallback, v => - { - return Task.FromResult(new Foo() { Id = 3, Address = "Foo3" }); - }); + input.Change("Test3"); }); - - var input = cut.Find(".form-select"); - await cut.InvokeAsync(() => { input.Change("test2"); }); - Assert.Equal("Foo2", cut.Instance.Value.Address); - - await cut.InvokeAsync(() => { input.Change("test3"); }); - Assert.Equal("Foo3", cut.Instance.Value.Address); + Assert.Equal("Test3", cut.Instance.Value); + Assert.True(updated); } [Fact] public async Task OnClearAsync_Ok() { var clear = false; - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "
Test1
"), new("2", "
Test2
") @@ -964,9 +960,9 @@ await cut.InvokeAsync(() => [Fact] public async Task Toggle_Ok() { - var cut = Context.RenderComponent>(pb => + var cut = Context.RenderComponent>(pb => { - pb.Add(a => a.Items, new SelectedItem[] + pb.Add(a => a.Items, new SelectedItem[] { new("1", "Test1"), new("2", "Test2") @@ -975,26 +971,4 @@ public async Task Toggle_Ok() await cut.Instance.Show(); await cut.Instance.Hide(); } - - [Fact] - public void GenericValue_Ok() - { - var items = new List>() - { - new() - { - Value = new Foo() { Id = 1, Name = "Foo1" }, - Text = "Foo1" - }, - new() - { - Value = new Foo() { Id = 2, Name = "Foo2" }, - Text = "Foo2" - } - }; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, items); - }); - } } From 5e08bc37a738c40fc00fdb2788b1f8708bd879ed Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 16:09:01 +0800 Subject: [PATCH 08/13] =?UTF-8?q?revert:=20=E6=81=A2=E5=A4=8D=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=B3=9B=E5=9E=8B=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Select/Select.razor.cs | 60 +------------------ 1 file changed, 2 insertions(+), 58 deletions(-) diff --git a/src/BootstrapBlazor/Components/Select/Select.razor.cs b/src/BootstrapBlazor/Components/Select/Select.razor.cs index a0eebf44571..bb8689e57df 100644 --- a/src/BootstrapBlazor/Components/Select/Select.razor.cs +++ b/src/BootstrapBlazor/Components/Select/Select.razor.cs @@ -12,7 +12,7 @@ namespace BootstrapBlazor.Components; /// Select 组件实现类 ///
/// -public partial class Select : ISelect, IModelEqualityComparer +public partial class Select : ISelect { [Inject] [NotNull] @@ -92,13 +92,6 @@ public partial class Select : ISelect, IModelEqualityComparer [Parameter] public Func? OnInputChangedCallback { get; set; } - /// - /// 获得/设置 选项输入更新后转换为 Value 回调方法 默认 null - /// - /// 设置 后生效 - [Parameter] - public Func>? TextConvertToValueCallback { get; set; } - /// /// 获得/设置 无搜索结果时显示文字 /// @@ -168,26 +161,6 @@ public partial class Select : ISelect, IModelEqualityComparer [Parameter] public bool DisableItemChangedWhenFirstRender { get; set; } - /// - /// 获得/设置 比较数据是否相同回调方法 默认为 null - /// 提供此回调方法时忽略 属性 - /// - [Parameter] - public Func? ValueEqualityComparer { get; set; } - - Func? IModelEqualityComparer.ModelEqualityComparer - { - get => ValueEqualityComparer; - set => ValueEqualityComparer = value; - } - - /// - /// 获得/设置 数据主键标识标签 默认为 用于判断数据主键标签,如果模型未设置主键时可使用 参数自定义判断数据模型支持联合主键 - /// - [Parameter] - [NotNull] - public Type? CustomKeyAttribute { get; set; } = typeof(KeyAttribute); - [NotNull] private Virtualize? VirtualizeElement { get; set; } @@ -481,27 +454,6 @@ private async Task OnClickItem(SelectedItem item) } private async Task SelectedItemChanged(SelectedItem item) - { - if (item is SelectedItem d && !Equals(d.Value, Value)) - { - item.Active = true; - SelectedItem = item; - - CurrentValue = d.Value; - - // 触发 SelectedItemChanged 事件 - if (OnSelectedItemChanged != null) - { - await OnSelectedItemChanged(SelectedItem); - } - } - else - { - await ValueTypeChanged(item); - } - } - - private async Task ValueTypeChanged(SelectedItem item) { if (_lastSelectedValueString != item.Value) { @@ -510,7 +462,7 @@ private async Task ValueTypeChanged(SelectedItem item) SelectedItem = item; // 触发 StateHasChanged - _lastSelectedValueString = item.Value ?? ""; + _lastSelectedValueString = item.Value; CurrentValueAsString = _lastSelectedValueString; // 触发 SelectedItemChanged 事件 @@ -584,12 +536,4 @@ private async Task OnChange(ChangeEventArgs args) } } } - - /// - /// - /// - /// - /// - /// - public bool Equals(TValue? x, TValue? y) => this.Equals(x, y); } From 7ecc52d6b63a0e630b1dfcfa5a6e4871a0690691 Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 16:09:07 +0800 Subject: [PATCH 09/13] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/SelectTest.cs | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/UnitTest/Components/SelectTest.cs b/test/UnitTest/Components/SelectTest.cs index a757020d1cb..e8499a8be79 100644 --- a/test/UnitTest/Components/SelectTest.cs +++ b/test/UnitTest/Components/SelectTest.cs @@ -971,4 +971,44 @@ public async Task Toggle_Ok() await cut.Instance.Show(); await cut.Instance.Hide(); } + + [Fact] + public async Task OnBeforeSelectedItemChange_OK() + { + var cut = Context.RenderComponent(pb => + { + pb.AddChildContent>(pb => + { + pb.Add(a => a.Items, new List() + { + new("1", "Test1"), + new("2", "Test2") { IsDisabled = true } + }); + pb.Add(a => a.SwalCategory, SwalCategory.Question); + pb.Add(a => a.SwalTitle, "Swal-Title"); + pb.Add(a => a.SwalContent, "Swal-Content"); + pb.Add(a => a.OnBeforeSelectedItemChange, item => Task.FromResult(true)); + pb.Add(a => a.OnSelectedItemChanged, item => Task.CompletedTask); + pb.Add(a => a.SwalFooter, "test-swal-footer"); + }); + }); + var modals = cut.FindComponents(); + var modal = modals[modals.Count - 1]; + _ = Task.Run(() => cut.InvokeAsync(() => cut.FindComponent>().Instance.ConfirmSelectedItem(0))); + var tick = DateTime.Now; + while (!cut.Markup.Contains("test-swal-footer")) + { + Thread.Sleep(100); + if (DateTime.Now > tick.AddSeconds(2)) + { + break; + } + } + var button = cut.Find(".btn-danger"); + await cut.InvokeAsync(() => + { + button.Click(); + }); + await cut.InvokeAsync(() => modal.Instance.CloseCallback()); + } } From 36b8707eb6cc1e2e73f3333c1c4a2437a5cc1fd3 Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 20:15:37 +0800 Subject: [PATCH 10/13] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=94=B9=E5=8F=AF?= =?UTF-8?q?=E8=A7=81=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Select/VirtualizeQueryOption.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BootstrapBlazor/Components/Select/VirtualizeQueryOption.cs b/src/BootstrapBlazor/Components/Select/VirtualizeQueryOption.cs index 0f4fa59c0ac..6e99078fbd0 100644 --- a/src/BootstrapBlazor/Components/Select/VirtualizeQueryOption.cs +++ b/src/BootstrapBlazor/Components/Select/VirtualizeQueryOption.cs @@ -13,15 +13,15 @@ public class VirtualizeQueryOption /// /// 请求记录开始索引 /// - public int StartIndex { get; internal set; } + public int StartIndex { get; set; } /// /// 请求记录总数 /// - public int Count { get; internal set; } + public int Count { get; set; } /// /// Select 组件搜索文本 /// - public string? SearchText { get; internal set; } + public string? SearchText { get; set; } } From 55f24ccbca766453f982e7d8b8731a2ce16678fa Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 20:15:51 +0800 Subject: [PATCH 11/13] =?UTF-8?q?refactor:=20=E7=A7=BB=E5=8A=A8=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=88=B0=E7=8B=AC=E7=AB=8B=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SelectGeneric/ISelectGeneric.cs | 18 - .../SelectGeneric/SelectGeneric.razor | 115 ---- .../SelectGeneric/SelectGeneric.razor.cs | 567 ------------------ .../SelectGeneric/SelectGeneric.razor.js | 139 ----- .../SelectGeneric/SelectGeneric.razor.scss | 241 -------- .../SelectGeneric/SelectOptionGeneric.cs | 65 -- 6 files changed, 1145 deletions(-) delete mode 100644 src/BootstrapBlazor/Components/SelectGeneric/ISelectGeneric.cs delete mode 100644 src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor delete mode 100644 src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs delete mode 100644 src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.js delete mode 100644 src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.scss delete mode 100644 src/BootstrapBlazor/Components/SelectGeneric/SelectOptionGeneric.cs diff --git a/src/BootstrapBlazor/Components/SelectGeneric/ISelectGeneric.cs b/src/BootstrapBlazor/Components/SelectGeneric/ISelectGeneric.cs deleted file mode 100644 index de6d9af06ca..00000000000 --- a/src/BootstrapBlazor/Components/SelectGeneric/ISelectGeneric.cs +++ /dev/null @@ -1,18 +0,0 @@ -// 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; - -/// -/// ISelect 接口 -/// -public interface ISelectGeneric -{ - /// - /// 增加 SelectedItem 项方法 - /// - /// - void Add(SelectedItem item); -} diff --git a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor deleted file mode 100644 index cd5694609a8..00000000000 --- a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor +++ /dev/null @@ -1,115 +0,0 @@ -@namespace BootstrapBlazor.Components -@using Microsoft.AspNetCore.Components.Web.Virtualization -@typeparam TValue -@inherits SelectBase -@attribute [BootstrapModuleAutoLoader(JSObjectReference = true)] - -@if (IsShowLabel) -{ - -} -
- - @Options - - - - @if (GetClearable()) - { - - } - - @if (!IsPopover) - { - - } - -
- -@code { - RenderFragment> RenderRow => item => - @
- @if (ItemTemplate != null) - { - @ItemTemplate(item) - } - else if (IsMarkupString) - { - @((MarkupString)item.Text) - } - else - { - @item.Text - } -
; - - RenderFragment RenderPlaceHolderRow => context => - @; -} diff --git a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs deleted file mode 100644 index c5ade71f620..00000000000 --- a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs +++ /dev/null @@ -1,567 +0,0 @@ -// 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.Web.Virtualization; -using Microsoft.Extensions.Localization; - -namespace BootstrapBlazor.Components; - -/// -/// Select 组件实现类 -/// -/// -[CascadingTypeParameter(nameof(TValue))] -public partial class SelectGeneric : ISelectGeneric, IModelEqualityComparer -{ - [Inject] - [NotNull] - private SwalService? SwalService { get; set; } - - /// - /// 获得 样式集合 - /// - private string? ClassString => CssBuilder.Default("select dropdown") - .AddClass("cls", IsClearable) - .AddClassFromAttributes(AdditionalAttributes) - .Build(); - - /// - /// 获得 样式集合 - /// - private string? InputClassString => CssBuilder.Default("form-select form-control") - .AddClass($"border-{Color.ToDescriptionString()}", Color != Color.None && !IsDisabled && !IsValid.HasValue) - .AddClass($"border-success", IsValid.HasValue && IsValid.Value) - .AddClass($"border-danger", IsValid.HasValue && !IsValid.Value) - .AddClass(CssClass).AddClass(ValidCss) - .Build(); - - private string? ClearClassString => CssBuilder.Default("clear-icon") - .AddClass($"text-{Color.ToDescriptionString()}", Color != Color.None) - .AddClass($"text-success", IsValid.HasValue && IsValid.Value) - .AddClass($"text-danger", IsValid.HasValue && !IsValid.Value) - .Build(); - - private bool GetClearable() => IsClearable && !IsDisabled; - - /// - /// 设置当前项是否 Active 方法 - /// - /// - /// - private string? ActiveItem(SelectedItem item) => CssBuilder.Default("dropdown-item") - .AddClass("active", Equals(item.Value, Value)) - .AddClass("disabled", item.IsDisabled) - .Build(); - - private string? SearchClassString => CssBuilder.Default("search") - .AddClass("is-fixed", IsFixedSearch) - .Build(); - - private readonly List> _children = []; - - /// - /// 获得/设置 右侧清除图标 默认 fa-solid fa-angle-up - /// - [Parameter] - [NotNull] - public string? ClearIcon { get; set; } - - /// - /// 获得/设置 搜索文本发生变化时回调此方法 - /// - [Parameter] - public Func>>? OnSearchTextChanged { get; set; } - - /// - /// 获得/设置 是否固定下拉框中的搜索栏 默认 false - /// - [Parameter] - public bool IsFixedSearch { get; set; } - - /// - /// 获得/设置 是否可编辑 默认 false - /// - [Parameter] - public bool IsEditable { get; set; } - - /// - /// 获得/设置 选项输入更新后回调方法 默认 null - /// - /// 设置 后生效 - [Parameter] - public Func? OnInputChangedCallback { get; set; } - - /// - /// 获得/设置 选项输入更新后转换为 Value 回调方法 默认 null - /// - /// 设置 后生效 - [Parameter] - public Func>? TextConvertToValueCallback { get; set; } - - /// - /// 获得/设置 无搜索结果时显示文字 - /// - [Parameter] - public string? NoSearchDataText { get; set; } - - /// - /// 获得 PlaceHolder 属性 - /// - [Parameter] - public string? PlaceHolder { get; set; } - - /// - /// 获得/设置 是否可清除 默认 false - /// - [Parameter] - public bool IsClearable { get; set; } - - /// - /// 获得/设置 选项模板支持静态数据 - /// - [Parameter] - public RenderFragment? Options { get; set; } - - /// - /// 获得/设置 显示部分模板 默认 null - /// - [Parameter] - public RenderFragment?>? DisplayTemplate { get; set; } - - /// - /// 获得/设置 是否开启虚拟滚动 默认 false 未开启 注意:开启虚拟滚动后不支持 参数设置,设置初始值时请设置 - /// - [Parameter] - public bool IsVirtualize { get; set; } - - /// - /// 获得/设置 虚拟滚动行高 默认为 33 - /// - /// 需要设置 值为 true 时生效 - [Parameter] - public float RowHeight { get; set; } = 33f; - - /// - /// 获得/设置 过载阈值数 默认为 4 - /// - /// 需要设置 值为 true 时生效 - [Parameter] - public int OverscanCount { get; set; } = 4; - - /// - /// 获得/设置 默认文本 时生效 默认 null - /// - /// 开启 并且通过 提供数据源时,由于渲染时还未调用或者调用后数据集未包含 选项值,此时使用 DefaultText 值渲染 - [Parameter] - public string? DefaultVirtualizeItemText { get; set; } - - /// - /// 获得/设置 清除文本内容 OnClear 回调方法 默认 null - /// - [Parameter] - public Func? OnClearAsync { get; set; } - - /// - /// 获得/设置 禁止首次加载时触发 OnSelectedItemChanged 回调方法 默认 false - /// - [Parameter] - public bool DisableItemChangedWhenFirstRender { get; set; } - - /// - /// 获得/设置 比较数据是否相同回调方法 默认为 null - /// 提供此回调方法时忽略 属性 - /// - [Parameter] - public Func? ValueEqualityComparer { get; set; } - - Func? IModelEqualityComparer.ModelEqualityComparer - { - get => ValueEqualityComparer; - set => ValueEqualityComparer = value; - } - - /// - /// 获得/设置 数据主键标识标签 默认为 用于判断数据主键标签,如果模型未设置主键时可使用 参数自定义判断数据模型支持联合主键 - /// - [Parameter] - [NotNull] - public Type? CustomKeyAttribute { get; set; } = typeof(KeyAttribute); - - [NotNull] - private Virtualize>? VirtualizeElement { get; set; } - - /// - /// 获得/设置 绑定数据集 - /// - [Parameter] - [NotNull] - public IEnumerable>? Items { get; set; } - - /// - /// 获得/设置 选项模板 - /// - [Parameter] - public RenderFragment>? ItemTemplate { get; set; } - - /// - /// 获得/设置 下拉框项目改变前回调委托方法 返回 true 时选项值改变,否则选项值不变 - /// - [Parameter] - public Func, Task>? OnBeforeSelectedItemChange { get; set; } - - /// - /// SelectedItemChanged 回调方法 - /// - [Parameter] - public Func, Task>? OnSelectedItemChanged { get; set; } - - /// - /// 获得/设置 Swal 图标 默认 Question - /// - [Parameter] - public SwalCategory SwalCategory { get; set; } = SwalCategory.Question; - - /// - /// 获得/设置 Swal 标题 默认 null - /// - [Parameter] - public string? SwalTitle { get; set; } - - /// - /// 获得/设置 Swal 内容 默认 null - /// - [Parameter] - public string? SwalContent { get; set; } - - /// - /// 获得/设置 Footer 默认 null - /// - [Parameter] - public string? SwalFooter { get; set; } - - [Inject] - [NotNull] - private IStringLocalizer>? Localizer { get; set; } - - /// - /// 获得 input 组件 Id 方法 - /// - /// - protected override string? RetrieveId() => InputId; - - /// - /// 获得/设置 Select 内部 Input 组件 Id - /// - private string? InputId => $"{Id}_input"; - - private TValue? _lastSelectedValue; - - private bool _init = true; - - private List>? _itemsCache; - - private ItemsProviderResult> _result; - - /// - /// 当前选择项实例 - /// - private SelectedItem? SelectedItem { get; set; } - - private List> Rows - { - get - { - _itemsCache ??= string.IsNullOrEmpty(SearchText) ? GetRowsByItems() : GetRowsBySearch(); - return _itemsCache; - } - } - - private SelectedItem SelectedRow - { - get - { - SelectedItem ??= GetSelectedRow(); - return SelectedItem; - } - } - - private SelectedItem GetSelectedRow() - { - var item = Rows.Find(i => Equals(i.Value, Value)) - ?? Rows.Find(i => i.Active) - ?? Rows.Where(i => !i.IsDisabled).FirstOrDefault() - ?? new SelectedItem(Value, DefaultVirtualizeItemText!); - - if (!_init || !DisableItemChangedWhenFirstRender) - { - _ = SelectedItemChanged(item); - _init = false; - } - return item; - } - - private List> GetRowsByItems() - { - var items = new List>(); - items.AddRange(Items); - items.AddRange(_children); - return items; - } - - private List> GetRowsBySearch() - { - var items = OnSearchTextChanged?.Invoke(SearchText) ?? FilterBySearchText(GetRowsByItems()); - return items.ToList(); - } - - private IEnumerable> FilterBySearchText(IEnumerable> source) => string.IsNullOrEmpty(SearchText) - ? source - : source.Where(i => i.Text.Contains(SearchText, StringComparison)); - - /// - /// - /// - protected override void OnParametersSet() - { - base.OnParametersSet(); - - Items ??= []; - PlaceHolder ??= Localizer[nameof(PlaceHolder)]; - NoSearchDataText ??= Localizer[nameof(NoSearchDataText)]; - DropdownIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectDropdownIcon); - ClearIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectClearIcon); - - // 内置对枚举类型的支持 - if (!Items.Any() && ValueType.IsEnum()) - { - var item = NullableUnderlyingType == null ? "" : PlaceHolder; - Items = ValueType.ToSelectList(string.IsNullOrEmpty(item) ? null : new SelectedItem(default!, item)); - } - - _itemsCache = null; - SelectedItem = null; - } - - /// - /// 获得/设置 数据总条目 - /// - private int TotalCount { get; set; } - - private List> GetVirtualItems() => FilterBySearchText(GetRowsByItems()).ToList(); - - /// - /// 虚拟滚动数据加载回调方法 - /// - [Parameter] - [NotNull] - public Func>>>? OnQueryAsync { get; set; } - - private async ValueTask>> LoadItems(ItemsProviderRequest request) - { - // 有搜索条件时使用原生请求数量 - // 有总数时请求剩余数量 - var count = !string.IsNullOrEmpty(SearchText) ? request.Count : GetCountByTotal(); - var data = await OnQueryAsync(new() { StartIndex = request.StartIndex, Count = count, SearchText = SearchText }); - - TotalCount = data.TotalCount; - var items = data.Items ?? []; - _result = new ItemsProviderResult>(items, TotalCount); - return _result; - - int GetCountByTotal() => TotalCount == 0 ? request.Count : Math.Min(request.Count, TotalCount - request.StartIndex); - } - - private async Task SearchTextChanged(string val) - { - SearchText = val; - _itemsCache = null; - - if (OnQueryAsync != null) - { - // 通过 ItemProvider 提供数据 - await VirtualizeElement.RefreshDataAsync(); - } - } - - /// - /// - /// - /// - protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(ConfirmSelectedItem)); - - /// - /// 客户端回车回调方法 - /// - /// - /// - [JSInvokable] - public async Task ConfirmSelectedItem(int index) - { - if (index < Rows.Count) - { - await OnClickItem(Rows[index]); - StateHasChanged(); - } - } - - /// - /// 下拉框选项点击时调用此方法 - /// - private async Task OnClickItem(SelectedItem item) - { - var ret = true; - if (OnBeforeSelectedItemChange != null) - { - ret = await OnBeforeSelectedItemChange(item); - if (ret) - { - // 返回 True 弹窗提示 - var option = new SwalOption() - { - Category = SwalCategory, - Title = SwalTitle, - Content = SwalContent - }; - if (!string.IsNullOrEmpty(SwalFooter)) - { - option.ShowFooter = true; - option.FooterTemplate = builder => builder.AddContent(0, SwalFooter); - } - ret = await SwalService.ShowModal(option); - } - else - { - // 返回 False 直接运行 - ret = true; - } - } - if (ret) - { - await SelectedItemChanged(item); - } - } - - private async Task SelectedItemChanged(SelectedItem item) - { - if (!Equals(item.Value, Value)) - { - item.Active = true; - SelectedItem = item; - - CurrentValue = item.Value; - - // 触发 SelectedItemChanged 事件 - if (OnSelectedItemChanged != null) - { - await OnSelectedItemChanged(SelectedItem); - } - } - else - { - await ValueTypeChanged(item); - } - } - - private async Task ValueTypeChanged(SelectedItem item) - { - if (!Equals(_lastSelectedValue, item.Value)) - { - _lastSelectedValue = item.Value; - - item.Active = true; - SelectedItem = item; - - // 触发 StateHasChanged - CurrentValue = item.Value; - - // 触发 SelectedItemChanged 事件 - if (OnSelectedItemChanged != null) - { - await OnSelectedItemChanged(SelectedItem); - } - } - } - - /// - /// 添加静态下拉项方法 - /// - /// - public void Add(SelectedItem item) => _children.Add(item); - - /// - /// 清空搜索栏文本内容 - /// - public void ClearSearchText() => SearchText = null; - - private async Task OnClearValue() - { - if (ShowSearch) - { - ClearSearchText(); - } - if (OnClearAsync != null) - { - await OnClearAsync(); - } - - SelectedItem? item; - if (OnQueryAsync != null) - { - await VirtualizeElement.RefreshDataAsync(); - item = _result.Items.FirstOrDefault(); - } - else - { - item = Items.FirstOrDefault(); - } - if (item != null) - { - await SelectedItemChanged(item); - } - } - - private string? ReadonlyString => IsEditable ? null : "readonly"; - - private async Task OnChange(ChangeEventArgs args) - { - if (args.Value is string v) - { - // Items 中没有时插入一个 SelectedItem - var item = Items.FirstOrDefault(i => i.Text == v); - - if (item == null) - { - TValue? val = default; - if (TextConvertToValueCallback != null) - { - val = await TextConvertToValueCallback(v); - } - item = new SelectedItem(val, v); - - var items = new List>() { item }; - items.AddRange(Items); - Items = items; - CurrentValue = val; - } - else - { - CurrentValue = item.Value; - } - - if (OnInputChangedCallback != null) - { - await OnInputChangedCallback(v); - } - } - } - - /// - /// - /// - /// - /// - /// - public bool Equals(TValue? x, TValue? y) => this.Equals(x, y); -} diff --git a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.js b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.js deleted file mode 100644 index 5f18ce671ff..00000000000 --- a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.js +++ /dev/null @@ -1,139 +0,0 @@ -import { getHeight, getInnerHeight, getTransitionDelayDurationFromElement } from "../../modules/utility.js" -import Data from "../../modules/data.js" -import EventHandler from "../../modules/event-handler.js" -import Popover from "../../modules/base-popover.js" - -export function init(id, invoke, method) { - const el = document.getElementById(id) - - if (el == null) { - return - } - - const search = el.querySelector("input.search-text") - const popover = Popover.init(el) - - const shown = () => { - if (search) { - search.focus(); - } - const prev = popover.toggleMenu.querySelector('.dropdown-item.preActive') - if (prev) { - prev.classList.remove('preActive') - } - scrollToActive(popover.toggleMenu, prev) - } - - const keydown = e => { - if (popover.toggleElement.classList.contains('show')) { - const items = popover.toggleMenu.querySelectorAll('.dropdown-item:not(.search, .disabled)') - let activeItem = popover.toggleMenu.querySelector('.dropdown-item.preActive') - if (activeItem == null) activeItem = popover.toggleMenu.querySelector('.dropdown-item.active') - - if (activeItem) { - if (items.length > 1) { - activeItem.classList.remove('preActive') - if (e.key === "ArrowUp") { - do { - activeItem = activeItem.previousElementSibling - } - while (activeItem && !activeItem.classList.contains('dropdown-item')) - if (!activeItem) { - activeItem = items[items.length - 1] - } - activeItem.classList.add('preActive') - scrollToActive(popover.toggleMenu, activeItem) - e.preventDefault() - e.stopPropagation() - } - else if (e.key === "ArrowDown") { - do { - activeItem = activeItem.nextElementSibling - } - while (activeItem && !activeItem.classList.contains('dropdown-item')) - if (!activeItem) { - activeItem = items[0] - } - activeItem.classList.add('preActive') - scrollToActive(popover.toggleMenu, activeItem) - e.preventDefault() - e.stopPropagation() - } - } - - if (e.key === "Enter") { - popover.toggleMenu.classList.remove('show') - let index = indexOf(el, activeItem) - invoke.invokeMethodAsync(method, index) - } - } - } - } - - EventHandler.on(el, 'shown.bs.dropdown', shown); - EventHandler.on(el, 'keydown', keydown) - - const select = { - el, - popover - } - Data.set(id, select) -} - -export function show(id) { - const select = Data.get(id) - if (select) { - const delay = getTransitionDelayDurationFromElement(select.popover.toggleElement); - const handler = setTimeout(() => { - clearTimeout(handler); - select.popover.show(); - }, delay); - } -} - -export function hide(id) { - const select = Data.get(id) - const delay = getTransitionDelayDurationFromElement(select.popover.toggleElement); - if (select) { - const handler = setTimeout(() => { - clearTimeout(handler); - select.popover.hide(); - }, delay) - } -} - -export function dispose(id) { - const select = Data.get(id) - Data.remove(id) - - if (select) { - EventHandler.off(select.el, 'shown.bs.dropdown') - EventHandler.off(select.el, 'keydown') - Popover.dispose(select.popover) - } -} - - -function scrollToActive(el, activeItem) { - if (!activeItem) { - activeItem = el.querySelector('.dropdown-item.active') - } - - if (activeItem) { - const innerHeight = getInnerHeight(el) - const itemHeight = getHeight(activeItem); - const index = indexOf(el, activeItem) - const margin = itemHeight * index - (innerHeight - itemHeight) / 2; - if (margin >= 0) { - el.scrollTo(0, margin); - } - else { - el.scrollTo(0, 0); - } - } -} - -function indexOf(el, element) { - const items = el.querySelectorAll('.dropdown-item') - return Array.prototype.indexOf.call(items, element) -} diff --git a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.scss b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.scss deleted file mode 100644 index abb759cc40b..00000000000 --- a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.scss +++ /dev/null @@ -1,241 +0,0 @@ -.select, -.popover-dropdown { - --bb-dropdown-link-pre-active-bg: #{$bb-dropdown-link-pre-active-bg}; -} - -.select { - --bb-select-focus-shadow: #{$bb-select-focus-shadow}; - --bb-select-padding-right: #{$bb-select-padding-right}; - --bb-select-padding: #{$bb-select-padding}; - --bb-select-search-padding: #{$bb-select-search-padding}; - --bb-select-search-margin-bottom: #{$bb-select-search-margin-bottom}; - --bb-select-search-border-color: #{$bb-select-search-border-color}; - --bb-select-search-padding-right: #{$bb-select-search-padding-right}; - --bb-select-search-icon-color: #{$bb-select-search-icon-color}; - --bb-select-search-icon-right: #{$bb-select-search-icon-right}; - --bb-select-search-icon-top: #{$bb-select-search-icon-top}; - --bb-select-search-height: #{$bb-select-search-height}; - --bb-select-append-width: #{$bb-select-append-width}; - --bb-select-append-color: #{$bb-select-append-color}; -} - -.select:not(.cascade) .dropdown-menu { - overflow-x: hidden; - width: 100%; -} - -.cascade, -.select { - --bb-select-dropdown-menu-margin-top: 8px; -} - -.cascade .dropdown-menu, -.selec .dropdown-menu { - margin-block-start: var(--bb-select-dropdown-menu-margin-top) !important; -} - -.select .form-select { - background-image: none; - background-color: var(--bs-body-bg); - border: var(--bs-border-width) solid var(--bs-border-color); - border-radius: var(--bs-border-radius); - padding: var(--bb-select-padding); - padding-inline-end: var(--bb-select-padding-right); - cursor: pointer; -} - -.select .form-select:disabled { - background-color: var(--bs-secondary-bg); -} - -.dropdown-menu { - --bs-dropdown-border-radius: var(--bs-border-radius); - overflow: auto; - max-height: var(--bb-dropdown-max-height); -} - -.dropdown-menu .dropdown-virtual { - overflow-y: auto; - margin: calc(0px - var(--bs-dropdown-padding-y)) var(--bs-dropdown-padding-x); - max-height: calc(var(--bb-dropdown-max-height) - 2px); - padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x); -} - -.dropdown-menu .search + .dropdown-virtual { - max-height: calc(var(--bb-dropdown-max-height) - var(--bb-select-search-height)); -} - -.dropdown-item { - cursor: pointer; -} - -.dropdown-item.preActive { - background-color: var(--bb-dropdown-link-pre-active-bg); -} - -.dropdown-menu-arrow { - width: 0; - height: 0; - border-width: 0 6px 6px; - border-style: solid; - border-color: transparent transparent rgba(0,0,0,.15); - position: absolute; - left: 20px; - margin-block-start: 4px; - z-index: 1001; - display: none; -} - -.dropdown-menu-arrow:after { - content: " "; - width: 0; - height: 0; - border-width: 0 6px 6px; - border-style: solid; - border-color: transparent transparent var(--bs-body-bg); - position: absolute; - top: 1px; - left: -6px; -} - -[data-bs-theme='dark'] .dropdown-menu-arrow:after { - content: none; -} - -.show > .dropdown-menu, -.show > .dropdown-menu-arrow { - display: block; -} - -.form-select:focus { - box-shadow: var(--bb-select-focus-shadow); - border-color: var(--bb-border-focus-color); -} - -.form-select:not(:disabled):hover { - border-color: var(--bb-border-hover-color); -} - -.form-select.show + .form-select-append i { - transform: rotate(0); -} - -.dropdown-menu[data-popper-placement="bottom-start"].show + .dropdown-menu-arrow, -.dropdown-menu[data-bs-popper="none"].show + .dropdown-menu-arrow { - display: block; -} - -.form-select-append { - position: absolute; - height: 100%; - width: var(--bb-select-append-width); - right: 0; - top: 0; - color: var(--bb-select-append-color); - pointer-events: none; - display: flex; - align-items: center; - justify-content: center; -} - -.form-select-append i { - transition: all .3s; - transform: rotate(180deg); -} - -.show > .form-select-append i { - transform: rotate(0); -} - -.select .clear-icon { - position: absolute; - height: 100%; - width: var(--bb-select-append-width); - right: 0; - top: 0; - color: var(--bb-select-append-color); - align-items: center; - justify-content: center; - cursor: pointer; - display: none; -} - -.select:hover .clear-icon { - display: flex; -} - -.select.cls:hover .form-select-append { - display: none; -} - -.form-select.is-valid:focus, -.was-validated .form-select:valid:focus, -.form-select.is-invalid:focus, -.was-validated .form-select:invalid:focus { - box-shadow: none; -} - -.form-select.is-valid:not([multiple]):not([size]), -.form-select.is-valid:not([multiple])[size="1"], -.was-validated .form-select:valid:not([multiple]):not([size]), -.was-validated .form-select:valid:not([multiple])[size="1"], -.form-select.is-invalid:not([multiple]):not([size]), -.form-select.is-invalid:not([multiple])[size="1"], -.was-validated .form-select:invalid:not([multiple]):not([size]), -.was-validated .form-select:invalid:not([multiple])[size="1"] { - background-position: right -1rem center, center right 1.5rem; - padding-inline-end: var(--bb-select-padding-right); -} - -.arrow-danger { - border-color: transparent transparent var(--bs-danger); -} - -.arrow-success { - border-color: transparent transparent var(--bs-success); -} - -.arrow-primary { - border-color: transparent transparent var(--bs-primary); -} - -.arrow-warning { - border-color: transparent transparent var(--bs-warning); -} - -.arrow-info { - border-color: transparent transparent var(--bs-info); -} - -.dropdown-menu .search { - padding: var(--bb-select-search-padding); - position: relative; - border-block-end: var(--bs-border-width) solid var(--bb-select-search-border-color); - margin-block-end: var(--bb-select-search-margin-bottom); -} - -.dropdown-menu .search.is-fixed { - position: sticky; - top: calc(-1 * var(--bs-dropdown-padding-y)); - background-color: var(--bs-dropdown-bg); -} - -.dropdown-menu .search .search-text { - padding-inline-end: var(--bb-select-search-padding-right); -} - -.dropdown-menu .search .icon { - position: absolute; - right: var(--bb-select-search-icon-right); - top: var(--bb-select-search-icon-top); - color: var(--bb-select-search-icon-color); -} - -.select:not(.multi-select) .dropdown-toggle { - position: relative; -} - -.select .dropdown-toggle:after, -.btn-popover-confirm.dropdown-toggle:after { - content: none; -} diff --git a/src/BootstrapBlazor/Components/SelectGeneric/SelectOptionGeneric.cs b/src/BootstrapBlazor/Components/SelectGeneric/SelectOptionGeneric.cs deleted file mode 100644 index 8dc1751696f..00000000000 --- a/src/BootstrapBlazor/Components/SelectGeneric/SelectOptionGeneric.cs +++ /dev/null @@ -1,65 +0,0 @@ -// 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; - -/// -/// SelectOptionPro 组件 -/// -public class SelectOptionGeneric : ComponentBase -{ - /// - /// 获得/设置 显示名称 - /// - [Parameter] - public string? Text { get; set; } - - /// - /// 获得/设置 选项值 - /// - [Parameter] - public TValue? Value { get; set; } - - /// - /// 获得/设置 是否选中 默认 false - /// - [Parameter] - public bool Active { get; set; } - - /// - /// 获得/设置 是否禁用 默认 false - /// - [Parameter] - public bool IsDisabled { get; set; } - - /// - /// 获得/设置 分组名称 - /// - [Parameter] - public string? GroupName { get; set; } - - /// - /// 父组件通过级联参数获得 - /// - [CascadingParameter] - private ISelectGeneric? Container { get; set; } - - /// - /// OnInitialized 方法 - /// - protected override void OnInitialized() - { - base.OnInitialized(); - - Container?.Add(ToSelectedItem()); - } - - private SelectedItem ToSelectedItem() => new(Value, Text ?? "") - { - Active = Active, - GroupName = GroupName ?? "", - IsDisabled = IsDisabled - }; -} From dacfd665242c1ef403010408cb23963ab6b59428 Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 20:16:09 +0800 Subject: [PATCH 12/13] chore: bump version 9.1.3-beta07 --- src/BootstrapBlazor/BootstrapBlazor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index b90291ec018..08cdf115551 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@ - 9.1.3-beta06 + 9.1.3-beta07 From 8b1f955d08b0de0f61a4196cabc8f2285a89a3ba Mon Sep 17 00:00:00 2001 From: Argo-AsicoTech Date: Fri, 13 Dec 2024 20:23:58 +0800 Subject: [PATCH 13/13] =?UTF-8?q?revert:=20=E6=92=A4=E9=94=80=E6=B3=9B?= =?UTF-8?q?=E5=9E=8B=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/EnumExtensions.cs | 27 - src/BootstrapBlazor/Misc/SelectedItem.cs | 2 +- src/BootstrapBlazor/Misc/SelectedItemOfT.cs | 53 - test/UnitTest/Components/SelectGenericTest.cs | 1000 ----------------- test/UnitTest/Components/SwalTest.cs | 6 +- 5 files changed, 4 insertions(+), 1084 deletions(-) delete mode 100644 src/BootstrapBlazor/Misc/SelectedItemOfT.cs delete mode 100644 test/UnitTest/Components/SelectGenericTest.cs diff --git a/src/BootstrapBlazor/Extensions/EnumExtensions.cs b/src/BootstrapBlazor/Extensions/EnumExtensions.cs index ffa64d18efb..b3b4402d5ef 100644 --- a/src/BootstrapBlazor/Extensions/EnumExtensions.cs +++ b/src/BootstrapBlazor/Extensions/EnumExtensions.cs @@ -72,33 +72,6 @@ public static List ToSelectList(this Type type, SelectedItem? addi return ret; } - /// - /// 获取指定枚举类型的枚举值集合,默认通过 DisplayAttribute DescriptionAttribute 标签显示 DisplayName 支持资源文件 回退机制显示字段名称 - /// - /// - /// - /// - public static List> ToSelectList(this Type type, SelectedItem? additionalItem = null) - { - var ret = new List>(); - if (additionalItem != null) - { - ret.Add(additionalItem); - } - - if (type.IsEnum()) - { - var t = Nullable.GetUnderlyingType(type) ?? type; - foreach (var field in Enum.GetNames(t)) - { - var desc = Utility.GetDisplayName(t, field); - var val = (TValue)Enum.Parse(t, field); - ret.Add(new SelectedItem(val, desc)); - } - } - return ret; - } - /// /// 判断类型是否为枚举类型 /// diff --git a/src/BootstrapBlazor/Misc/SelectedItem.cs b/src/BootstrapBlazor/Misc/SelectedItem.cs index d31c19112a2..6b986d36e8f 100644 --- a/src/BootstrapBlazor/Misc/SelectedItem.cs +++ b/src/BootstrapBlazor/Misc/SelectedItem.cs @@ -20,7 +20,7 @@ public SelectedItem() { } ///
public SelectedItem(string value, string text) { - Value = value ?? ""; + Value = value; Text = text; } diff --git a/src/BootstrapBlazor/Misc/SelectedItemOfT.cs b/src/BootstrapBlazor/Misc/SelectedItemOfT.cs deleted file mode 100644 index cb7bef8405f..00000000000 --- a/src/BootstrapBlazor/Misc/SelectedItemOfT.cs +++ /dev/null @@ -1,53 +0,0 @@ -// 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; - -/// -/// 泛型实现类 -/// -public class SelectedItem -{ - /// - /// 构造函数 - /// - public SelectedItem() { } - - /// - /// 构造函数 - /// - /// - /// - public SelectedItem(T? value, string text) - { - Value = value; - Text = text; - } - - /// - /// 获得/设置 显示名称 - /// - public string Text { get; set; } = ""; - - /// - /// 获得/设置 选项值 - /// - public T? Value { get; set; } - - /// - /// 获得/设置 是否选中 - /// - public bool Active { get; set; } - - /// - /// 获得/设置 是否禁用 - /// - public bool IsDisabled { get; set; } - - /// - /// 获得/设置 分组名称 - /// - public string GroupName { get; set; } = ""; -} diff --git a/test/UnitTest/Components/SelectGenericTest.cs b/test/UnitTest/Components/SelectGenericTest.cs deleted file mode 100644 index 4977887105e..00000000000 --- a/test/UnitTest/Components/SelectGenericTest.cs +++ /dev/null @@ -1,1000 +0,0 @@ -// 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 AngleSharp.Dom; -using AngleSharp.Html.Dom; -using Microsoft.AspNetCore.Components.Web.Virtualization; -using System.ComponentModel.DataAnnotations; -using System.Reflection; - -namespace UnitTest.Components; - -public class SelectGenericTest : BootstrapBlazorTestBase -{ - [Fact] - public void SeletectedItem_Ok() - { - var item = new SelectedItem(null!, "Text"); - Assert.Equal(item.Value, string.Empty); - } - - [Fact] - public async Task OnSearchTextChanged_Null() - { - var cut = Context.RenderComponent(pb => - { - pb.AddChildContent>(pb => - { - pb.Add(a => a.ShowSearch, true); - pb.Add(a => a.Items, new List>() - { - new("1", "Test1"), - new("2", "Test2") { IsDisabled = true } - }); - }); - }); - - var ctx = cut.FindComponent>(); - await ctx.InvokeAsync(async () => - { - await ctx.Instance.ConfirmSelectedItem(0); - - // 搜索 T - ctx.Find(".search-text").Input("T"); - await ctx.Instance.ConfirmSelectedItem(0); - }); - - ctx.SetParametersAndRender(pb => - { - pb.Add(a => a.OnBeforeSelectedItemChange, item => Task.FromResult(false)); - pb.Add(a => a.OnSelectedItemChanged, item => Task.CompletedTask); - }); - await ctx.InvokeAsync(() => ctx.Instance.ConfirmSelectedItem(0)); - - ctx.Instance.ClearSearchText(); - - ctx.SetParametersAndRender(pb => - { - pb.Add(a => a.OnBeforeSelectedItemChange, null); - pb.Add(a => a.OnSelectedItemChanged, null); - pb.Add(a => a.OnSearchTextChanged, text => - { - return new List>() - { - new("1", "Test1") - }; - }); - }); - - await ctx.InvokeAsync(() => - { - ctx.Find(".search-text").Input("T"); - }); - cut.DoesNotContain("Test2"); - } - - [Fact] - public void Options_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Options, builder => - { - builder.OpenComponent>(0); - builder.AddAttribute(1, "Text", "Test-Select"); - builder.CloseComponent(); - - builder.OpenComponent(2); - builder.CloseComponent(); - }); - }); - Assert.Contains("Test-Select", cut.Markup); - } - - [Fact] - public void Disabled_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.IsDisabled, true); - pb.Add(a => a.Options, builder => - { - builder.OpenComponent>(0); - builder.AddAttribute(1, nameof(SelectOptionGeneric.IsDisabled), true); - builder.CloseComponent(); - - builder.OpenComponent(2); - builder.CloseComponent(); - }); - }); - Assert.Contains("_input\" disabled=\"disabled\"", cut.Markup); - Assert.Contains("dropdown-item active disabled", cut.Markup); - } - - [Fact] - public void IsClearable_Ok() - { - var val = "Test2"; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.IsClearable, true); - pb.Add(a => a.Items, new List>() - { - new("", "请选择"), - new("2", "Test2"), - new("3", "Test3") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.OnValueChanged, v => - { - val = v; - return Task.CompletedTask; - }); - }); - var clearButton = cut.Find(".clear-icon"); - cut.InvokeAsync(() => clearButton.Click()); - Assert.Empty(val); - - // 提高代码覆盖率 - var select = cut; - select.SetParametersAndRender(pb => - { - pb.Add(a => a.Color, Color.Danger); - }); - - var validPi = typeof(SelectGeneric).GetProperty("IsValid", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; - validPi.SetValue(select.Instance, true); - - var pi = typeof(SelectGeneric).GetProperty("ClearClassString", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; - val = pi.GetValue(select.Instance, null)!.ToString(); - Assert.Contains("text-success", val); - - validPi.SetValue(select.Instance, false); - val = pi.GetValue(select.Instance, null)!.ToString(); - Assert.Contains("text-danger", val); - } - - [Fact] - public void SelectOption_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Text, "Test-SelectOption"); - pb.Add(a => a.GroupName, "Test-GroupName"); - pb.Add(a => a.IsDisabled, false); - pb.Add(a => a.Active, true); - pb.Add(a => a.Value, ""); - }); - } - - [Fact] - public void Enum_Ok() - { - var cut = Context.RenderComponent>(); - Assert.Equal(2, cut.FindAll(".dropdown-item").Count); - } - - [Fact] - public void NullableEnum_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.AdditionalAttributes, new Dictionary() - { - ["placeholder"] = "" - }); - }); - Assert.Equal(3, cut.FindAll(".dropdown-item").Count); - } - - [Fact] - public async Task OnSelectedItemChanged_OK() - { - var triggered = false; - - // 空值时,不触发 OnSelectedItemChanged 回调 - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("", "Test"), - new("1", "Test2") - }); - pb.Add(a => a.Value, ""); - pb.Add(a => a.OnSelectedItemChanged, item => - { - triggered = true; - return Task.CompletedTask; - }); - }); - Assert.True(triggered); - - // 切换候选项时触发 OnSelectedItemChanged 回调测试 - var items = cut.FindAll(".dropdown-item"); - var count = items.Count; - Assert.Equal(2, count); - - var item = items[1]; - await cut.InvokeAsync(() => { item.Click(); }); - Assert.True(triggered); - - // 切换回 空值 触发 OnSelectedItemChanged 回调测试 - triggered = false; - items = cut.FindAll(".dropdown-item"); - item = items[0]; - await cut.InvokeAsync(() => { item.Click(); }); - Assert.True(triggered); - - // 首次加载值不为空时触发 OnSelectedItemChanged 回调测试 - triggered = false; - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("", "Test"), - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - }); - Assert.True(triggered); - - // 切换回 空值 触发 OnSelectedItemChanged 回调测试 - triggered = false; - items = cut.FindAll(".dropdown-item"); - count = items.Count; - Assert.Equal(3, count); - item = items[0]; - await cut.InvokeAsync(() => { item.Click(); }); - Assert.True(triggered); - } - - [Fact] - public async Task OnSelectedItemChanged_Generic() - { - Foo? selectedValue = null; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new() { Value = new Foo() { Id = 1, Address = "Foo1" }, Text = "test1" }, - new() { Value = new Foo() { Id = 2, Address = "Foo2" }, Text = "test2" } - }); - pb.Add(a => a.Value, new Foo() { Id = 1, Address = "Foo1" }); - pb.Add(a => a.OnSelectedItemChanged, v => - { - if (v is SelectedItem d) - { - selectedValue = d.Value; - } - return Task.CompletedTask; - }); - pb.Add(a => a.CustomKeyAttribute, typeof(KeyAttribute)); - }); - - IModelEqualityComparer comparer = cut.Instance as IModelEqualityComparer; - Assert.NotNull(comparer); - comparer.ModelEqualityComparer = (x, y) => x.Id == y.Id; - - var items = cut.FindAll(".dropdown-item"); - await cut.InvokeAsync(() => items[1].Click()); - Assert.NotNull(selectedValue); - } - - [Fact] - public void DisableItemChangedWhenFirstRender_Ok() - { - var triggered = false; - - // 空值时,不触发 OnSelectedItemChanged 回调 - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test"), - new("2", "Test2") - }); - pb.Add(a => a.Value, ""); - pb.Add(a => a.OnSelectedItemChanged, item => - { - triggered = true; - return Task.CompletedTask; - }); - pb.Add(a => a.DisableItemChangedWhenFirstRender, true); - }); - Assert.False(triggered); - } - - [Fact] - public void Color_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Color, Color.Danger); - }); - Assert.Contains("border-danger", cut.Markup); - } - - [Fact] - public void Validate_Ok() - { - var valid = false; - var invalid = false; - var model = new Foo() { Name = "Test-Select1" }; - var cut = Context.RenderComponent(builder => - { - builder.Add(a => a.OnValidSubmit, context => - { - valid = true; - return Task.CompletedTask; - }); - builder.Add(a => a.OnInvalidSubmit, context => - { - invalid = true; - return Task.CompletedTask; - }); - builder.Add(a => a.Model, model); - builder.AddChildContent>(pb => - { - pb.Add(a => a.Value, model.Name); - pb.Add(a => a.OnValueChanged, v => - { - model.Name = v; - return Task.CompletedTask; - }); - pb.Add(a => a.ValueExpression, model.GenerateValueExpression()); - pb.Add(a => a.Items, new SelectedItem[] - { - new("", "Test"), - new("1", "Test1") { GroupName = "Test1" }, - new("2", "Test2") { GroupName = "Test2" } - }); - }); - }); - - cut.InvokeAsync(() => - { - var form = cut.Find("form"); - form.Submit(); - Assert.True(valid); - }); - - var ctx = cut.FindComponent>(); - ctx.InvokeAsync(async () => - { - await ctx.Instance.ConfirmSelectedItem(0); - var form = cut.Find("form"); - form.Submit(); - Assert.True(invalid); - }); - } - - [Fact] - public void ItemTemplate_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1") { GroupName = "Test1" }, - new("2", "Test2") { GroupName = "Test2" } - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.ItemTemplate, item => builder => - { - builder.OpenElement(0, "div"); - builder.AddContent(1, item.Text); - builder.CloseComponent(); - }); - }); - - cut.Find(".dropdown-item").Click(); - } - - [Fact] - public void GroupItemTemplate_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1") { GroupName = "Test1" }, - new("2", "Test2") { GroupName = "Test2" } - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.GroupItemTemplate, title => builder => - { - builder.OpenElement(0, "div"); - builder.AddAttribute(1, "class", "group-key"); - builder.AddContent(2, title); - builder.CloseComponent(); - }); - }); - cut.Contains("
Test1
"); - cut.Contains("
Test2
"); - } - - [Fact] - public void NullItems_Ok() - { - var cut = Context.RenderComponent>(); - Assert.Contains("select", cut.Markup); - } - - [Fact] - public void NullBool_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new List> - { - new(true, "True"), - new(false, "False"), - }); - pb.Add(a => a.Value, null); - }); - - // 值为 null - // 候选项中无,导致默认选择第一个 Value 被更改为 true - Assert.True(cut.Instance.Value); - } - - [Fact] - public void SearchIcon_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.ShowSearch, true); - pb.Add(a => a.SearchIcon, "search-icon"); - }); - Assert.Contains("search-icon", cut.Markup); - } - - [Fact] - public void IsFixedSearch_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.ShowSearch, true); - pb.Add(a => a.IsFixedSearch, true); - }); - Assert.Contains("search is-fixed", cut.Markup); - Assert.Contains("class=\"icon", cut.Markup); - } - - [Fact] - public void CustomClass_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.CustomClass, "test-custom-class"); - }); - Assert.Contains("test-custom-class", cut.Markup); - } - - [Fact] - public void ShowShadow_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - }); - Assert.Contains("shadow", cut.Markup); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.ShowShadow, false); - }); - Assert.DoesNotContain("shadow", cut.Markup); - } - - [Fact] - public void DropdownIcon_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.DropdownIcon, "search-icon"); - }); - Assert.Contains("search-icon", cut.Markup); - } - - [Fact] - public void DisplayTemplate_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.DisplayTemplate, item => builder => - { - builder.AddContent(0, $"test-display-template-{item?.Text}"); - }); - }); - Assert.Contains("test-display-template-Test2", cut.Markup); - } - - [Fact] - public void IsPopover_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.IsPopover, true); - }); - Assert.DoesNotContain("dropdown-menu-arrow", cut.Markup); - Assert.DoesNotContain("data-bs-toggle=\"dropdown\"", cut.Markup); - } - - [Fact] - public void Offset_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.IsPopover, false); - pb.Add(a => a.Offset, "[0, 11]"); - }); - Assert.Contains("data-bs-offset=\"[0, 11]\"", cut.Markup); - } - - [Fact] - public void Placement_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.Placement, Placement.Top); - }); - cut.Contains($"data-bs-placement=\"{Placement.Top.ToDescriptionString()}\""); - - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.Placement, Placement.Auto); - }); - cut.DoesNotContain("data-bs-placement"); - } - - [Fact] - public void ItemClick_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.IsPopover, true); - }); - - cut.InvokeAsync(() => - { - var item = cut.Find(".dropdown-item"); - item.Click(); - Assert.True(item.ClassList.Contains("active")); - }); - } - - [Fact] - public void IsVirtualize_Items() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.IsVirtualize, true); - pb.Add(a => a.RowHeight, 33f); - pb.Add(a => a.OverscanCount, 4); - }); - - cut.SetParametersAndRender(pb => pb.Add(a => a.ShowSearch, true)); - cut.InvokeAsync(async () => - { - // 搜索 T - cut.Find(".search-text").Input("T"); - await cut.Instance.ConfirmSelectedItem(0); - }); - } - - [Fact] - public async Task IsVirtualize_Items_Clearable_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.IsVirtualize, true); - pb.Add(a => a.RowHeight, 33f); - pb.Add(a => a.OverscanCount, 4); - pb.Add(a => a.IsClearable, true); - pb.Add(a => a.ShowSearch, true); - }); - - // 覆盖有搜索条件时,点击清空按钮 - // 期望 UI 显示值为默认值 - // 期望 下拉框为全数据 - var input = cut.Find(".search-text"); - await cut.InvokeAsync(() => input.Input("2")); - - // 下拉框仅显示一个选项 Test2 - var items = cut.FindAll(".dropdown-item"); - Assert.Single(items); - - // UI 值为 Test2 - await cut.InvokeAsync(() => items[0].Click()); - var el = cut.Find(".form-select") as IHtmlInputElement; - Assert.NotNull(el); - Assert.Equal("Test2", el.Value); - Assert.Equal("2", cut.Instance.Value); - - // 点击 Clear 按钮 - var button = cut.Find(".clear-icon"); - await cut.InvokeAsync(() => button.Click()); - - // UI 恢复 Test1 - Assert.Equal("Test1", el.Value); - - // 下拉框显示所有选项 - items = cut.FindAll(".dropdown-item"); - Assert.Equal(2, items.Count); - } - - [Fact] - public async Task IsVirtualize_OnQueryAsync_Clearable_Ok() - { - var query = false; - var startIndex = 0; - var requestCount = 0; - var searchText = string.Empty; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.OnQueryAsync, option => - { - query = true; - startIndex = option.StartIndex; - requestCount = option.Count; - searchText = option.SearchText; - return Task.FromResult(new QueryData>() - { - Items = string.IsNullOrEmpty(searchText) - ? [new("", "All"), new("1", "Test1"), new("2", "Test2")] - : [new("2", "Test2")], - TotalCount = string.IsNullOrEmpty(searchText) ? 2 : 1 - }); - }); - pb.Add(a => a.Value, ""); - pb.Add(a => a.IsVirtualize, true); - pb.Add(a => a.IsClearable, true); - pb.Add(a => a.ShowSearch, true); - }); - - // 覆盖有搜索条件时,点击清空按钮 - // 期望 UI 显示值为默认值 - // 期望 下拉框为全数据 - var input = cut.Find(".search-text"); - await cut.InvokeAsync(() => input.Input("2")); - - // 下拉框仅显示一个选项 Test2 - var items = cut.FindAll(".dropdown-item"); - Assert.Single(items); - - // UI 值为 Test2 - await cut.InvokeAsync(() => items[0].Click()); - var el = cut.Find(".form-select") as IHtmlInputElement; - Assert.NotNull(el); - Assert.Equal("Test2", el.Value); - Assert.Equal("2", cut.Instance.Value); - - query = false; - // 点击 Clear 按钮 - var button = cut.Find(".clear-icon"); - await cut.InvokeAsync(() => button.Click()); - - // UI 恢复 Test1 - Assert.Equal("All", el.Value); - - // 下拉框显示所有选项 - Assert.True(query); - } - - [Fact] - public async Task IsVirtualize_BindValue() - { - var value = "3"; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Value, value); - pb.Add(a => a.IsVirtualize, true); - pb.Add(a => a.ValueChanged, EventCallback.Factory.Create(this, new Action(item => - { - value = item; - }))); - pb.Add(a => a.OnQueryAsync, option => - { - return Task.FromResult(new QueryData>() - { - Items = new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }, - TotalCount = 2 - }); - }); - }); - - var input = cut.Find(".form-select"); - Assert.Null(input.GetAttribute("value")); - - var select = cut.Instance; - Assert.Equal("3", select.Value); - - var item = cut.Find(".dropdown-item"); - await cut.InvokeAsync(() => { item.Click(); }); - Assert.Equal("1", value); - - input = cut.Find(".form-select"); - Assert.Equal("Test1", input.GetAttribute("value")); - } - - [Fact] - public void IsVirtualize_DefaultVirtualizeItemText() - { - string? value = "3"; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.IsVirtualize, true); - pb.Add(a => a.DefaultVirtualizeItemText, "Test 3"); - pb.Add(a => a.Value, value); - pb.Add(a => a.ValueChanged, EventCallback.Factory.Create(this, new Action(item => - { - value = item; - }))); - pb.Add(a => a.OnQueryAsync, option => - { - return Task.FromResult(new QueryData>() - { - Items = new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }, - TotalCount = 2 - }); - }); - }); - - cut.InvokeAsync(() => - { - var input = cut.Find(".form-select"); - Assert.Equal("Test 3", input.GetAttribute("value")); - }); - } - - [Fact] - public void LoadItems_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.OnQueryAsync, option => - { - return Task.FromResult(new QueryData>()); - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.IsVirtualize, true); - }); - var select = cut.Instance; - var mi = select.GetType().GetMethod("LoadItems", BindingFlags.NonPublic | BindingFlags.Instance); - mi?.Invoke(select, [new ItemsProviderRequest(0, 1, CancellationToken.None)]); - - var totalCountProperty = select.GetType().GetProperty("TotalCount", BindingFlags.NonPublic | BindingFlags.Instance); - totalCountProperty?.SetValue(select, 2); - mi?.Invoke(select, [new ItemsProviderRequest(0, 1, CancellationToken.None)]); - } - - [Fact] - public void IsMarkupString_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "
Test1
"), - new("2", "
Test2
") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.IsMarkupString, true); - }); - Assert.Contains("
Test1
", cut.Markup); - } - - [Fact] - public async Task IsEditable_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "
Test1
"), - new("2", "
Test2
") - }); - pb.Add(a => a.Value, "2"); - }); - var input = cut.Find(".form-select"); - Assert.True(input.IsReadOnly()); - - var updated = false; - cut.SetParametersAndRender(pb => - { - pb.Add(a => a.IsEditable, true); - pb.Add(a => a.OnInputChangedCallback, v => - { - updated = true; - return Task.CompletedTask; - }); - pb.Add(a => a.TextConvertToValueCallback, v => - { - return Task.FromResult(v); - }); - }); - Assert.False(input.IsReadOnly()); - - await cut.InvokeAsync(() => { input.Change("Test3"); }); - Assert.Equal("Test3", cut.Instance.Value); - Assert.True(updated); - } - - [Fact] - public async Task IsEditable_Generic() - { - var items = new List>() - { - new() { Value = new Foo() { Id = 1, Address = "Foo1" }, Text = "test1" }, - new() { Value = new Foo() { Id = 2, Address = "Foo2" }, Text = "test2" } - }; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, items); - pb.Add(a => a.Value, new Foo() { Id = 1, Address = "Foo1" }); - pb.Add(a => a.IsEditable, true); - pb.Add(a => a.TextConvertToValueCallback, v => - { - return Task.FromResult(new Foo() { Id = 3, Address = "Foo3" }); - }); - }); - - var input = cut.Find(".form-select"); - await cut.InvokeAsync(() => { input.Change("test2"); }); - Assert.Equal("Foo2", cut.Instance.Value.Address); - - await cut.InvokeAsync(() => { input.Change("test3"); }); - Assert.Equal("Foo3", cut.Instance.Value.Address); - } - - [Fact] - public async Task OnClearAsync_Ok() - { - var clear = false; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "
Test1
"), - new("2", "
Test2
") - }); - pb.Add(a => a.Value, "2"); - pb.Add(a => a.ShowSearch, true); - pb.Add(a => a.IsClearable, true); - pb.Add(a => a.OnClearAsync, () => - { - clear = true; - return Task.CompletedTask; - }); - }); - - var span = cut.Find(".clear-icon"); - Assert.NotNull(span); - - await cut.InvokeAsync(() => - { - span.Click(); - }); - Assert.True(clear); - } - - [Fact] - public async Task Toggle_Ok() - { - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }); - }); - await cut.Instance.Show(); - await cut.Instance.Hide(); - } - - [Fact] - public void GenericValue_Ok() - { - var items = new List>() - { - new() - { - Value = new Foo() { Id = 1, Name = "Foo1" }, - Text = "Foo1" - }, - new() - { - Value = new Foo() { Id = 2, Name = "Foo2" }, - Text = "Foo2" - } - }; - var cut = Context.RenderComponent>(pb => - { - pb.Add(a => a.Items, items); - }); - } -} diff --git a/test/UnitTest/Components/SwalTest.cs b/test/UnitTest/Components/SwalTest.cs index b5313d893e8..1f742046be0 100644 --- a/test/UnitTest/Components/SwalTest.cs +++ b/test/UnitTest/Components/SwalTest.cs @@ -212,9 +212,9 @@ public void Show_Ok() // 带确认框的 Select cut.SetParametersAndRender(pb => { - pb.AddChildContent>(pb => + pb.AddChildContent>(pb => { - pb.Add(a => a.Items, new List>() + pb.Add(a => a.Items, new List() { new("1", "Test1"), new("2", "Test2") { IsDisabled = true } @@ -228,7 +228,7 @@ public void Show_Ok() }); }); - Task.Run(() => cut.InvokeAsync(() => cut.FindComponent>().Instance.ConfirmSelectedItem(0))); + Task.Run(() => cut.InvokeAsync(() => cut.FindComponent>().Instance.ConfirmSelectedItem(0))); tick = DateTime.Now; while (!cut.Markup.Contains("test-swal-footer")) {