diff --git a/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor b/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor index ef84b220261..10a95513b25 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor @@ -123,18 +123,24 @@ private enum MultiSelectEnumFoo +
+ + + + +
+ ShowSearch="true" IsFixedSearch="_isFixedSearch" IsClearable="_isClearable" OnSearchTextChanged="@OnSearch">
+ ShowSearch="true" IsFixedSearch="_isFixedSearch" IsClearable="_isClearable">
@@ -295,6 +301,58 @@ private enum MultiSelectEnumFoo + +
+

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

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

1. 使用 OnQueryAsync 作为数据源

+
+
+ + +
+
+ +
+
+ +

2. 使用 Items 作为数据源

+
+
+ + +
+
+ +
+
+
+ diff --git a/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor.cs index 15ba768f095..6693409a337 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor.cs @@ -10,6 +10,10 @@ namespace BootstrapBlazor.Server.Components.Samples; /// public partial class MultiSelects { + [Inject] + [NotNull] + private IStringLocalizer? LocalizerFoo { get; set; } + /// /// Foo 类为Demo测试用,如有需要请自行下载源码查阅 /// Foo class is used for Demo tet, please download the source code if necessary @@ -106,9 +110,20 @@ private enum MultiSelectEnumFoo private List CascadingItems1 { get; set; } = []; + [NotNull] + private List? Foos { get; set; } + + private string? _virtualItemString1; + + private string? _virtualItemString2; + + private IEnumerable VirtualItems => Foos.Select(i => new SelectedItem(i.Name!, i.Name!)).ToList(); + private string? _editString; private bool _isFixedSearch; + private bool _isClearable; + private bool _showSearch; private async Task OnEditCallback(string value) { @@ -123,6 +138,21 @@ private async Task OnEditCallback(string value) return item; } + 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))]; + } + return new QueryData + { + Items = items.Skip(option.StartIndex).Take(option.Count).Select(i => new SelectedItem(i.Name!, i.Name!)), + TotalCount = items.Count + }; + } + private SelectedItem[] GroupItems { get; } = [ new("Jilin", "吉林") { GroupName = "东北"}, @@ -142,7 +172,7 @@ private async Task OnEditCallback(string value) ]; /// - /// OnInitialized + /// /// protected override void OnInitialized() { @@ -183,6 +213,9 @@ protected override void OnInitialized() LongItems = GenerateDataSource(LongDataSource); Items = GenerateDataSource(DataSource); + Foos = Foo.GenerateFoo(LocalizerFoo); + _virtualItemString1 = Foos[79].Name; + _virtualItemString2 = Foos[45].Name; } private static List GenerateItems() => @@ -198,7 +231,7 @@ private static List GenerateItems() => new ("Lianyungang", "连云港") ]; - private static List GenerateDataSource(List source) => source.Select(i => new SelectedItem(i.Value, i.Text)).ToList(); + private static List GenerateDataSource(List source) => [.. source.Select(i => new SelectedItem(i.Value, i.Text))]; private void AddItems() { diff --git a/src/BootstrapBlazor.Server/Components/Samples/Selects.razor b/src/BootstrapBlazor.Server/Components/Samples/Selects.razor index 998e73e44e8..8bc1b2cfefc 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Selects.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/Selects.razor @@ -433,13 +433,19 @@ Name="IsVirtualize">

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

-
+
+
+ + + + +
@@ -452,7 +458,8 @@

1. 使用 OnQueryAsync 作为数据源

- +
@@ -462,7 +469,8 @@

2. 使用 Items 作为数据源

