feat(SelectGeneric): add SelectGeneric component (#4838)

* refactor: 重构 SelectedItem 增加构造函数

* refactor: 移动 SelectedItem 到子类

* refactor: 精简代码移除 SingleSelectBase 类

* refactor: 改造 SelectedItem 泛型

* feat: 增加 SelectGeneric 组件

* feat: 增加 ToSelectList 泛型扩展方法

* refactor: 改造 Select 组件移除对泛型的支持

* doc: 更新泛型 Select 组件示例

* test: 更新单元测试

* test: 更新单元测试

* refactor: 增加排除标签

* chore: bump version 9.1.3-beta06

* test: 增加单元测试

* test: 更新单元测试

* test: 增加 Display 单元测试

* test: 更新 Select 单元测试

* refactor: 更新 SelectGeneric 值不存在时的逻辑
This commit is contained in:
Argo Zhang
2024-12-13 12:26:54 +08:00
committed by GitHub
parent a21ae6a769
commit a46e51d34e
21 changed files with 1432 additions and 287 deletions

View File

@@ -442,7 +442,7 @@
<section ignore>@((MarkupString)Localizer["SelectsGenericDesc"].Value)</section>
<div class="row">
<div class="col-12 col-sm-6">
<Select Items="_genericItems" @bind-Value="_selectedFoo" IsEditable="true"></Select>
<SelectGeneric Items="_genericItems" @bind-Value="_selectedFoo" IsEditable="true"></SelectGeneric>
</div>
<div class="col-12 col-sm-6">
<Display Value="_selectedFoo?.Address"></Display>

View File

@@ -242,7 +242,7 @@ public sealed partial class Selects
return Task.CompletedTask;
}
private readonly List<SelectedItem<Foo>> _genericItems =
private readonly List<SelectedItem<Foo?>> _genericItems =
[
new() { Text = "Foo1", Value = new Foo() { Id = 1, Address = "Address_F001" } },
new() { Text = "Foo2", Value = new Foo() { Id = 2, Address = "Address_F002" } },

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<Version>9.1.3-beta05</Version>
<Version>9.1.3-beta06</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -36,6 +36,19 @@ public partial class MultiSelect<TValue>
.AddClass("d-none", SelectedItems.Count != 0)
.Build();
/// <summary>
/// 获得/设置 绑定数据集
/// </summary>
[Parameter]
[NotNull]
public IEnumerable<SelectedItem>? Items { get; set; }
/// <summary>
/// 获得/设置 选项模板
/// </summary>
[Parameter]
public RenderFragment<SelectedItem>? ItemTemplate { get; set; }
/// <summary>
/// 获得/设置 组件 PlaceHolder 文字 默认为 点击进行多选 ...
/// </summary>

View File

@@ -1,7 +1,7 @@
@namespace BootstrapBlazor.Components
@using Microsoft.AspNetCore.Components.Web.Virtualization
@typeparam TValue
@inherits SingleSelectBase<TValue>
@inherits SelectBase<TValue>
@attribute [BootstrapModuleAutoLoader(JSObjectReference = true)]
@if (IsShowLabel)

View File

@@ -12,6 +12,7 @@ namespace BootstrapBlazor.Components;
/// Select 组件实现类
/// </summary>
/// <typeparam name="TValue"></typeparam>
[ExcludeFromCodeCoverage]
public partial class Select<TValue> : ISelect, IModelEqualityComparer<TValue>
{
[Inject]
@@ -50,7 +51,7 @@ public partial class Select<TValue> : ISelect, IModelEqualityComparer<TValue>
/// <param name="item"></param>
/// <returns></returns>
private string? ActiveItem(SelectedItem item) => CssBuilder.Default("dropdown-item")
.AddClass("active", Match(item))
.AddClass("active", item.Value == CurrentValueAsString)
.AddClass("disabled", item.IsDisabled)
.Build();
@@ -191,6 +192,55 @@ public partial class Select<TValue> : ISelect, IModelEqualityComparer<TValue>
[NotNull]
private Virtualize<SelectedItem>? VirtualizeElement { get; set; }
/// <summary>
/// 获得/设置 绑定数据集
/// </summary>
[Parameter]
[NotNull]
public IEnumerable<SelectedItem>? Items { get; set; }
/// <summary>
/// 获得/设置 选项模板
/// </summary>
[Parameter]
public RenderFragment<SelectedItem>? ItemTemplate { get; set; }
/// <summary>
/// 获得/设置 下拉框项目改变前回调委托方法 返回 true 时选项值改变,否则选项值不变
/// </summary>
[Parameter]
public Func<SelectedItem, Task<bool>>? OnBeforeSelectedItemChange { get; set; }
/// <summary>
/// SelectedItemChanged 回调方法
/// </summary>
[Parameter]
public Func<SelectedItem, Task>? OnSelectedItemChanged { get; set; }
/// <summary>
/// 获得/设置 Swal 图标 默认 Question
/// </summary>
[Parameter]
public SwalCategory SwalCategory { get; set; } = SwalCategory.Question;
/// <summary>
/// 获得/设置 Swal 标题 默认 null
/// </summary>
[Parameter]
public string? SwalTitle { get; set; }
/// <summary>
/// 获得/设置 Swal 内容 默认 null
/// </summary>
[Parameter]
public string? SwalContent { get; set; }
/// <summary>
/// 获得/设置 Footer 默认 null
/// </summary>
[Parameter]
public string? SwalFooter { get; set; }
[Inject]
[NotNull]
private IStringLocalizer<Select<TValue>>? Localizer { get; set; }
@@ -214,6 +264,11 @@ public partial class Select<TValue> : ISelect, IModelEqualityComparer<TValue>
private ItemsProviderResult<SelectedItem> _result;
/// <summary>
/// 当前选择项实例
/// </summary>
private SelectedItem? SelectedItem { get; set; }
private List<SelectedItem> Rows
{
get
@@ -234,7 +289,7 @@ public partial class Select<TValue> : ISelect, IModelEqualityComparer<TValue>
private SelectedItem? GetSelectedRow()
{
var item = Rows.Find(Match)
var item = Rows.Find(i => i.Value == CurrentValueAsString)
?? Rows.Find(i => i.Active)
?? Rows.Where(i => !i.IsDisabled).FirstOrDefault()
?? GetVirtualizeItem();
@@ -374,8 +429,6 @@ public partial class Select<TValue> : ISelect, IModelEqualityComparer<TValue>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(ConfirmSelectedItem));
private bool Match(SelectedItem i) => i is SelectedItem<TValue> d ? Equals(d.Value, Value) : i.Value.Equals(CurrentValueAsString, StringComparison);
/// <summary>
/// 客户端回车回调方法
/// </summary>
@@ -453,13 +506,13 @@ public partial class Select<TValue> : ISelect, IModelEqualityComparer<TValue>
{
if (_lastSelectedValueString != item.Value)
{
_lastSelectedValueString = item.Value;
item.Active = true;
SelectedItem = item;
// 触发 StateHasChanged
CurrentValueAsString = item.Value;
_lastSelectedValueString = item.Value ?? "";
CurrentValueAsString = _lastSelectedValueString;
// 触发 SelectedItemChanged 事件
if (OnSelectedItemChanged != null)
@@ -518,41 +571,13 @@ public partial class Select<TValue> : ISelect, IModelEqualityComparer<TValue>
if (item == null)
{
// 判断是否为泛型 SelectedItem
var itemType = Items.GetType();
var isGeneric = false;
if (itemType.IsGenericType)
{
isGeneric = itemType.GetGenericArguments()[0].IsGenericType;
}
if (isGeneric)
{
TValue? val = default;
if (TextConvertToValueCallback != null)
{
val = await TextConvertToValueCallback(v);
}
item = new SelectedItem<TValue>() { Text = v, Value = val };
}
else
{
item = new SelectedItem(v, v);
}
item = new SelectedItem(v, v);
var items = new List<SelectedItem>() { item };
items.AddRange(Items);
Items = items;
CurrentValueAsString = v;
}
if (item is SelectedItem<TValue> value)
{
CurrentValue = value.Value;
}
else
{
CurrentValueAsString = v;
}
CurrentValueAsString = v;
if (OnInputChangedCallback != null)
{

View File

@@ -16,13 +16,6 @@ public abstract class SelectBase<TValue> : PopoverSelectBase<TValue>
[Parameter]
public Color Color { get; set; }
/// <summary>
/// 获得/设置 绑定数据集
/// </summary>
[Parameter]
[NotNull]
public IEnumerable<SelectedItem>? Items { get; set; }
/// <summary>
/// 获得/设置 是否显示搜索框 默认为 false 不显示
/// </summary>
@@ -54,12 +47,6 @@ public abstract class SelectBase<TValue> : PopoverSelectBase<TValue>
[Parameter]
public StringComparison StringComparison { get; set; } = StringComparison.OrdinalIgnoreCase;
/// <summary>
/// 获得/设置 选项模板
/// </summary>
[Parameter]
public RenderFragment<SelectedItem>? ItemTemplate { get; set; }
/// <summary>
/// 获得/设置 分组项模板
/// </summary>

View File

@@ -8,6 +8,7 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// SelectOption 组件
/// </summary>
[ExcludeFromCodeCoverage]
public class SelectOption : ComponentBase
{
/// <summary>

View File

@@ -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;
/// <summary>
/// Select 组件基类
/// </summary>
public abstract class SingleSelectBase<TValue> : SelectBase<TValue>
{
/// <summary>
/// 当前选择项实例
/// </summary>
protected SelectedItem? SelectedItem { get; set; }
/// <summary>
/// 获得/设置 Swal 图标 默认 Question
/// </summary>
[Parameter]
public SwalCategory SwalCategory { get; set; } = SwalCategory.Question;
/// <summary>
/// 获得/设置 Swal 标题 默认 null
/// </summary>
[Parameter]
public string? SwalTitle { get; set; }
/// <summary>
/// 获得/设置 Swal 内容 默认 null
/// </summary>
[Parameter]
public string? SwalContent { get; set; }
/// <summary>
/// 获得/设置 Footer 默认 null
/// </summary>
[Parameter]
public string? SwalFooter { get; set; }
/// <summary>
/// 获得/设置 下拉框项目改变前回调委托方法 返回 true 时选项值改变,否则选项值不变
/// </summary>
[Parameter]
public Func<SelectedItem, Task<bool>>? OnBeforeSelectedItemChange { get; set; }
/// <summary>
/// SelectedItemChanged 回调方法
/// </summary>
[Parameter]
public Func<SelectedItem, Task>? OnSelectedItemChanged { get; set; }
}

View File

@@ -0,0 +1,18 @@
// 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;
/// <summary>
/// ISelect 接口
/// </summary>
public interface ISelectGeneric<TValue>
{
/// <summary>
/// 增加 SelectedItem 项方法
/// </summary>
/// <param name="item"></param>
void Add(SelectedItem<TValue> item);
}

View File

@@ -0,0 +1,115 @@
@namespace BootstrapBlazor.Components
@using Microsoft.AspNetCore.Components.Web.Virtualization
@typeparam TValue
@inherits SelectBase<TValue>
@attribute [BootstrapModuleAutoLoader(JSObjectReference = true)]
@if (IsShowLabel)
{
<BootstrapLabel required="@Required" for="@InputId" ShowLabelTooltip="ShowLabelTooltip" Value="@DisplayText" />
}
<div @attributes="AdditionalAttributes" id="@Id" class="@ClassString">
<CascadingValue Value="this" IsFixed="true">
@Options
</CascadingValue>
<RenderTemplate>
<div class="dropdown-toggle" data-bs-toggle="@ToggleString" data-bs-placement="@PlacementString" data-bs-offset="@OffsetString" data-bs-custom-class="@CustomClassString">
@if (DisplayTemplate != null)
{
<div id="@InputId" class="@InputClassString" tabindex="0">
@DisplayTemplate(SelectedRow)
</div>
}
else
{
<input type="text" id="@InputId" disabled="@Disabled" placeholder="@PlaceHolder" class="@InputClassString" value="@SelectedRow?.Text" @onchange="OnChange" readonly="@ReadonlyString" />
}
<span class="@AppendClassString"><i class="@DropdownIcon"></i></span>
</div>
@if (GetClearable())
{
<span class="@ClearClassString" @onclick="OnClearValue"><i class="@ClearIcon"></i></span>
}
<div class="dropdown-menu">
@if (IsVirtualize)
{
@if (ShowSearch)
{
<div class="@SearchClassString">
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" @oninput="EventCallback.Factory.CreateBinder<string>(this, async v => await SearchTextChanged(v), SearchText)" aria-label="Search">
<i class="@SearchIconString"></i>
</div>
}
<div class="dropdown-virtual">
@if (OnQueryAsync == null)
{
<Virtualize ItemSize="RowHeight" OverscanCount="OverscanCount" Items="@GetVirtualItems()" ChildContent="RenderRow" />
}
else
{
<Virtualize ItemSize="RowHeight" OverscanCount="OverscanCount" ItemsProvider="LoadItems" Placeholder="RenderPlaceHolderRow" ItemContent="RenderRow" @ref="VirtualizeElement" />
}
</div>
}
else
{
@if (ShowSearch)
{
<div class="@SearchClassString">
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" @oninput="EventCallback.Factory.CreateBinder<string>(this, async v => await SearchTextChanged(v), SearchText)" aria-label="Search">
<i class="@SearchIconString"></i>
</div>
}
@foreach (var itemGroup in Rows.GroupBy(i => i.GroupName))
{
if (!string.IsNullOrEmpty(itemGroup.Key))
{
if (GroupItemTemplate != null)
{
@GroupItemTemplate(itemGroup.Key)
}
else
{
<Divider Text="@itemGroup.Key" />
}
}
@foreach (var item in itemGroup)
{
@RenderRow(item)
}
}
@if (Rows.Count == 0)
{
<div class="dropdown-item">@NoSearchDataText</div>
}
}
</div>
@if (!IsPopover)
{
<div class="dropdown-menu-arrow"></div>
}
</RenderTemplate>
</div>
@code {
RenderFragment<SelectedItem<TValue>> RenderRow => item =>
@<div class="@ActiveItem(item)" @onclick="() => OnClickItem(item)">
@if (ItemTemplate != null)
{
@ItemTemplate(item)
}
else if (IsMarkupString)
{
@((MarkupString)item.Text)
}
else
{
@item.Text
}
</div>;
RenderFragment<PlaceholderContext> RenderPlaceHolderRow => context =>
@<div class="dropdown-item">
<div class="is-ph"></div>
</div>;
}

View File

@@ -0,0 +1,566 @@
// 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;
/// <summary>
/// Select 组件实现类
/// </summary>
/// <typeparam name="TValue"></typeparam>
public partial class SelectGeneric<TValue> : ISelectGeneric<TValue>, IModelEqualityComparer<TValue>
{
[Inject]
[NotNull]
private SwalService? SwalService { get; set; }
/// <summary>
/// 获得 样式集合
/// </summary>
private string? ClassString => CssBuilder.Default("select dropdown")
.AddClass("cls", IsClearable)
.AddClassFromAttributes(AdditionalAttributes)
.Build();
/// <summary>
/// 获得 样式集合
/// </summary>
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;
/// <summary>
/// 设置当前项是否 Active 方法
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
private string? ActiveItem(SelectedItem<TValue> 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<SelectedItem<TValue>> _children = [];
/// <summary>
/// 获得/设置 右侧清除图标 默认 fa-solid fa-angle-up
/// </summary>
[Parameter]
[NotNull]
public string? ClearIcon { get; set; }
/// <summary>
/// 获得/设置 搜索文本发生变化时回调此方法
/// </summary>
[Parameter]
public Func<string, IEnumerable<SelectedItem<TValue>>>? OnSearchTextChanged { get; set; }
/// <summary>
/// 获得/设置 是否固定下拉框中的搜索栏 默认 false
/// </summary>
[Parameter]
public bool IsFixedSearch { get; set; }
/// <summary>
/// 获得/设置 是否可编辑 默认 false
/// </summary>
[Parameter]
public bool IsEditable { get; set; }
/// <summary>
/// 获得/设置 选项输入更新后回调方法 默认 null
/// </summary>
/// <remarks>设置 <see cref="IsEditable"/> 后生效</remarks>
[Parameter]
public Func<string, Task>? OnInputChangedCallback { get; set; }
/// <summary>
/// 获得/设置 选项输入更新后转换为 Value 回调方法 默认 null
/// </summary>
/// <remarks>设置 <see cref="IsEditable"/> 后生效</remarks>
[Parameter]
public Func<string, Task<TValue>>? TextConvertToValueCallback { get; set; }
/// <summary>
/// 获得/设置 无搜索结果时显示文字
/// </summary>
[Parameter]
public string? NoSearchDataText { get; set; }
/// <summary>
/// 获得 PlaceHolder 属性
/// </summary>
[Parameter]
public string? PlaceHolder { get; set; }
/// <summary>
/// 获得/设置 是否可清除 默认 false
/// </summary>
[Parameter]
public bool IsClearable { get; set; }
/// <summary>
/// 获得/设置 选项模板支持静态数据
/// </summary>
[Parameter]
public RenderFragment? Options { get; set; }
/// <summary>
/// 获得/设置 显示部分模板 默认 null
/// </summary>
[Parameter]
public RenderFragment<SelectedItem<TValue>?>? DisplayTemplate { get; set; }
/// <summary>
/// 获得/设置 是否开启虚拟滚动 默认 false 未开启 注意:开启虚拟滚动后不支持 <see cref="SelectBase{TValue}.ShowSearch"/> <see cref="PopoverSelectBase{TValue}.IsPopover"/> <seealso cref="IsFixedSearch"/> 参数设置,设置初始值时请设置 <see cref="DefaultVirtualizeItemText"/>
/// </summary>
[Parameter]
public bool IsVirtualize { get; set; }
/// <summary>
/// 获得/设置 虚拟滚动行高 默认为 33
/// </summary>
/// <remarks>需要设置 <see cref="IsVirtualize"/> 值为 true 时生效</remarks>
[Parameter]
public float RowHeight { get; set; } = 33f;
/// <summary>
/// 获得/设置 过载阈值数 默认为 4
/// </summary>
/// <remarks>需要设置 <see cref="IsVirtualize"/> 值为 true 时生效</remarks>
[Parameter]
public int OverscanCount { get; set; } = 4;
/// <summary>
/// 获得/设置 默认文本 <see cref="IsVirtualize"/> 时生效 默认 null
/// </summary>
/// <remarks>开启 <see cref="IsVirtualize"/> 并且通过 <see cref="OnQueryAsync"/> 提供数据源时,由于渲染时还未调用或者调用后数据集未包含 <see cref="DisplayBase{TValue}.Value"/> 选项值,此时使用 DefaultText 值渲染</remarks>
[Parameter]
public string? DefaultVirtualizeItemText { get; set; }
/// <summary>
/// 获得/设置 清除文本内容 OnClear 回调方法 默认 null
/// </summary>
[Parameter]
public Func<Task>? OnClearAsync { get; set; }
/// <summary>
/// 获得/设置 禁止首次加载时触发 OnSelectedItemChanged 回调方法 默认 false
/// </summary>
[Parameter]
public bool DisableItemChangedWhenFirstRender { get; set; }
/// <summary>
/// 获得/设置 比较数据是否相同回调方法 默认为 null
/// <para>提供此回调方法时忽略 <see cref="CustomKeyAttribute"/> 属性</para>
/// </summary>
[Parameter]
public Func<TValue, TValue, bool>? ValueEqualityComparer { get; set; }
Func<TValue, TValue, bool>? IModelEqualityComparer<TValue>.ModelEqualityComparer
{
get => ValueEqualityComparer;
set => ValueEqualityComparer = value;
}
/// <summary>
/// 获得/设置 数据主键标识标签 默认为 <see cref="KeyAttribute"/>用于判断数据主键标签,如果模型未设置主键时可使用 <see cref="ValueEqualityComparer"/> 参数自定义判断数据模型支持联合主键
/// </summary>
[Parameter]
[NotNull]
public Type? CustomKeyAttribute { get; set; } = typeof(KeyAttribute);
[NotNull]
private Virtualize<SelectedItem<TValue>>? VirtualizeElement { get; set; }
/// <summary>
/// 获得/设置 绑定数据集
/// </summary>
[Parameter]
[NotNull]
public IEnumerable<SelectedItem<TValue>>? Items { get; set; }
/// <summary>
/// 获得/设置 选项模板
/// </summary>
[Parameter]
public RenderFragment<SelectedItem<TValue>>? ItemTemplate { get; set; }
/// <summary>
/// 获得/设置 下拉框项目改变前回调委托方法 返回 true 时选项值改变,否则选项值不变
/// </summary>
[Parameter]
public Func<SelectedItem<TValue>, Task<bool>>? OnBeforeSelectedItemChange { get; set; }
/// <summary>
/// SelectedItemChanged 回调方法
/// </summary>
[Parameter]
public Func<SelectedItem<TValue>, Task>? OnSelectedItemChanged { get; set; }
/// <summary>
/// 获得/设置 Swal 图标 默认 Question
/// </summary>
[Parameter]
public SwalCategory SwalCategory { get; set; } = SwalCategory.Question;
/// <summary>
/// 获得/设置 Swal 标题 默认 null
/// </summary>
[Parameter]
public string? SwalTitle { get; set; }
/// <summary>
/// 获得/设置 Swal 内容 默认 null
/// </summary>
[Parameter]
public string? SwalContent { get; set; }
/// <summary>
/// 获得/设置 Footer 默认 null
/// </summary>
[Parameter]
public string? SwalFooter { get; set; }
[Inject]
[NotNull]
private IStringLocalizer<Select<TValue>>? Localizer { get; set; }
/// <summary>
/// 获得 input 组件 Id 方法
/// </summary>
/// <returns></returns>
protected override string? RetrieveId() => InputId;
/// <summary>
/// 获得/设置 Select 内部 Input 组件 Id
/// </summary>
private string? InputId => $"{Id}_input";
private TValue? _lastSelectedValue;
private bool _init = true;
private List<SelectedItem<TValue>>? _itemsCache;
private ItemsProviderResult<SelectedItem<TValue>> _result;
/// <summary>
/// 当前选择项实例
/// </summary>
private SelectedItem<TValue>? SelectedItem { get; set; }
private List<SelectedItem<TValue>> Rows
{
get
{
_itemsCache ??= string.IsNullOrEmpty(SearchText) ? GetRowsByItems() : GetRowsBySearch();
return _itemsCache;
}
}
private SelectedItem<TValue>? SelectedRow
{
get
{
SelectedItem ??= GetSelectedRow();
return SelectedItem;
}
}
private SelectedItem<TValue>? GetSelectedRow()
{
var item = Rows.Find(i => Equals(i.Value, Value))
?? Rows.Find(i => i.Active)
?? Rows.Where(i => !i.IsDisabled).FirstOrDefault()
?? new SelectedItem<TValue>(Value, DefaultVirtualizeItemText!);
if (!_init || !DisableItemChangedWhenFirstRender)
{
_ = SelectedItemChanged(item);
_init = false;
}
return item;
}
private List<SelectedItem<TValue>> GetRowsByItems()
{
var items = new List<SelectedItem<TValue>>();
items.AddRange(Items);
items.AddRange(_children);
return items;
}
private List<SelectedItem<TValue>> GetRowsBySearch()
{
var items = OnSearchTextChanged?.Invoke(SearchText) ?? FilterBySearchText(GetRowsByItems());
return items.ToList();
}
private IEnumerable<SelectedItem<TValue>> FilterBySearchText(IEnumerable<SelectedItem<TValue>> source) => string.IsNullOrEmpty(SearchText)
? source
: source.Where(i => i.Text.Contains(SearchText, StringComparison));
/// <summary>
/// <inheritdoc/>
/// </summary>
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<TValue>(string.IsNullOrEmpty(item) ? null : new SelectedItem<TValue>(default!, item));
}
_itemsCache = null;
SelectedItem = null;
}
/// <summary>
/// 获得/设置 数据总条目
/// </summary>
private int TotalCount { get; set; }
private List<SelectedItem<TValue>> GetVirtualItems() => FilterBySearchText(GetRowsByItems()).ToList();
/// <summary>
/// 虚拟滚动数据加载回调方法
/// </summary>
[Parameter]
[NotNull]
public Func<VirtualizeQueryOption, Task<QueryData<SelectedItem<TValue>>>>? OnQueryAsync { get; set; }
private async ValueTask<ItemsProviderResult<SelectedItem<TValue>>> 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<SelectedItem<TValue>>(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();
}
}
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(ConfirmSelectedItem));
/// <summary>
/// 客户端回车回调方法
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
[JSInvokable]
public async Task ConfirmSelectedItem(int index)
{
if (index < Rows.Count)
{
await OnClickItem(Rows[index]);
StateHasChanged();
}
}
/// <summary>
/// 下拉框选项点击时调用此方法
/// </summary>
private async Task OnClickItem(SelectedItem<TValue> 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<TValue> 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<TValue> 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);
}
}
}
/// <summary>
/// 添加静态下拉项方法
/// </summary>
/// <param name="item"></param>
public void Add(SelectedItem<TValue> item) => _children.Add(item);
/// <summary>
/// 清空搜索栏文本内容
/// </summary>
public void ClearSearchText() => SearchText = null;
private async Task OnClearValue()
{
if (ShowSearch)
{
ClearSearchText();
}
if (OnClearAsync != null)
{
await OnClearAsync();
}
SelectedItem<TValue>? 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<TValue>(val, v);
var items = new List<SelectedItem<TValue>>() { item };
items.AddRange(Items);
Items = items;
CurrentValue = val;
}
else
{
CurrentValue = item.Value;
}
if (OnInputChangedCallback != null)
{
await OnInputChangedCallback(v);
}
}
}
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public bool Equals(TValue? x, TValue? y) => this.Equals<TValue>(x, y);
}

