diff --git a/src/BootstrapBlazor.Server/Components/Samples/Selects.razor b/src/BootstrapBlazor.Server/Components/Samples/Selects.razor index 888538646e7..900889ade34 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Selects.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/Selects.razor @@ -6,41 +6,6 @@

@Localizer["SelectsDescription"]

- -

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

- -
-
- - - - -
-
- -

1. 使用 OnQueryAsync 作为数据源

-
-
- -
-
- -
-
- -

2. 使用 Items 作为数据源

-
-
- -
-
- -
-
-
- @@ -351,12 +316,22 @@ -
+
+
+
+ + + + +
+
+
+
-
-
@@ -418,6 +393,47 @@
+ +

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

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

1. 使用 OnQueryAsync 作为数据源

+
+
+ +
+
+ +
+
+ +

2. 使用 Items 作为数据源

+
+
+ +
+
+ +
+
+
+ diff --git a/src/BootstrapBlazor.Server/Components/Samples/Selects.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Selects.razor.cs index 8963f44854b..a35a7caf517 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Selects.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Selects.razor.cs @@ -40,7 +40,11 @@ public sealed partial class Selects [NotNull] private IStringLocalizer? LocalizerFoo { get; set; } - private bool ShowSearch { get; set; } + private bool _showSearch; + + private bool _isShowSearchClearable; + + private bool _isClearable; private string? _fooName; diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index f4f92dc8d3c..ca91c28b7d1 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@ - 8.9.2-beta09 + 8.9.2-beta10 diff --git a/src/BootstrapBlazor/Components/Select/Select.razor b/src/BootstrapBlazor/Components/Select/Select.razor index 6ae7df52cd7..7161f79ac6b 100644 --- a/src/BootstrapBlazor/Components/Select/Select.razor +++ b/src/BootstrapBlazor/Components/Select/Select.razor @@ -63,7 +63,7 @@ } - @foreach (var itemGroup in DataSource.GroupBy(i => i.GroupName)) + @foreach (var itemGroup in _dataSource.GroupBy(i => i.GroupName)) { if (!string.IsNullOrEmpty(itemGroup.Key)) { @@ -81,7 +81,7 @@ @RenderRow(item) } } - @if (!DataSource.Any()) + @if (!_dataSource.Any()) { } diff --git a/src/BootstrapBlazor/Components/Select/Select.razor.cs b/src/BootstrapBlazor/Components/Select/Select.razor.cs index 9e421165b97..339cffc3e38 100644 --- a/src/BootstrapBlazor/Components/Select/Select.razor.cs +++ b/src/BootstrapBlazor/Components/Select/Select.razor.cs @@ -57,13 +57,9 @@ public partial class Select : ISelect .AddClass("is-fixed", IsFixedSearch) .Build(); - /// - /// Razor 文件中 Options 模板子项 - /// - private List Children { get; } = []; + private readonly List _children = []; - [NotNull] - private List DataSource { get; } = []; + private readonly List _dataSource = []; /// /// 获得/设置 右侧清除图标 默认 fa-solid fa-angle-up @@ -270,7 +266,9 @@ protected override bool TryParseValueFromString(string value, [MaybeNullWhen(fal private bool TryParseSelectItem(string value, [MaybeNullWhen(false)] out TValue result, out string? validationErrorMessage) { - SelectedItem = (VirtualItems ?? DataSource).FirstOrDefault(i => i.Value == value) ?? GetVirtualizeItem(); + SelectedItem = Items.FirstOrDefault(i => i.Value == value) + ?? VirtualItems?.FirstOrDefault(i => i.Value == value) + ?? GetVirtualizeItem(); // support SelectedItem? type result = SelectedItem != null ? (TValue)(object)SelectedItem : default; @@ -289,21 +287,21 @@ private bool TryParseSelectItem(string value, [MaybeNullWhen(false)] out TValue private void ResetSelectedItem() { - DataSource.Clear(); + _dataSource.Clear(); if (string.IsNullOrEmpty(SearchText)) { - DataSource.AddRange(Items); - DataSource.AddRange(Children); + _dataSource.AddRange(Items); + _dataSource.AddRange(_children); if (VirtualItems != null) { - DataSource.AddRange(VirtualItems); + _dataSource.AddRange(VirtualItems); } - SelectedItem = DataSource.Find(i => i.Value.Equals(CurrentValueAsString, StringComparison)) - ?? DataSource.Find(i => i.Active) - ?? DataSource.Where(i => !i.IsDisabled).FirstOrDefault() + SelectedItem = _dataSource.Find(i => i.Value.Equals(CurrentValueAsString, StringComparison)) + ?? _dataSource.Find(i => i.Active) + ?? _dataSource.Where(i => !i.IsDisabled).FirstOrDefault() ?? GetVirtualizeItem(); if (SelectedItem != null) @@ -328,7 +326,7 @@ private void ResetSelectedItem() } else { - DataSource.AddRange(OnSearchTextChanged(SearchText)); + _dataSource.AddRange(OnSearchTextChanged(SearchText)); } } @@ -347,7 +345,7 @@ private void ResetSelectedItem() public async Task ConfirmSelectedItem(int index) { var ds = string.IsNullOrEmpty(SearchText) - ? DataSource + ? _dataSource : OnSearchTextChanged(SearchText); var item = ds.ElementAt(index); await OnClickItem(item); @@ -415,7 +413,7 @@ private async Task SelectedItemChanged(SelectedItem item) /// 添加静态下拉项方法 /// /// - public void Add(SelectedItem item) => Children.Add(item); + public void Add(SelectedItem item) => _children.Add(item); /// /// 清空搜索栏文本内容 @@ -433,7 +431,25 @@ private async Task OnClearValue() await OnClearAsync(); } - var item = DataSource.FirstOrDefault(); + SelectedItem? item; + if (IsVirtualize) + { + if (VirtualizeElement != null) + { + await VirtualizeElement.RefreshDataAsync(); + item = VirtualItems!.FirstOrDefault(); + } + else + { + VirtualItems = Items; + item = Items.FirstOrDefault(); + } + } + else + { + item = Items.FirstOrDefault(); + } + if (item != null) { await SelectedItemChanged(item); diff --git a/test/UnitTest/Components/SelectTest.cs b/test/UnitTest/Components/SelectTest.cs index 3feae3fec5b..e6d3fcb77d4 100644 --- a/test/UnitTest/Components/SelectTest.cs +++ b/test/UnitTest/Components/SelectTest.cs @@ -3,6 +3,7 @@ // Website: https://www.blazor.zone or https://argozhang.github.io/ using AngleSharp.Dom; +using AngleSharp.Html.Dom; using Microsoft.AspNetCore.Components.Web.Virtualization; using System.Reflection; @@ -620,8 +621,56 @@ public void IsVirtualize_Items() } [Fact] - public void IsVirtualize_OnQueryAsync() + 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; @@ -629,32 +678,55 @@ public void IsVirtualize_OnQueryAsync() { pb.Add(a => a.OnQueryAsync, option => { + query = true; startIndex = option.StartIndex; requestCount = option.Count; searchText = option.SearchText; return Task.FromResult(new QueryData() { - Items = new SelectedItem[] - { - new("1", "Test1"), - new("2", "Test2") - }, - TotalCount = 2 + Items = string.IsNullOrEmpty(searchText) + ? new SelectedItem[] + { + new("", "All"), + new("1", "Test1"), + new("2", "Test2") + } : [new("2", "Test2")], + TotalCount = string.IsNullOrEmpty(searchText) ? 2 : 1 }); }); - pb.Add(a => a.Value, "2"); + pb.Add(a => a.Value, ""); pb.Add(a => a.IsVirtualize, true); + pb.Add(a => a.IsClearable, true); + pb.Add(a => a.ShowSearch, true); }); - cut.SetParametersAndRender(pb => pb.Add(a => a.ShowSearch, true)); - cut.InvokeAsync(async () => - { - // 搜索 T - cut.Find(".search-text").Input("T"); - await cut.Instance.ConfirmSelectedItem(0); + // 覆盖有搜索条件时,点击清空按钮 + // 期望 UI 显示值为默认值 + // 期望 下拉框为全数据 + var input = cut.Find(".search-text"); + await cut.InvokeAsync(() => input.Input("2")); - Assert.Equal(string.Empty, searchText); - }); + // 下拉框仅显示一个选项 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]