- +
diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index 96f50b58957..259def325b9 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@  - 9.5.0-beta01 + 9.5.0-beta02 diff --git a/src/BootstrapBlazor/Components/Select/MultiSelect.razor b/src/BootstrapBlazor/Components/Select/MultiSelect.razor index 7437536e7d2..544c6b2e08e 100644 --- a/src/BootstrapBlazor/Components/Select/MultiSelect.razor +++ b/src/BootstrapBlazor/Components/Select/MultiSelect.razor @@ -1,6 +1,7 @@ @namespace BootstrapBlazor.Components +@using Microsoft.AspNetCore.Components.Web.Virtualization @typeparam TValue -@inherits SelectBase +@inherits SimpleSelectBase @attribute [BootstrapModuleAutoLoader("Select/MultiSelect.razor.js", JSObjectReference = true)] @if (IsShowLabel) @@ -48,6 +49,10 @@ }
+ @if (GetClearable()) + { + + }
@if (ShowSearch) { @@ -57,7 +62,23 @@
} - @if (Rows.Count == 0) + @if (IsVirtualize) + { + + } + else if (Rows.Count == 0) { } @@ -91,28 +112,50 @@ } @foreach (var item in itemGroup) { - -
-
- -
- @if (ItemTemplate != null) - { - @ItemTemplate(item) - } - else if (IsMarkupString) - { - @((MarkupString)item.Text) - } - else - { - @item.Text - } -
-
+ @RenderRow(item) + } + + if (!string.IsNullOrEmpty(itemGroup.Key)) + { + if (GroupItemTemplate != null) + { + @GroupItemTemplate(itemGroup.Key) + } + else + { + + } } }
}
+ +@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/Select/MultiSelect.razor.cs b/src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs index abe1feb0574..ff0de238e79 100644 --- a/src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs +++ b/src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs @@ -3,21 +3,21 @@ // 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; using System.Collections; -using System.Collections.Specialized; -using System.Reflection; namespace BootstrapBlazor.Components; /// -/// MultiSelect 组件 +/// MultiSelect component /// public partial class MultiSelect { private List SelectedItems { get; } = []; - private static string? ClassString => CssBuilder.Default("select dropdown multi-select") + private string? ClassString => CssBuilder.Default("select dropdown multi-select") + .AddClass("is-clearable", IsClearable) .Build(); private string? EditSubmitKeyString => EditSubmitKey == EditSubmitKey.Space ? EditSubmitKey.ToDescriptionString() : null; @@ -40,22 +40,11 @@ public partial class MultiSelect .AddClass("d-none", SelectedItems.Count != 0) .Build(); - private string? SearchLoadingIconString => CssBuilder.Default("icon searching-icon") - .AddClass(SearchLoadingIcon) - .Build(); - - /// - /// 获得/设置 绑定数据集 - /// - [Parameter] - [NotNull] - public IEnumerable? Items { get; set; } - /// - /// 获得/设置 选项模板 + /// 获得/设置 显示部分模板 默认 null /// [Parameter] - public RenderFragment? ItemTemplate { get; set; } + public RenderFragment>? DisplayTemplate { get; set; } /// /// 获得/设置 是否显示关闭按钮 默认为 true 显示 @@ -87,17 +76,11 @@ public partial class MultiSelect [Parameter] public bool IsSingleLine { get; set; } - /// - /// 获得/设置 是否可编辑 默认 false - /// - [Parameter] - public bool IsEditable { get; set; } - /// /// 获得/设置 编辑模式下输入选项更新后回调方法 默认 null /// 返回 实例时输入选项生效,返回 null 时选项不生效进行舍弃操作,建议在回调方法中自行提示 /// - /// 设置 后生效 + /// Effective when is set. [Parameter] public Func>? OnEditCallback { get; set; } @@ -113,19 +96,6 @@ public partial class MultiSelect [Parameter] public RenderFragment? ButtonTemplate { get; set; } - /// - /// 获得/设置 显示部分模板 默认 null - /// - [Parameter] - public RenderFragment>? DisplayTemplate { get; set; } - - /// - /// 获得/设置 搜索文本发生变化时回调此方法 - /// - [Parameter] - [NotNull] - public Func>? OnSearchTextChanged { get; set; } - /// /// 获得/设置 选中项集合发生改变时回调委托方法 /// @@ -180,29 +150,16 @@ public partial class MultiSelect public string? MinErrorMessage { get; set; } /// - /// 获得/设置 设置清除图标 默认 fa-solid fa-xmark + /// Gets or sets the right-side clear icon. Default is null. /// [Parameter] [NotNull] - public string? ClearIcon { get; set; } + public string? ClearableIcon { get; set; } [Inject] [NotNull] private IStringLocalizer>? Localizer { get; set; } - private List? _itemsCache; - - private List Rows - { - get - { - _itemsCache ??= string.IsNullOrEmpty(SearchText) ? GetRowsByItems() : GetRowsBySearch(); - return _itemsCache; - } - } - - private string? PreviousValue { get; set; } - private string? PlaceholderString => SelectedItems.Count == 0 ? PlaceHolder : null; private string? ScrollIntoViewBehaviorString => ScrollIntoViewBehavior == ScrollIntoViewBehavior.Smooth ? null : ScrollIntoViewBehavior.ToDescriptionString(); @@ -224,6 +181,7 @@ protected override void OnParametersSet() DropdownIcon ??= IconTheme.GetIconByKey(ComponentIcons.MultiSelectDropdownIcon); ClearIcon ??= IconTheme.GetIconByKey(ComponentIcons.MultiSelectClearIcon); + ClearableIcon ??= IconTheme.GetIconByKey(ComponentIcons.MultiSelectClearableIcon); ResetItems(); ResetRules(); @@ -232,12 +190,20 @@ protected override void OnParametersSet() // 通过 Value 对集合进行赋值 var _currentValue = CurrentValueAsString; - if (PreviousValue != _currentValue) + if (_lastSelectedValueString != _currentValue) { - PreviousValue = _currentValue; + _lastSelectedValueString = _currentValue; var list = _currentValue.Split(',', StringSplitOptions.RemoveEmptyEntries); + SelectedItems.Clear(); - SelectedItems.AddRange(Rows.Where(item => list.Any(i => i.Trim() == item.Value))); + if (IsVirtualize) + { + SelectedItems.AddRange(list.Select(i => new SelectedItem(i, i))); + } + else + { + SelectedItems.AddRange(Rows.Where(item => list.Any(i => i.Trim() == item.Value))); + } } } @@ -256,25 +222,45 @@ protected override void OnAfterRender(bool firstRender) /// /// /// - protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new { ConfirmMethodCallback = nameof(ConfirmSelectedItem), SearchMethodCallback = nameof(TriggerOnSearch), TriggerEditTag = nameof(TriggerEditTag), ToggleRow = nameof(ToggleRow) }); + protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new + { + ConfirmMethodCallback = nameof(ConfirmSelectedItem), + SearchMethodCallback = nameof(TriggerOnSearch), + TriggerEditTag = nameof(TriggerEditTag), + ToggleRow = nameof(ToggleRow) + }); + + private int _totalCount; + private ItemsProviderResult _result; - private List GetRowsByItems() + private List GetVirtualItems() => [.. FilterBySearchText(GetRowsByItems())]; + + private async ValueTask> LoadItems(ItemsProviderRequest request) { - var items = new List(); - if (Items != null) - { - items.AddRange(Items); - } - return items; + // 有搜索条件时使用原生请求数量 + // 有总数时请求剩余数量 + var count = !string.IsNullOrEmpty(SearchText) ? request.Count : GetCountByTotal(); + var data = await OnQueryAsync(new() { StartIndex = request.StartIndex, Count = count, SearchText = SearchText }); + + _itemsCache = null; + _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 List GetRowsBySearch() + /// + /// + /// + /// + protected override async Task OnClearValue() { - var items = OnSearchTextChanged?.Invoke(SearchText) ?? FilterBySearchText(GetRowsByItems()); - return items.ToList(); - } + await base.OnClearValue(); - private IEnumerable FilterBySearchText(IEnumerable source) => source.Where(i => i.Text.Contains(SearchText, StringComparison)); + SelectedItems.Clear(); + } /// /// FormatValueAsString 方法 @@ -287,6 +273,24 @@ private List GetRowsBySearch() private bool _isToggle; + /// + /// + /// + /// + protected override List GetRowsByItems() + { + var items = new List(); + if (_result.Items != null) + { + items.AddRange(_result.Items); + } + else if (Items != null) + { + items.AddRange(Items); + } + return items; + } + /// /// 客户端回车回调方法 /// @@ -432,8 +436,7 @@ private async Task SetValue() await OnSelectedItemsChanged.Invoke(SelectedItems); } - PreviousValue = CurrentValueAsString; - + _lastSelectedValueString = CurrentValueAsString; if (!ValueChanged.HasDelegate) { StateHasChanged(); @@ -550,18 +553,4 @@ private void ResetItems() } } } - - /// - /// 客户端搜索栏回调方法 - /// - /// - /// - [JSInvokable] - public Task TriggerOnSearch(string searchText) - { - _itemsCache = null; - SearchText = searchText; - StateHasChanged(); - return Task.CompletedTask; - } } diff --git a/src/BootstrapBlazor/Components/Select/Select.razor b/src/BootstrapBlazor/Components/Select/Select.razor index 5e0f022183f..55f9a2e6b6c 100644 --- a/src/BootstrapBlazor/Components/Select/Select.razor +++ b/src/BootstrapBlazor/Components/Select/Select.razor @@ -1,7 +1,7 @@ @namespace BootstrapBlazor.Components @using Microsoft.AspNetCore.Components.Web.Virtualization @typeparam TValue -@inherits SelectBase +@inherits SimpleSelectBase @attribute [BootstrapModuleAutoLoader(JSObjectReference = true)] @if (IsShowLabel) @@ -41,14 +41,17 @@ } @if (IsVirtualize) { - protected override string? RetrieveId() => InputId; - [NotNull] - private Virtualize? _virtualizeElement = default; - private string? InputId => $"{Id}_input"; - private string _lastSelectedValueString = string.Empty; - 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 @@ -271,14 +165,20 @@ private SelectedItem? SelectedRow { if (Value is null) { + _init = false; return null; } + if (IsVirtualize) + { + _init = false; + return new SelectedItem(CurrentValueAsString, CurrentValueAsString); + } + var item = GetItemWithEnumValue() ?? Rows.Find(i => i.Value == CurrentValueAsString) ?? Rows.Find(i => i.Active) - ?? Rows.FirstOrDefault(i => !i.IsDisabled) - ?? GetVirtualizeItem(CurrentValueAsString); + ?? Rows.FirstOrDefault(i => !i.IsDisabled); if (item != null) { @@ -299,27 +199,6 @@ private SelectedItem? SelectedRow ? Rows.Find(i => i.Value == Convert.ToInt32(Value).ToString()) : null; - private List GetRowsByItems() - { - var items = new List(); - if (Items != null) - { - items.AddRange(Items); - } - items.AddRange(_children); - return items; - } - - private List GetRowsBySearch() - { - var items = OnSearchTextChanged?.Invoke(SearchText) ?? FilterBySearchText(GetRowsByItems()); - return [.. items]; - } - - private IEnumerable FilterBySearchText(IEnumerable source) => string.IsNullOrEmpty(SearchText) - ? source - : source.Where(i => i.Text.Contains(SearchText, StringComparison)); - /// /// /// @@ -353,22 +232,7 @@ protected override async Task OnParametersSetAsync() SelectedItem = null; } - /// - /// - /// - /// - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await base.OnAfterRenderAsync(firstRender); - - if (firstRender) - { - await RefreshVirtualizeElement(); - StateHasChanged(); - } - } - - private int TotalCount { get; set; } + private int _totalCount; private List GetVirtualItems() => [.. FilterBySearchText(GetRowsByItems())]; @@ -379,21 +243,13 @@ private async ValueTask> LoadItems(ItemsProvid var count = !string.IsNullOrEmpty(SearchText) ? request.Count : GetCountByTotal(); var data = await OnQueryAsync(new() { StartIndex = request.StartIndex, Count = count, SearchText = SearchText }); - TotalCount = data.TotalCount; + _itemsCache = null; + _totalCount = data.TotalCount; var items = data.Items ?? []; - _result = new ItemsProviderResult(items, TotalCount); + _result = new ItemsProviderResult(items, _totalCount); return _result; - int GetCountByTotal() => TotalCount == 0 ? request.Count : Math.Min(request.Count, TotalCount - request.StartIndex); - } - - private async Task RefreshVirtualizeElement() - { - if (IsVirtualize && OnQueryAsync != null) - { - // 通过 ItemProvider 提供数据 - await _virtualizeElement.RefreshDataAsync(); - } + int GetCountByTotal() => _totalCount == 0 ? request.Count : Math.Min(request.Count, _totalCount - request.StartIndex); } /// @@ -423,7 +279,7 @@ private bool TryParseSelectItem(string value, [MaybeNullWhen(false)] out TValue SelectedItem? item = null; if (_result.Items != null) { - item = _result.Items.FirstOrDefault(i => i.Value == value) ?? new SelectedItem(value, DefaultVirtualizeItemText ?? value); + item = _result.Items.FirstOrDefault(i => i.Value == value); } return item; } @@ -431,7 +287,26 @@ private bool TryParseSelectItem(string value, [MaybeNullWhen(false)] out TValue /// /// /// - protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new { ConfirmMethodCallback = nameof(ConfirmSelectedItem), SearchMethodCallback = nameof(TriggerOnSearch) }); + protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new + { + ConfirmMethodCallback = nameof(ConfirmSelectedItem), + SearchMethodCallback = nameof(TriggerOnSearch) + }); + + /// + /// + /// + /// + protected override List GetRowsByItems() + { + var items = new List(); + if (Items != null) + { + items.AddRange(Items); + } + items.AddRange(_children); + return items; + } /// /// Confirms the selected item. @@ -448,20 +323,6 @@ public async Task ConfirmSelectedItem(int index) } } - /// - /// Triggers the search callback method. - /// - /// The search text. - /// A task that represents the asynchronous operation. - [JSInvokable] - public async Task TriggerOnSearch(string searchText) - { - _itemsCache = null; - SearchText = searchText; - await RefreshVirtualizeElement(); - StateHasChanged(); - } - /// /// Handles the click event for a dropdown item. /// @@ -524,33 +385,16 @@ private async Task SelectedItemChanged(SelectedItem item) public void Add(SelectedItem item) => _children.Add(item); /// - /// Clears the search text. + /// /// - public void ClearSearchText() => SearchText = null; - - private async Task OnClearValue() + /// + protected override async Task OnClearValue() { - if (ShowSearch) - { - ClearSearchText(); - } - if (OnClearAsync != null) - { - await OnClearAsync(); - } + await base.OnClearValue(); - if (OnQueryAsync != null) - { - await _virtualizeElement.RefreshDataAsync(); - } - - _lastSelectedValueString = string.Empty; - CurrentValue = default; SelectedItem = null; } - private bool IsNullable() => !ValueType.IsValueType || NullableUnderlyingType != null; - private string? ReadonlyString => IsEditable ? null : "readonly"; private async Task OnChange(ChangeEventArgs args) diff --git a/src/BootstrapBlazor/Components/Select/Select.razor.scss b/src/BootstrapBlazor/Components/Select/Select.razor.scss index 4145796e73e..c0f526ace5e 100644 --- a/src/BootstrapBlazor/Components/Select/Select.razor.scss +++ b/src/BootstrapBlazor/Components/Select/Select.razor.scss @@ -57,7 +57,6 @@ .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); } @@ -165,7 +164,7 @@ display: flex; } - &.cls:not(.disabled):hover .form-select-append { + &.is-clearable:not(.disabled):hover .form-select-append { display: none; } } diff --git a/src/BootstrapBlazor/Components/Select/SelectBase.cs b/src/BootstrapBlazor/Components/Select/SelectBase.cs index 80c102a3602..2295ba19785 100644 --- a/src/BootstrapBlazor/Components/Select/SelectBase.cs +++ b/src/BootstrapBlazor/Components/Select/SelectBase.cs @@ -97,6 +97,53 @@ public abstract class SelectBase : PopoverSelectBase [Parameter] public string? PlaceHolder { get; set; } + /// + /// Gets or sets whether virtual scrolling is enabled. Default is false. + /// + [Parameter] + public bool IsVirtualize { get; set; } + + /// + /// Gets or sets the row height for virtual scrolling. Default is 33. + /// + /// Effective when is set to true. + [Parameter] + public float RowHeight { get; set; } = 33f; + + /// + /// Gets or sets the overscan count for virtual scrolling. Default is 4. + /// + /// Effective when is set to true. + [Parameter] + public int OverscanCount { get; set; } = 4; + + /// + /// Gets or sets the default text for virtualized items. Default is null. + /// + [Parameter] + [ExcludeFromCodeCoverage] + [Obsolete("已弃用,删除即可;Deprecated, just delete")] + public string? DefaultVirtualizeItemText { get; set; } + + /// + /// Gets or sets the callback method when the clear button is clicked. Default is null. + /// + [Parameter] + public Func? OnClearAsync { get; set; } + + /// + /// Gets or sets the right-side clear icon. Default is fa-solid fa-angle-up. + /// + [Parameter] + [NotNull] + public string? ClearIcon { get; set; } + + /// + /// Gets or sets whether the select component is clearable. Default is false. + /// + [Parameter] + public bool IsClearable { get; set; } + /// /// Gets the search icon string with default "icon search-icon" class. /// @@ -128,6 +175,22 @@ public abstract class SelectBase : PopoverSelectBase .AddClass("is-fixed-search", ShowSearch && IsFixedSearch) .Build(); + /// + /// Gets the clear icon class string. + /// + protected 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(); + + /// + /// Gets the SearchLoadingIcon icon class string. + /// + protected string? SearchLoadingIconString => CssBuilder.Default("icon searching-icon") + .AddClass(SearchLoadingIcon) + .Build(); + /// /// /// @@ -150,4 +213,34 @@ protected override void OnParametersSet() /// /// A representing the asynchronous operation. public Task Hide() => InvokeVoidAsync("hide", Id); + + private bool IsNullable() => !ValueType.IsValueType || NullableUnderlyingType != null; + + /// + /// + /// + /// + protected bool GetClearable() => IsClearable && !IsDisabled && IsNullable(); + + /// + /// Clears the search text. + /// + public void ClearSearchText() => SearchText = null; + + /// + /// Clears the selected value. + /// + /// + protected virtual async Task OnClearValue() + { + if (ShowSearch) + { + ClearSearchText(); + } + if (OnClearAsync != null) + { + await OnClearAsync(); + } + CurrentValue = default; + } } diff --git a/src/BootstrapBlazor/Components/Select/SimpleSelectBase.cs b/src/BootstrapBlazor/Components/Select/SimpleSelectBase.cs new file mode 100644 index 00000000000..5b7cfd63eab --- /dev/null +++ b/src/BootstrapBlazor/Components/Select/SimpleSelectBase.cs @@ -0,0 +1,145 @@ +// 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; + +namespace BootstrapBlazor.Components; + +/// +/// SimpleSelectBase component base class +/// +/// +public abstract class SimpleSelectBase : SelectBase +{ + /// + /// Gets virtualize component instrance + /// + [NotNull] + protected Virtualize? _virtualizeElement = default; + + /// + /// Gets or sets the last selected value string. + /// + protected string _lastSelectedValueString = string.Empty; + + /// + /// Gets or sets the items. + /// + [Parameter] + [NotNull] + public IEnumerable? Items { get; set; } + + /// + /// Gets or sets the callback method for loading virtualized items. + /// + [Parameter] + [NotNull] + public Func>>? OnQueryAsync { get; set; } + + /// + /// Gets or sets the callback method when the search text changes. + /// + [Parameter] + public Func>? OnSearchTextChanged { get; set; } + + /// + /// Gets or sets whether the select component is editable. Default is false. + /// + [Parameter] + public bool IsEditable { get; set; } + + /// + /// Gets or sets the item template. + /// + [Parameter] + public RenderFragment? ItemTemplate { get; set; } + + /// + /// Gets or sets the selected items cache. + /// + protected List? _itemsCache; + + /// + /// Gets the dropdown menu rows. + /// + protected List Rows + { + get + { + _itemsCache ??= string.IsNullOrEmpty(SearchText) ? GetRowsByItems() : GetRowsBySearch(); + return _itemsCache; + } + } + + /// + /// Gets the rows by Items. + /// + /// + protected abstract List GetRowsByItems(); + + private List GetRowsBySearch() + { + var items = OnSearchTextChanged?.Invoke(SearchText) ?? FilterBySearchText(GetRowsByItems()); + return [.. items]; + } + + /// + /// Filter the items by search text. + /// + /// + /// + protected IEnumerable FilterBySearchText(IEnumerable source) => string.IsNullOrEmpty(SearchText) + ? source + : source.Where(i => i.Text.Contains(SearchText, StringComparison)); + + /// + /// Triggers the search callback method. + /// + /// The search text. + /// A task that represents the asynchronous operation. + [JSInvokable] + public async Task TriggerOnSearch(string searchText) + { + _itemsCache = null; + SearchText = searchText; + await RefreshVirtualizeElement(); + StateHasChanged(); + } + + /// + /// Refreshes the virtualize component. + /// + /// + protected async Task RefreshVirtualizeElement() + { + if (IsVirtualize && OnQueryAsync != null) + { + // 通过 ItemProvider 提供数据 + await _virtualizeElement.RefreshDataAsync(); + } + } + + /// + /// Clears the selected value. + /// + /// + protected override async Task OnClearValue() + { + if (ShowSearch) + { + ClearSearchText(); + } + if (OnClearAsync != null) + { + await OnClearAsync(); + } + CurrentValue = default; + if (OnQueryAsync != null) + { + await _virtualizeElement.RefreshDataAsync(); + } + _lastSelectedValueString = string.Empty; + } +} diff --git a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor index 1f400aa4c40..74842d3a496 100644 --- a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor +++ b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor @@ -22,7 +22,7 @@ } else { - + }
diff --git a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs index 9d9d95da2cb..713b7849a3d 100644 --- a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs +++ b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.cs @@ -23,7 +23,7 @@ public partial class SelectGeneric : ISelectGeneric, IModelEqual /// 获得 样式集合 ///
private string? ClassString => CssBuilder.Default("select dropdown") - .AddClass("cls", IsClearable) + .AddClass("is-clearable", IsClearable) .AddClassFromAttributes(AdditionalAttributes) .Build(); @@ -37,14 +37,6 @@ public partial class SelectGeneric : ISelectGeneric, IModelEqual .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 方法 /// @@ -57,13 +49,6 @@ public partial class SelectGeneric : ISelectGeneric, IModelEqual private readonly List> _children = []; - /// - /// 获得/设置 右侧清除图标 默认 fa-solid fa-angle-up - /// - [Parameter] - [NotNull] - public string? ClearIcon { get; set; } - /// /// 获得/设置 搜索文本发生变化时回调此方法 /// @@ -90,12 +75,6 @@ public partial class SelectGeneric : ISelectGeneric, IModelEqual [Parameter] public Func>? TextConvertToValueCallback { get; set; } - /// - /// 获得/设置 是否可清除 默认 false - /// - [Parameter] - public bool IsClearable { get; set; } - /// /// 获得/设置 选项模板支持静态数据 /// @@ -108,39 +87,6 @@ public partial class SelectGeneric : ISelectGeneric, IModelEqual [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 /// @@ -242,13 +188,6 @@ public partial class SelectGeneric : ISelectGeneric, IModelEqual private ItemsProviderResult> _result; - /// - /// 获得 SearchLoadingIcon 图标字符串 - /// - private string? SearchLoadingIconString => CssBuilder.Default("icon searching-icon") - .AddClass(SearchLoadingIcon) - .Build(); - private string? ScrollIntoViewBehaviorString => ScrollIntoViewBehavior == ScrollIntoViewBehavior.Smooth ? null : ScrollIntoViewBehavior.ToDescriptionString(); /// @@ -265,7 +204,7 @@ private List> Rows } } - private SelectedItem SelectedRow + private SelectedItem? SelectedRow { get { @@ -274,17 +213,35 @@ private SelectedItem SelectedRow } } - private SelectedItem GetSelectedRow() + private SelectedItem? GetSelectedRow() { + if (Value is null) + { + _init = false; + return null; + } + + if (IsVirtualize) + { + _init = false; + return new SelectedItem(default!, CurrentValueAsString); + } + var item = Rows.Find(i => Equals(i.Value, Value)) ?? Rows.Find(i => i.Active) - ?? Rows.Where(i => !i.IsDisabled).FirstOrDefault() - ?? new SelectedItem(Value, DefaultVirtualizeItemText!); + ?? Rows.Where(i => !i.IsDisabled).FirstOrDefault(); - if (!_init || !DisableItemChangedWhenFirstRender) + if (item != null) { - _ = SelectedItemChanged(item); - _init = false; + if (_init && DisableItemChangedWhenFirstRender) + { + + } + else + { + _ = SelectedItemChanged(item); + _init = false; + } } return item; } @@ -504,35 +461,18 @@ private async Task ValueTypeChanged(SelectedItem item) public void Add(SelectedItem item) => _children.Add(item); /// - /// 清空搜索栏文本内容 + /// /// - public void ClearSearchText() => SearchText = null; - - private async Task OnClearValue() + /// + protected override async Task OnClearValue() { - if (ShowSearch) - { - ClearSearchText(); - } - if (OnClearAsync != null) - { - await OnClearAsync(); - } + await base.OnClearValue(); - SelectedItem? item; if (OnQueryAsync != null) { await VirtualizeElement.RefreshDataAsync(); - item = _result.Items.FirstOrDefault(); - } - else - { - item = Items.FirstOrDefault(); - } - if (item != null) - { - await SelectedItemChanged(item); } + SelectedItem = new SelectedItem(default!, ""); } private string? ReadonlyString => IsEditable ? null : "readonly"; diff --git a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.scss b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.scss index abb759cc40b..3e3eee6f65e 100644 --- a/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.scss +++ b/src/BootstrapBlazor/Components/SelectGeneric/SelectGeneric.razor.scss @@ -164,7 +164,7 @@ display: flex; } -.select.cls:hover .form-select-append { +.select.is-clearable:not(.disabled):hover .form-select-append { display: none; } diff --git a/src/BootstrapBlazor/Enums/ComponentIcons.cs b/src/BootstrapBlazor/Enums/ComponentIcons.cs index 1d1d7b2177d..4063844f389 100644 --- a/src/BootstrapBlazor/Enums/ComponentIcons.cs +++ b/src/BootstrapBlazor/Enums/ComponentIcons.cs @@ -455,6 +455,11 @@ public enum ComponentIcons /// MultiSelectClearIcon, + /// + /// MultiSelect 组件 ClearableIcon 图标 + /// + MultiSelectClearableIcon, + /// /// SelectTree 组件 DropdownIcon 图标 /// diff --git a/src/BootstrapBlazor/Icons/BootstrapIcons.cs b/src/BootstrapBlazor/Icons/BootstrapIcons.cs index d06483d5f5b..95491bbd27a 100644 --- a/src/BootstrapBlazor/Icons/BootstrapIcons.cs +++ b/src/BootstrapBlazor/Icons/BootstrapIcons.cs @@ -110,6 +110,7 @@ internal static class BootstrapIcons { ComponentIcons.MultiSelectDropdownIcon, "bi bi-chevron-up" }, { ComponentIcons.MultiSelectClearIcon, "bi bi-x" }, + { ComponentIcons.MultiSelectClearableIcon, "bi bi-x-circle" }, { ComponentIcons.SelectTreeDropdownIcon, "bi bi-chevron-up" }, diff --git a/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs b/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs index 9503d061a45..89958267838 100644 --- a/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs +++ b/src/BootstrapBlazor/Icons/FontAwesomeIcons.cs @@ -108,6 +108,7 @@ internal static class FontAwesomeIcons { ComponentIcons.MultiSelectDropdownIcon, "fa-solid fa-angle-up" }, { ComponentIcons.MultiSelectClearIcon, "fa-solid fa-xmark" }, + { ComponentIcons.MultiSelectClearableIcon, "fa-regular fa-circle-xmark" }, { ComponentIcons.SelectTreeDropdownIcon, "fa-solid fa-angle-up" }, diff --git a/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs b/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs index 7cc019d8d30..e731f3d1bfd 100644 --- a/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs +++ b/src/BootstrapBlazor/Icons/MaterialDesignIcons.cs @@ -110,6 +110,7 @@ internal static class MaterialDesignIcons { ComponentIcons.MultiSelectDropdownIcon, "mdi mdi-chevron-up" }, { ComponentIcons.MultiSelectClearIcon, "mdi mdi-close" }, + { ComponentIcons.MultiSelectClearableIcon, "mdi mdi-trash-can-outline" }, { ComponentIcons.SelectTreeDropdownIcon, "mdi mdi-chevron-up" }, diff --git a/test/UnitTest/Components/MultiSelectTest.cs b/test/UnitTest/Components/MultiSelectTest.cs index 0d8db04fd8a..6092f33e02d 100644 --- a/test/UnitTest/Components/MultiSelectTest.cs +++ b/test/UnitTest/Components/MultiSelectTest.cs @@ -3,6 +3,9 @@ // 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 System.Reflection; + namespace UnitTest.Components; public class MultiSelectTest : BootstrapBlazorTestBase @@ -635,15 +638,16 @@ public void ClearIcon_Ok() new("1", "Test1"), new("2", "Test2") }); - pb.Add(a => a.ClearIcon, "icon-clear"); + pb.Add(a => a.IsClearable, true); }); - Assert.Contains("icon-clear", cut.Markup); + Assert.Contains("fa-regular fa-circle-xmark", cut.Markup); cut.SetParametersAndRender(pb => { - pb.Add(a => a.ClearIcon, null); + pb.Add(a => a.ClearableIcon, "icon-clear-test"); }); - Assert.Contains("fa-solid fa-xmark", cut.Markup); + Assert.DoesNotContain("fa-regular fa-circle-xmark", cut.Markup); + Assert.Contains("icon-clear-test", cut.Markup); } [Fact] @@ -661,4 +665,206 @@ public void IsMarkupString_Ok() }); Assert.Contains("
Test1
", cut.Markup); } + + [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(() => cut.Instance.TriggerOnSearch("2")); + + // 下拉框仅显示一个选项 Test2 + var items = cut.FindAll(".dropdown-item"); + Assert.Single(items); + + // 点击 Clear 按钮 + var button = cut.Find(".clear-icon"); + await cut.InvokeAsync(() => button.Click()); + + // 下拉框显示所有选项 + 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(() => cut.Instance.TriggerOnSearch("2")); + + // 下拉框仅显示一个选项 Test2 + var items = cut.FindAll(".dropdown-item"); + Assert.Single(items); + + query = false; + // 点击 Clear 按钮 + var button = cut.Find(".clear-icon"); + await cut.InvokeAsync(() => button.Click()); + + // 下拉框显示所有选项 + 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 + }); + }); + }); + + // 3 不在集合内,但是由于是虚拟集合,只能显示 + var select = cut.Instance; + Assert.Equal("3", select.Value); + + var item = cut.Find(".dropdown-item"); + await cut.InvokeAsync(() => + { + item.Click(); + }); + Assert.Equal("3,1", 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 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; + }); + }); + cut.Contains("select dropdown multi-select is-clearable"); + cut.Contains("fa-regular fa-circle-xmark"); + + var span = cut.Find(".clear-icon"); + Assert.NotNull(span); + + await cut.InvokeAsync(() => + { + span.Click(); + }); + Assert.True(clear); + } } diff --git a/test/UnitTest/Components/SelectGenericTest.cs b/test/UnitTest/Components/SelectGenericTest.cs index a33bdb1a586..97c5a71c1f4 100644 --- a/test/UnitTest/Components/SelectGenericTest.cs +++ b/test/UnitTest/Components/SelectGenericTest.cs @@ -150,7 +150,7 @@ public void IsClearable_Ok() }); var clearButton = cut.Find(".clear-icon"); cut.InvokeAsync(() => clearButton.Click()); - Assert.Empty(val); + Assert.Null(val); // 提高代码覆盖率 var select = cut; @@ -159,10 +159,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(SelectGeneric).BaseType!.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(SelectGeneric).BaseType!.GetProperty("ClearClassString", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!; val = pi.GetValue(select.Instance, null)!.ToString(); Assert.Contains("text-success", val); @@ -452,8 +452,8 @@ public void NullBool_Ok() }); // 值为 null - // 候选项中无,导致默认选择第一个 Value 被更改为 true - Assert.True(cut.Instance.Value); + // 候选项中无,可为空值为 null + Assert.Null(cut.Instance.Value); } [Fact] @@ -703,9 +703,10 @@ public async Task IsVirtualize_Items_Clearable_Ok() // 点击 Clear 按钮 var button = cut.Find(".clear-icon"); await cut.InvokeAsync(() => button.Click()); + Assert.Null(cut.Instance.Value); - // UI 恢复 Test1 - Assert.Equal("Test1", el.Value); + // UI 恢复 "" + Assert.Equal("", el.Value); // 下拉框显示所有选项 items = cut.FindAll(".dropdown-item"); @@ -761,9 +762,10 @@ public async Task IsVirtualize_OnQueryAsync_Clearable_Ok() // 点击 Clear 按钮 var button = cut.Find(".clear-icon"); await cut.InvokeAsync(() => button.Click()); + Assert.Null(cut.Instance.Value); - // UI 恢复 Test1 - Assert.Equal("All", el.Value); + // UI 恢复 "" + Assert.Equal("", el.Value); // 下拉框显示所有选项 Assert.True(query); @@ -796,7 +798,7 @@ public async Task IsVirtualize_BindValue() }); var input = cut.Find(".form-select"); - Assert.Null(input.GetAttribute("value")); + Assert.Equal("3", input.GetAttribute("value")); var select = cut.Instance; Assert.Equal("3", select.Value); @@ -809,40 +811,6 @@ public async Task IsVirtualize_BindValue() 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() { diff --git a/test/UnitTest/Components/SelectTest.cs b/test/UnitTest/Components/SelectTest.cs index 0da9524b0f3..3554a5a3674 100644 --- a/test/UnitTest/Components/SelectTest.cs +++ b/test/UnitTest/Components/SelectTest.cs @@ -264,7 +264,7 @@ public void IsNullable_Ok() private static bool IsNullable(object select) { - var mi = select.GetType().GetMethod("IsNullable", BindingFlags.Instance | BindingFlags.NonPublic)!; + var mi = select.GetType().BaseType!.BaseType!.GetMethod("IsNullable", BindingFlags.Instance | BindingFlags.NonPublic)!; return (bool)mi.Invoke(select, null)!; } @@ -900,7 +900,7 @@ public async Task IsVirtualize_BindValue() { 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; }))); @@ -935,40 +935,6 @@ await cut.InvokeAsync(() => 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() {