View File

@@ -0,0 +1,139 @@
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)
}

View File

@@ -0,0 +1,241 @@
.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;
}

View File

@@ -0,0 +1,65 @@
// 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;
/// <summary>
/// SelectOptionPro 组件
/// </summary>
public class SelectOptionGeneric<TValue> : ComponentBase
{
/// <summary>
/// 获得/设置 显示名称
/// </summary>
[Parameter]
public string? Text { get; set; }
/// <summary>
/// 获得/设置 选项值
/// </summary>
[Parameter]
public TValue? Value { get; set; }
/// <summary>
/// 获得/设置 是否选中 默认 false
/// </summary>
[Parameter]
public bool Active { get; set; }
/// <summary>
/// 获得/设置 是否禁用 默认 false
/// </summary>
[Parameter]
public bool IsDisabled { get; set; }
/// <summary>
/// 获得/设置 分组名称
/// </summary>
[Parameter]
public string? GroupName { get; set; }
/// <summary>
/// 父组件通过级联参数获得
/// </summary>
[CascadingParameter]
private ISelectGeneric<TValue>? Container { get; set; }
/// <summary>
/// OnInitialized 方法
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();
Container?.Add(ToSelectedItem());
}
private SelectedItem<TValue> ToSelectedItem() => new(Value, Text ?? "")
{
Active = Active,
GroupName = GroupName ?? "",
IsDisabled = IsDisabled
};
}

