From 41dbb195101584d4dbeaf1f99c518b35fe0d6757 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 13 Dec 2024 20:26:26 +0800 Subject: [PATCH] chore(Select): revert Select component update (#4844) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 增加级联参数 * doc: 更新示例文档 * doc: 更改示例文件名 * doc: 增加原 Select 组件文档 * test: 增加 SelectGeneric 组件单元测试 * revert: 撤销忽略单元测试标签 * revert: 撤销单元测试更新 * revert: 恢复支持泛型逻辑 * test: 更新单元测试 * refactor: 更改可见性 * refactor: 移动组件到独立包 * chore: bump version 9.1.3-beta07 * revert: 撤销泛型更改 --- .../Components/Samples/SelectGenerics.razor | 441 ++++++++++++++ .../Samples/SelectGenerics.razor.cs | 405 +++++++++++++ .../Samples/SelectGenerics.razor.css | 62 ++ .../Components/Samples/Selects.razor | 14 - .../Components/Samples/Selects.razor.cs | 9 - src/BootstrapBlazor/BootstrapBlazor.csproj | 2 +- .../Components/Select/Select.razor.cs | 61 +- .../Components/Select/SelectOption.cs | 1 - .../Select/VirtualizeQueryOption.cs | 6 +- .../SelectGeneric/ISelectGeneric.cs | 18 - .../SelectGeneric/SelectGeneric.razor | 115 ---- .../SelectGeneric/SelectGeneric.razor.cs | 566 ------------------ .../SelectGeneric/SelectGeneric.razor.js | 139 ----- .../SelectGeneric/SelectGeneric.razor.scss | 241 -------- .../SelectGeneric/SelectOptionGeneric.cs | 65 -- .../Extensions/EnumExtensions.cs | 27 - src/BootstrapBlazor/Misc/SelectedItem.cs | 2 +- src/BootstrapBlazor/Misc/SelectedItemOfT.cs | 53 -- test/UnitTest/Components/SelectTest.cs | 382 ++++++------ test/UnitTest/Components/SwalTest.cs | 6 +- 20 files changed, 1116 insertions(+), 1499 deletions(-) create mode 100644 src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor create mode 100644 src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor.cs create mode 100644 src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor.css 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 delete mode 100644 src/BootstrapBlazor/Misc/SelectedItemOfT.cs diff --git a/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor new file mode 100644 index 00000000000..585d8f970d6 --- /dev/null +++ b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor @@ -0,0 +1,441 @@ +@page "/select-generic" +@inject DialogService Dialog +@inject IStringLocalizer Localizer +@inject IOptionsMonitor WebsiteOption + +

@Localizer["SelectsTitle"]

+ +

@Localizer["SelectsDescription"]

+ + +

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

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

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

+
+
+ +
+
+
+ + +
+
+ +
+
+ +
+
+
+ + +
+
+ +
+
+ +
+
+
+ + +
+
+ +
+
+ +
+
+
+
+
+ + + +
+
+ + + + + + + + +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+ + +
+
+ +
+
+
@CurrentGuid
+
+
+
+ + +

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

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

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

+

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

+
+
+ + +
+
+ + +
+
+ + +
+
+
@SelectedEnumItem
+
+
+
+ + +
@((MarkupString)Localizer["SelectsNullableDescription"].Value)
+
+
+ +
+
+
@GetSelectedIntItemString()
+
+
+
+ + +
+

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

+
@((MarkupString)Localizer["SelectsNullableBooleanDescription2"].Value)
+
+
+
+ +
+
+
@GetSelectedBoolItemString()
+
+
+
+ + +
+
+ + + @{ + var foo = context.Value!; + } + + + +
+
+ +
+
+
+ + +
+
+
+ + + + +
+
+
+
+
+ +
+
+ +
+
+
+ + +
+
+ +
+
+
+ + +
+
+ + + + @context.Text + + +
+
+
+ + +
+
+ +
+
+ +
+
+
+ + +
+
+ +
+
+
+ + +
@((MarkupString)Localizer["SelectsIsEditableDesc"].Value)
+
+
+ +
+
+
+ + +
@((MarkupString)Localizer["SelectsVirtualizeDescription"].Value)
+ +
+
+ + + + +
+
+ + + + +
+
+ +

1. 使用 OnQueryAsync 作为数据源

+
+
+ +
+
+ +
+
+ +

2. 使用 Items 作为数据源

+
+
+ +
+
+ +
+
+
+ + +
@((MarkupString)Localizer["SelectsGenericDesc"].Value)
+
+
+ +
+
+ +
+
+
+ + + + diff --git a/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor.cs b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor.cs new file mode 100644 index 00000000000..9aaa30f9d36 --- /dev/null +++ b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor.cs @@ -0,0 +1,405 @@ +// 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 SelectGenerics +{ + [NotNull] + private ConsoleLogger? Logger { get; set; } + + private Foo Model { get; set; } = new Foo(); + + private IEnumerable> Items { get; set; } = new List>() + { + new("Beijing", "北京"), + new("Shanghai", "上海") { Active = true }, + }; + + private IEnumerable> ClearableItems { get; set; } = new List>() + { + new("", "未选择"), + new("Beijing", "北京"), + new("Shanghai", "上海") + }; + + private IEnumerable> VirtualItems => Foos.Select(i => new SelectedItem(i, i.Name!)); + + private Foo VirtualItem1 { get; set; } = new(); + + private Foo VirtualItem2 { get; set; } = new(); + + [NotNull] + private List? Foos { get; set; } + + [Inject] + [NotNull] + private IStringLocalizer? LocalizerFoo { get; set; } + + private bool _showSearch; + + private bool _isShowSearchClearable; + + private bool _isClearable; + + private Foo _foo = new(); + + /// + /// + /// + 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, 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(), "Guid1"), + new(Guid.NewGuid(), "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 = null }, + 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 = null }, + 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 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 = new(); + + /// + /// 获得事件方法 + /// + /// + 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/SelectGenerics.razor.css b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.razor.css new file mode 100644 index 00000000000..ac594ac0fde --- /dev/null +++ b/src/BootstrapBlazor.Shared/Components/Samples/SelectGenerics.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; + } diff --git a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor index 093dd481024..e67f5be7f8f 100644 --- a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor +++ b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor @@ -436,20 +436,6 @@ - -
@((MarkupString)Localizer["SelectsGenericDesc"].Value)
-
-
- -
-
- -
-
-
- diff --git a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs index 6bb60dc47fb..c07134ba8de 100644 --- a/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs +++ b/src/BootstrapBlazor.Shared/Components/Samples/Selects.razor.cs @@ -242,15 +242,6 @@ private Task OnTimeZoneValueChanged(string timeZoneId) return Task.CompletedTask; } - 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; - /// /// 获得事件方法 /// 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 diff --git a/src/BootstrapBlazor/Components/Select/Select.razor.cs b/src/BootstrapBlazor/Components/Select/Select.razor.cs index 8ee49a0f29e..bb8689e57df 100644 --- a/src/BootstrapBlazor/Components/Select/Select.razor.cs +++ b/src/BootstrapBlazor/Components/Select/Select.razor.cs @@ -12,8 +12,7 @@ namespace BootstrapBlazor.Components; /// Select 组件实现类 /// /// -[ExcludeFromCodeCoverage] -public partial class Select : ISelect, IModelEqualityComparer +public partial class Select : ISelect { [Inject] [NotNull] @@ -93,13 +92,6 @@ public partial class Select : ISelect, IModelEqualityComparer [Parameter] public Func? OnInputChangedCallback { get; set; } - /// - /// 获得/设置 选项输入更新后转换为 Value 回调方法 默认 null - /// - /// 设置 后生效 - [Parameter] - public Func>? TextConvertToValueCallback { get; set; } - /// /// 获得/设置 无搜索结果时显示文字 /// @@ -169,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; } @@ -482,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) { @@ -511,7 +462,7 @@ private async Task ValueTypeChanged(SelectedItem item) SelectedItem = item; // 触发 StateHasChanged - _lastSelectedValueString = item.Value ?? ""; + _lastSelectedValueString = item.Value; CurrentValueAsString = _lastSelectedValueString; // 触发 SelectedItemChanged 事件 @@ -585,12 +536,4 @@ private async Task OnChange(ChangeEventArgs args) } } } - - /// - /// - /// - /// - /// - /// - public bool Equals(TValue? x, TValue? y) => this.Equals(x, y); } 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 { /// 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; } } 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 7b544789455..00000000000 --- a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs +++ /dev/null @@ -1,566 +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 组件实现类 -/// -/// -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 - }; -} 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/SelectTest.cs b/test/UnitTest/Components/SelectTest.cs index 3d4680d1379..e8499a8be79 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") @@ -977,24 +973,42 @@ public async Task Toggle_Ok() } [Fact] - public void GenericValue_Ok() + public async Task OnBeforeSelectedItemChange_OK() { - var items = new List>() + var cut = Context.RenderComponent(pb => { - new() + pb.AddChildContent>(pb => { - Value = new Foo() { Id = 1, Name = "Foo1" }, - Text = "Foo1" - }, - new() + 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)) { - Value = new Foo() { Id = 2, Name = "Foo2" }, - Text = "Foo2" + break; } - }; - var cut = Context.RenderComponent>(pb => + } + var button = cut.Find(".btn-danger"); + await cut.InvokeAsync(() => { - pb.Add(a => a.Items, items); + button.Click(); }); + await cut.InvokeAsync(() => modal.Instance.CloseCallback()); } } 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")) {