View File

@@ -72,6 +72,33 @@ public static class EnumExtensions
return ret;
}
/// <summary>
/// 获取指定枚举类型的枚举值集合,默认通过 DisplayAttribute DescriptionAttribute 标签显示 DisplayName 支持资源文件 回退机制显示字段名称
/// </summary>
/// <param name="type"></param>
/// <param name="additionalItem"></param>
/// <returns></returns>
public static List<SelectedItem<TValue>> ToSelectList<TValue>(this Type type, SelectedItem<TValue>? additionalItem = null)
{
var ret = new List<SelectedItem<TValue>>();
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<TValue>(val, desc));
}
}
return ret;
}
/// <summary>
/// 判断类型是否为枚举类型
/// </summary>

View File

@@ -18,12 +18,16 @@ public class SelectedItem
/// <summary>
/// 构造函数
/// </summary>
public SelectedItem(string value, string text) => (Value, Text) = (value, text);
public SelectedItem(string value, string text)
{
Value = value ?? "";
Text = text;
}
/// <summary>
/// 获得/设置 显示名称
/// </summary>
public virtual string Text { get; set; } = "";
public string Text { get; set; } = "";
/// <summary>
/// 获得/设置 选项值

View File

@@ -6,12 +6,48 @@
namespace BootstrapBlazor.Components;
/// <summary>
/// <see cref="SelectedItem"/> 泛型实现类
/// 泛型实现类
/// </summary>
public class SelectedItem<T> : SelectedItem
public class SelectedItem<T>
{
/// <summary>
/// 获得/设置 泛型值
/// 构造函数
/// </summary>
public new T? Value { get; set; }
public SelectedItem() { }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="value"></param>
/// <param name="text"></param>
public SelectedItem(T? value, string text)
{
Value = value;
Text = text;
}
/// <summary>
/// 获得/设置 显示名称
/// </summary>
public string Text { get; set; } = "";
/// <summary>
/// 获得/设置 选项值
/// </summary>
public T? Value { get; set; }
/// <summary>
/// 获得/设置 是否选中
/// </summary>
public bool Active { get; set; }
/// <summary>
/// 获得/设置 是否禁用
/// </summary>
public bool IsDisabled { get; set; }
/// <summary>
/// 获得/设置 分组名称
/// </summary>
public string GroupName { get; set; } = "";
}

View File

@@ -226,6 +226,14 @@ public class DisplayTest : BootstrapBlazorTestBase
Assert.Contains("中学", cut.Markup);
}
[Fact]
public void Format_Test()
{
var cut = Context.RenderComponent<MockComponent>();
var result = cut.Instance.Test(new SelectedItem("1", "Test"));
Assert.Equal("1", result);
}
class DisplayGenericValueMock<T>
{
[NotNull]
@@ -246,4 +254,12 @@ public class DisplayTest : BootstrapBlazorTestBase
return Value.ToString();
}
}
class MockComponent : DisplayBase<SelectedItem>
{
public string? Test(SelectedItem v)
{
return base.FormatValueAsString(v);
}
}
}

View File

@@ -13,15 +13,22 @@ 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<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Select<string>>(pb =>
pb.AddChildContent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.ShowSearch, true);
pb.Add(a => a.Items, new List<SelectedItem>()
pb.Add(a => a.Items, new List<SelectedItem<string>>()
{
new("1", "Test1"),
new("2", "Test2") { IsDisabled = true }
@@ -29,7 +36,7 @@ public class SelectTest : BootstrapBlazorTestBase
});
});
var ctx = cut.FindComponent<Select<string>>();
var ctx = cut.FindComponent<SelectGeneric<string>>();
await ctx.InvokeAsync(async () =>
{
await ctx.Instance.ConfirmSelectedItem(0);
@@ -54,7 +61,7 @@ public class SelectTest : BootstrapBlazorTestBase
pb.Add(a => a.OnSelectedItemChanged, null);
pb.Add(a => a.OnSearchTextChanged, text =>
{
return new List<SelectedItem>()
return new List<SelectedItem<string>>()
{
new("1", "Test1")
};
@@ -71,11 +78,11 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void Options_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Options, builder =>
{
builder.OpenComponent<SelectOption>(0);
builder.OpenComponent<SelectOptionGeneric<string>>(0);
builder.AddAttribute(1, "Text", "Test-Select");
builder.CloseComponent();
@@ -89,13 +96,13 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void Disabled_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.IsDisabled, true);
pb.Add(a => a.Options, builder =>
{
builder.OpenComponent<SelectOption>(0);
builder.AddAttribute(1, nameof(SelectOption.IsDisabled), true);
builder.OpenComponent<SelectOptionGeneric<string>>(0);
builder.AddAttribute(1, nameof(SelectOptionGeneric<string>.IsDisabled), true);
builder.CloseComponent();
builder.OpenComponent<SelectOption>(2);
@@ -110,10 +117,10 @@ public class SelectTest : BootstrapBlazorTestBase
public void IsClearable_Ok()
{
var val = "Test2";
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.IsClearable, true);
pb.Add(a => a.Items, new List<SelectedItem>()
pb.Add(a => a.Items, new List<SelectedItem<string>>()
{
new("", "请选择"),
new("2", "Test2"),
@@ -137,10 +144,10 @@ public class SelectTest : BootstrapBlazorTestBase
pb.Add(a => a.Color, Color.Danger);
});
var validPi = typeof(Select<string>).GetProperty("IsValid", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
var validPi = typeof(SelectGeneric<string>).GetProperty("IsValid", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
validPi.SetValue(select.Instance, true);
var pi = typeof(Select<string>).GetProperty("ClearClassString", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
var pi = typeof(SelectGeneric<string>).GetProperty("ClearClassString", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
val = pi.GetValue(select.Instance, null)!.ToString();
Assert.Contains("text-success", val);
@@ -152,7 +159,7 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void SelectOption_Ok()
{
var cut = Context.RenderComponent<SelectOption>(pb =>
var cut = Context.RenderComponent<SelectOptionGeneric<string>>(pb =>
{
pb.Add(a => a.Text, "Test-SelectOption");
pb.Add(a => a.GroupName, "Test-GroupName");
@@ -165,14 +172,14 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void Enum_Ok()
{
var cut = Context.RenderComponent<Select<EnumEducation>>();
var cut = Context.RenderComponent<SelectGeneric<EnumEducation>>();
Assert.Equal(2, cut.FindAll(".dropdown-item").Count);
}
[Fact]
public void NullableEnum_Ok()
{
var cut = Context.RenderComponent<Select<EnumEducation?>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<EnumEducation?>>(pb =>
{
pb.Add(a => a.AdditionalAttributes, new Dictionary<string, object>()
{
@@ -188,9 +195,9 @@ public class SelectTest : BootstrapBlazorTestBase
var triggered = false;
// 空值时,不触发 OnSelectedItemChanged 回调
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("", "Test"),
new("1", "Test2")
@@ -202,35 +209,29 @@ public class SelectTest : BootstrapBlazorTestBase
return Task.CompletedTask;
});
});
Assert.False(triggered);
Assert.True(triggered);
// 切换候选项时触发 OnSelectedItemChanged 回调测试
await cut.InvokeAsync(() =>
{
var items = cut.FindAll(".dropdown-item");
var count = items.Count;
Assert.Equal(2, count);
var items = cut.FindAll(".dropdown-item");
var count = items.Count;
Assert.Equal(2, count);
var item = items[1];
item.Click();
});
var item = items[1];
await cut.InvokeAsync(() => { item.Click(); });
Assert.True(triggered);
// 切换回 空值 触发 OnSelectedItemChanged 回调测试
triggered = false;
await cut.InvokeAsync(() =>
{
var items = cut.FindAll(".dropdown-item");
var item = items[0];
item.Click();
});
items = cut.FindAll(".dropdown-item");
item = items[0];
await cut.InvokeAsync(() => { item.Click(); });
Assert.True(triggered);
// 首次加载值不为空时触发 OnSelectedItemChanged 回调测试
triggered = false;
cut.SetParametersAndRender(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("", "Test"),
new("1", "Test1"),
@@ -242,14 +243,11 @@ public class SelectTest : BootstrapBlazorTestBase
// 切换回 空值 触发 OnSelectedItemChanged 回调测试
triggered = false;
await cut.InvokeAsync(() =>
{
var items = cut.FindAll(".dropdown-item");
var count = items.Count;
Assert.Equal(3, count);
var item = items[0];
item.Click();
});
items = cut.FindAll(".dropdown-item");
count = items.Count;
Assert.Equal(3, count);
item = items[0];
await cut.InvokeAsync(() => { item.Click(); });
Assert.True(triggered);
}
@@ -257,7 +255,7 @@ public class SelectTest : BootstrapBlazorTestBase
public async Task OnSelectedItemChanged_Generic()
{
Foo? selectedValue = null;
var cut = Context.RenderComponent<Select<Foo>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<Foo>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem<Foo>[]
{
@@ -291,9 +289,9 @@ public class SelectTest : BootstrapBlazorTestBase
var triggered = false;
// 空值时,不触发 OnSelectedItemChanged 回调
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test"),
new("2", "Test2")
@@ -312,7 +310,7 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void Color_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Color, Color.Danger);
});
@@ -338,7 +336,7 @@ public class SelectTest : BootstrapBlazorTestBase
return Task.CompletedTask;
});
builder.Add(a => a.Model, model);
builder.AddChildContent<Select<string>>(pb =>
builder.AddChildContent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Value, model.Name);
pb.Add(a => a.OnValueChanged, v =>
@@ -347,7 +345,7 @@ public class SelectTest : BootstrapBlazorTestBase
return Task.CompletedTask;
});
pb.Add(a => a.ValueExpression, model.GenerateValueExpression());
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("", "Test"),
new("1", "Test1") { GroupName = "Test1" },
@@ -363,7 +361,7 @@ public class SelectTest : BootstrapBlazorTestBase
Assert.True(valid);
});
var ctx = cut.FindComponent<Select<string>>();
var ctx = cut.FindComponent<SelectGeneric<string>>();
ctx.InvokeAsync(async () =>
{
await ctx.Instance.ConfirmSelectedItem(0);
@@ -376,9 +374,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void ItemTemplate_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1") { GroupName = "Test1" },
new("2", "Test2") { GroupName = "Test2" }
@@ -398,9 +396,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void GroupItemTemplate_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1") { GroupName = "Test1" },
new("2", "Test2") { GroupName = "Test2" }
@@ -421,19 +419,19 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void NullItems_Ok()
{
var cut = Context.RenderComponent<Select<string>>();
var cut = Context.RenderComponent<SelectGeneric<string>>();
Assert.Contains("select", cut.Markup);
}
[Fact]
public void NullBool_Ok()
{
var cut = Context.RenderComponent<Select<bool?>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<bool?>>(pb =>
{
pb.Add(a => a.Items, new List<SelectedItem>
pb.Add(a => a.Items, new List<SelectedItem<bool?>>
{
new("true", "True"),
new("false", "False"),
new(true, "True"),
new(false, "False"),
});
pb.Add(a => a.Value, null);
});
@@ -443,32 +441,12 @@ public class SelectTest : BootstrapBlazorTestBase
Assert.True(cut.Instance.Value);
}
[Fact]
public void SelectItem_Ok()
{
var v = new SelectedItem("2", "Text2");
var cut = Context.RenderComponent<Select<SelectedItem>>(pb =>
{
pb.Add(a => a.Items, new List<SelectedItem>
{
new("1", "Text1"),
new("2", "Text2"),
});
pb.Add(a => a.Value, v);
pb.Add(a => a.ValueChanged, EventCallback.Factory.Create<SelectedItem?>(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<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -483,9 +461,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void IsFixedSearch_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -501,9 +479,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void CustomClass_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -517,9 +495,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void ShowShadow_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -538,9 +516,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void DropdownIcon_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -554,9 +532,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void DisplayTemplate_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -573,9 +551,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void IsPopover_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -590,9 +568,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void Offset_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -607,9 +585,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void Placement_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -629,9 +607,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void ItemClick_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -651,9 +629,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void IsVirtualize_Items()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -676,9 +654,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public async Task IsVirtualize_Items_Clearable_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -727,7 +705,7 @@ public class SelectTest : BootstrapBlazorTestBase
var startIndex = 0;
var requestCount = 0;
var searchText = string.Empty;
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.OnQueryAsync, option =>
{
@@ -735,7 +713,7 @@ public class SelectTest : BootstrapBlazorTestBase
startIndex = option.StartIndex;
requestCount = option.Count;
searchText = option.SearchText;
return Task.FromResult(new QueryData<SelectedItem>()
return Task.FromResult(new QueryData<SelectedItem<string>>()
{
Items = string.IsNullOrEmpty(searchText)
? [new("", "All"), new("1", "Test1"), new("2", "Test2")]
@@ -779,22 +757,22 @@ public class SelectTest : BootstrapBlazorTestBase
}
[Fact]
public void IsVirtualize_BindValue()
public async Task IsVirtualize_BindValue()
{
var value = new SelectedItem("3", "Test 3");
var cut = Context.RenderComponent<Select<SelectedItem>>(pb =>
var value = "3";
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Value, value);
pb.Add(a => a.IsVirtualize, true);
pb.Add(a => a.ValueChanged, EventCallback.Factory.Create<SelectedItem?>(this, new Action<SelectedItem?>(item =>
pb.Add(a => a.ValueChanged, EventCallback.Factory.Create<string?>(this, new Action<string?>(item =>
{
value = item;
})));
pb.Add(a => a.OnQueryAsync, option =>
{
return Task.FromResult(new QueryData<SelectedItem>()
return Task.FromResult(new QueryData<SelectedItem<string>>()
{
Items = new SelectedItem[]
Items = new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -804,31 +782,25 @@ public class SelectTest : BootstrapBlazorTestBase
});
});
cut.InvokeAsync(() =>
{
var input = cut.Find(".form-select");
Assert.Equal("Test 3", input.GetAttribute("value"));
});
cut.Contains("Test 3");
var input = cut.Find(".form-select");
Assert.Null(input.GetAttribute("value"));
var select = cut.Instance;
Assert.Equal("3", select.Value?.Value);
Assert.Equal("3", select.Value);
cut.InvokeAsync(() =>
{
var item = cut.Find(".dropdown-item");
item.Click();
Assert.Equal("1", value.Value);
var item = cut.Find(".dropdown-item");
await cut.InvokeAsync(() => { item.Click(); });
Assert.Equal("1", value);
var input = cut.Find(".form-select");
Assert.Equal("Test1", input.GetAttribute("value"));
});
input = cut.Find(".form-select");
Assert.Equal("Test1", input.GetAttribute("value"));
}
[Fact]
public void IsVirtualize_DefaultVirtualizeItemText()
{
string? value = "3";
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.IsVirtualize, true);
pb.Add(a => a.DefaultVirtualizeItemText, "Test 3");
@@ -839,9 +811,9 @@ public class SelectTest : BootstrapBlazorTestBase
})));
pb.Add(a => a.OnQueryAsync, option =>
{
return Task.FromResult(new QueryData<SelectedItem>()
return Task.FromResult(new QueryData<SelectedItem<string>>()
{
Items = new SelectedItem[]
Items = new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -861,11 +833,11 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public void LoadItems_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.OnQueryAsync, option =>
{
return Task.FromResult(new QueryData<SelectedItem>());
return Task.FromResult(new QueryData<SelectedItem<string>>());
});
pb.Add(a => a.Value, "2");
pb.Add(a => a.IsVirtualize, true);
@@ -879,40 +851,12 @@ public class SelectTest : BootstrapBlazorTestBase
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<Select<SelectedItem>>(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<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "<div>Test1</div>"),
new("2", "<div>Test2</div>")
@@ -926,9 +870,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public async Task IsEditable_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "<div>Test1</div>"),
new("2", "<div>Test2</div>")
@@ -947,13 +891,14 @@ public class SelectTest : BootstrapBlazorTestBase
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");
});
await cut.InvokeAsync(() => { input.Change("Test3"); });
Assert.Equal("Test3", cut.Instance.Value);
Assert.True(updated);
}
@@ -966,7 +911,7 @@ public class SelectTest : BootstrapBlazorTestBase
new() { Value = new Foo() { Id = 1, Address = "Foo1" }, Text = "test1" },
new() { Value = new Foo() { Id = 2, Address = "Foo2" }, Text = "test2" }
};
var cut = Context.RenderComponent<Select<Foo>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<Foo>>(pb =>
{
pb.Add(a => a.Items, items);
pb.Add(a => a.Value, new Foo() { Id = 1, Address = "Foo1" });
@@ -989,9 +934,9 @@ public class SelectTest : BootstrapBlazorTestBase
public async Task OnClearAsync_Ok()
{
var clear = false;
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "<div>Test1</div>"),
new("2", "<div>Test2</div>")
@@ -1019,9 +964,9 @@ public class SelectTest : BootstrapBlazorTestBase
[Fact]
public async Task Toggle_Ok()
{
var cut = Context.RenderComponent<Select<string>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new SelectedItem[]
pb.Add(a => a.Items, new SelectedItem<string>[]
{
new("1", "Test1"),
new("2", "Test2")
@@ -1047,7 +992,7 @@ public class SelectTest : BootstrapBlazorTestBase
Text = "Foo2"
}
};
var cut = Context.RenderComponent<Select<Foo>>(pb =>
var cut = Context.RenderComponent<SelectGeneric<Foo>>(pb =>
{
pb.Add(a => a.Items, items);
});

View File

@@ -212,9 +212,9 @@ public class SwalTest : BootstrapBlazorTestBase
// 带确认框的 Select
cut.SetParametersAndRender(pb =>
{
pb.AddChildContent<Select<string>>(pb =>
pb.AddChildContent<SelectGeneric<string>>(pb =>
{
pb.Add(a => a.Items, new List<SelectedItem>()
pb.Add(a => a.Items, new List<SelectedItem<string>>()
{
new("1", "Test1"),
new("2", "Test2") { IsDisabled = true }
@@ -228,7 +228,7 @@ public class SwalTest : BootstrapBlazorTestBase
});
});
Task.Run(() => cut.InvokeAsync(() => cut.FindComponent<Select<string>>().Instance.ConfirmSelectedItem(0)));
Task.Run(() => cut.InvokeAsync(() => cut.FindComponent<SelectGeneric<string>>().Instance.ConfirmSelectedItem(0)));
tick = DateTime.Now;
while (!cut.Markup.Contains("test-swal-footer"))
{