fix(module select): simulate setTimeout, ensure that the click event is processed properly (#3525)

* fix(module select): dom symmetrical difference event

* refactor: selector

* fix: test

fix: bunit cannot handle js event

---------

Co-authored-by: James Yeung <shunjiey@hotmail.com>
This commit is contained in:
zxyao 2023-12-23 11:23:16 +08:00 committed by GitHub
parent 517fee6316
commit fd73cc980f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 295 additions and 615 deletions

View File

@ -423,7 +423,7 @@ namespace AntDesign.Internal
/// </summary>
/// <param name="args">MouseEventArgs</param>
/// <returns></returns>
public virtual async Task OnClickDiv(MouseEventArgs args)
protected virtual async Task OnClickDiv(MouseEventArgs args)
{
if (!IsButton)
{
@ -479,6 +479,10 @@ namespace AntDesign.Internal
}
}
/// <summary>
/// on document mouse up
/// </summary>
/// <param name="element"></param>
protected virtual void OnMouseUp(JsonElement element)
{
if (_mouseUpInOverlay)
@ -491,10 +495,10 @@ namespace AntDesign.Internal
{
if (OnMaskClick.HasDelegate)
{
OnMaskClick.InvokeAsync(null);
_ = OnMaskClick.InvokeAsync(null);
}
Hide();
_ = Hide();
}
}

View File

@ -38,7 +38,7 @@ namespace AntDesign.JsInterop
return new DomEventKey(selector, eventName, _id);
}
public void AddExclusive<T>(object dom, string eventName, Action<T> callback, bool preventDefault = false)
public void AddExclusive<T>(object dom, string eventName, Action<T> callback, bool preventDefault = false, bool stopPropagation = false)
{
var key = FormatKey(dom, eventName);
if (_exclusiveDotNetObjectStore.ContainsKey(key))
@ -48,7 +48,7 @@ namespace AntDesign.JsInterop
{
callback(p);
}));
_jsRuntime.InvokeAsync<string>(JSInteropConstants.AddDomEventListener, dom, eventName, preventDefault, dotNetObject);
_jsRuntime.InvokeAsync<string>(JSInteropConstants.AddDomEventListener, dom, eventName, preventDefault, dotNetObject, stopPropagation);
_exclusiveDotNetObjectStore.Add(key, dotNetObject);
}

View File

@ -13,7 +13,7 @@ namespace AntDesign.JsInterop
void AddEventListenerToFirstChild<T>(object dom, string eventName, Action<T> callback, bool preventDefault = false);
void AddExclusive<T>(object dom, string eventName, Action<T> callback, bool preventDefault = false);
void AddExclusive<T>(object dom, string eventName, Action<T> callback, bool preventDefault = false, bool stopPropagation = false);
ValueTask AddResizeObserver(ElementReference dom, Action<List<ResizeObserverEntry>> callback);

View File

@ -10,7 +10,7 @@ export class eventHelper {
return element.dispatchEvent(evt);
}
static addDomEventListener(element, eventName: string, preventDefault: boolean, invoker: any) {
static addDomEventListener(element, eventName: string, preventDefault: boolean, invoker: any, stopPropagation: boolean = false) {
const callback = args => {
const obj = {};
for (let k in args) {
@ -28,6 +28,9 @@ export class eventHelper {
if (preventDefault === true) {
args.preventDefault();
}
if (stopPropagation) {
args.stopPropagation();
}
};
const dom = domInfoHelper.get(element);

View File

@ -110,7 +110,7 @@ namespace AntDesign
/// </summary>
/// <param name="args">MouseEventArgs</param>
/// <returns></returns>
public override async Task OnClickDiv(MouseEventArgs args)
protected override async Task OnClickDiv(MouseEventArgs args)
{
if (!IsButton)
{

View File

@ -7,7 +7,6 @@
<CascadingValue Value="this" IsFixed="@(!IsGroupingEnabled)">
<CascadingValue Value=@("ant-select-dropdown") Name="PrefixCls" IsFixed>
<div class="@ClassMapper.Class" style="@Style" id="@Id" tabindex="-1" @ref="Ref">
<OverlayTrigger @ref="@_dropDown"
Visible="Open"
Disabled="Disabled"
@ -106,15 +105,15 @@
<CascadingValue Value="@MaxTagPlaceholder" Name="ParentMaxTagPlaceholerTemplate">
<CascadingValue Value="@ShowSearchIcon" Name="ShowSearchIcon">
<CascadingValue Value="@ShowArrowIcon" Name="ShowArrowIcon">
<SelectContent Prefix="@ClassPrefix"
<SelectContent Prefix="@ClassPrefix"
RefBack="@context"
@ref="_selectContent"
TItemValue="TItemValue"
TItem="TItem"
SearchValue="@_searchValue"
SearchDebounceMilliseconds="@SearchDebounceMilliseconds"
SearchDebounceMilliseconds="@SearchDebounceMilliseconds"
IsOverlayShow="@_dropDown.IsOverlayShow()"
MaxTagCount="@_maxTagCountAsInt"
MaxTagCount="@_maxTagCountAsInt"
OnInput="@OnInputAsync"
OnKeyUp="@OnKeyUpAsync"
OnKeyDown="@OnKeyDownAsync"
@ -131,7 +130,6 @@
</CascadingValue>
</Unbound>
</OverlayTrigger>
</div>
</CascadingValue>
</CascadingValue>

View File

@ -791,7 +791,7 @@ namespace AntDesign
protected async Task SetDropdownStyleAsync()
{
string maxWidth = "", minWidth = "", definedWidth = "";
var domRect = await JsInvokeAsync<DomRect>(JSInteropConstants.GetBoundingClientRect, Ref);
var domRect = await JsInvokeAsync<DomRect>(JSInteropConstants.GetBoundingClientRect, _selectContent.Ref);
var width = domRect.Width.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture);
minWidth = $"min-width: {width}px;";
if (DropdownMatchSelectWidth.IsT0 && DropdownMatchSelectWidth.AsT0)

View File

@ -59,6 +59,8 @@ namespace AntDesign
internal RenderFragment FeedbackIcon => FormItem?.FeedbackIcon;
internal ClassMapper CurrentClassMapper => ClassMapper;
/// <summary>
/// Overlay adjustment strategy (when for example browser resize is happening)
/// </summary>
@ -981,7 +983,7 @@ namespace AntDesign
/// Check if Focused property is False; Set the Focused property to true, change the
/// style and set the Focus on the Input element via DOM. It also invoke the OnFocus Action.
/// </summary>
protected async Task SetInputFocusAsync()
internal async Task SetInputFocusAsync()
{
if (!Focused)
{
@ -1004,11 +1006,6 @@ namespace AntDesign
}
}
internal async Task OnArrowClick(MouseEventArgs args)
{
await _dropDown.OnClickDiv(args);
}
/// <summary>
/// Close the overlay
/// </summary>

View File

@ -1,218 +1,33 @@
@using AntDesign
@using System.Text.Json
@using AntDesign.JsInterop
@namespace AntDesign.Select.Internal
@typeparam TItemValue
@typeparam TItem
@inherits AntDomComponentBase
@if (ParentSelect.SelectMode == SelectMode.Default)
{
<div class="@Prefix-selector" @ref="@Ref" style="@(ParentSelect.PrefixIcon is null?"":"padding-left: 4px;")">
@if (ParentSelect.PrefixIcon != null)
<div class="@ParentSelect.CurrentClassMapper.Class"
style="@ParentSelect.Style"
id="@Id"
tabindex="-1"
@ref="@Ref"
>
@if (ParentSelect.SelectMode == SelectMode.Default)
{
<span class="@Prefix-prefix" unselectable="on" aria-hidden="true" style="user-select: none;display:flex;align-items: center;padding-right: 4px;">
@ParentSelect.PrefixIcon
</span>
}
<span class="@Prefix-selection-search" style="@_inputWidth">
<input @ref="ParentSelect._inputRef"
@oninput="@OnInputChange"
@onkeyup="OnKeyUp"
@onkeydown="OnKeyDown"
@attributes=@AdditonalAttributes()
@bind-value="@SearchValue"
id="@(ParentSelect.Id)_list"
type="search"
readonly="@(!ParentSelect.IsSearchEnabled)"
unselectable="@(ParentSelect.IsSearchEnabled ? false : "on")"
role="combobox"
class="@Prefix-selection-search-input"
autocomplete="off"
aria-owns="@(ParentSelect.Id)_list"
aria-expanded="@IsOverlayShow"
aria-autocomplete="list"
aria-controls="@(ParentSelect.Id)_list"
aria-haspopup="listbox"
accesskey="@ParentSelect?.AccessKey"
style="@_inputStyle" />
</span>
@if (ShowPlaceholder)
{
<span class="@Prefix-selection-placeholder">@Placeholder</span>
}
else
{
var selectedItem = ParentSelect.SelectedOptionItems.FirstOrDefault();
if (string.IsNullOrEmpty(SearchValue) && selectedItem != null)
{
@if (ParentLabelTemplate != null)
<div class="@Prefix-selector" style="@(ParentSelect.PrefixIcon is null?"":"padding-left: 4px;")">
@if (ParentSelect.PrefixIcon != null)
{
<CascadingValue Value="this" Name="SelectContent">
<CascadingValue Value="@selectedItem" Name="SelectOption">
@ParentLabelTemplate(selectedItem.Item)
</CascadingValue>
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
@getLabel(selectedItem)
</span>
}
}
}
</div>
<SelectSuffixIcon
TItem="TItem"
TItemValue="TItemValue"
IsOverlayShow="@IsOverlayShow"
OnClearClick="@OnClearClick"
ShowArrowIcon="@ShowArrowIcon"
ShowSearchIcon="@ShowArrowIcon"/>
}
else //ParentSelect.SelectMode != SelectMode.Default
{
<div class="@Prefix-selector" @ref="@Ref" style="@(ParentSelect.PrefixIcon is null?"":"padding-left: 4px;")">
<div class="@Prefix-selection-overflow" @ref="@_overflow">
@if (ParentSelect.PrefixIcon != null)
{
<div class="@Prefix-selection-overflow-item" style="opacity: 1;order:-100;">
<span @ref="@_prefixRef" class="@Prefix-prefix" unselectable="on" aria-hidden="true" style="user-select: none;display:flex;align-items: center;padding-right: 4px;min-height:28px;">
<span class="@Prefix-prefix" unselectable="on" aria-hidden="true" style="user-select: none;display:flex;align-items: center;padding-right: 4px;">
@ParentSelect.PrefixIcon
</span>
</div>
}
@if (!ShowPlaceholder)
{
var selectedItems = ParentSelect.SelectedOptionItems;
@if (ParentSelect.HasTagCount)
{
@for (int i = 0; i < Math.Min(ParentSelect.MaxTagCount.AsT0, selectedItems.Count); i++)
{
var selectedOption = selectedItems[i];
<div class="@Prefix-selection-overflow-item" style="@OverflowStyle(i)">
@if (ParentLabelTemplate != null)
{
<CascadingValue Value="this" Name="SelectContent">
<CascadingValue Value="@selectedOption" Name="SelectOption">
@ParentLabelTemplate(selectedOption.Item)
</CascadingValue>
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
<span class="@Prefix-selection-item-content">@getTagLabel(this,selectedOption)</span>
<span unselectable="on" aria-hidden="true" style="user-select: none;" class="@Prefix-selection-item-remove"
@onmousedown="(e)=> RemoveClicked(e, selectedOption)" onclick="event.stopPropagation()">
<Icon Type="close"></Icon>
</span>
</span>
}
</div>
}
@if (selectedItems.Count > ParentSelect.MaxTagCount.AsT0)
{
<div class="@Prefix-selection-overflow-item @Prefix-selection-overflow-item-rest" style="opacity: 1; order: @(selectedItems.Count-1);">
@if (ParentMaxTagPlaceholerTemplate != null)
{
<CascadingValue Value="this" Name="SelectContent">
@ParentMaxTagPlaceholerTemplate(selectedItems.Skip(ParentSelect.MaxTagCount.AsT0).Select(i => i.Item))
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
<span class="@Prefix-selection-item-content">+ @(selectedItems.Count - ParentSelect.MaxTagCount.AsT0)@Ellipse</span>
</span>
}
</div>
}
}
else if (ParentSelect.IsResponsive)
{
@for (int i = 0; i < selectedItems.Count; i++)
{
var selectedOption = selectedItems[i];
<div class="@Prefix-selection-overflow-item" @key="@selectedOption.InternalId" @ref="@selectedOption.SelectedTagRef" style="@OverflowStyle(i)">
@if (ParentLabelTemplate != null)
{
<CascadingValue Value="this" Name="SelectContent">
<CascadingValue Value="@selectedOption" Name="SelectOption">
@ParentLabelTemplate(selectedOption.Item)
</CascadingValue>
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
<span class="@Prefix-selection-item-content">@getTagLabel(this,selectedOption)</span>
<span unselectable="on" aria-hidden="true" style="user-select: none;" class="@Prefix-selection-item-remove"
@onmousedown="(e)=> RemoveClicked(e, selectedOption)" onclick="event.stopPropagation()">
<Icon Type="close"></Icon>
</span>
</span>
}
</div>
}
@if (selectedItems.Count > _calculatedMaxCount)
{
<div class="@Prefix-selection-overflow-item @Prefix-selection-overflow-item-rest" @key="@_internalId" @ref="@_aggregateTag" style="opacity 1; order: @(_calculatedMaxCount-1);">
@if (ParentMaxTagPlaceholerTemplate != null)
{
<CascadingValue Value="this" Name="SelectContent">
@ParentMaxTagPlaceholerTemplate(selectedItems.Skip(_calculatedMaxCount).Select(i => i.Item))
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
<span class="@Prefix-selection-item-content">+ @(selectedItems.Count-_calculatedMaxCount)@Ellipse</span>
</span>
}
</div>
}
}
else
{
string firstAfterPrefix = $"max-width: {GetFirstItemMaxWidth()}%;";
@foreach (var selectedOption in selectedItems)
{
<div class="@Prefix-selection-overflow-item" style="opacity: 1;@firstAfterPrefix">
@if (ParentLabelTemplate != null)
{
<CascadingValue Value="this" Name="SelectContent">
<CascadingValue Value="@selectedOption" Name="SelectOption">
@ParentLabelTemplate(selectedOption.Item)
</CascadingValue>
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
<span class="@Prefix-selection-item-content">@getTagLabel(this,selectedOption)</span>
<span unselectable="on" aria-hidden="true" style="user-select: none;" class="@Prefix-selection-item-remove"
@onmousedown="(e)=> RemoveClicked(e, selectedOption)" onclick="event.stopPropagation()">
<Icon Type="close"></Icon>
</span>
</span>
}
</div>
firstAfterPrefix = "max-width: 98%;";
}
}
}
<div class="@Prefix-selection-overflow-item @Prefix-selection-overflow-item-suffix" style="opacity: 1; order: @(ParentSelect.IsResponsive?_calculatedMaxCount-1:ParentSelect.SelectedOptionItems.Count)">
<div class="@Prefix-selection-search" style="@_inputWidth">
<span class="@Prefix-selection-search" style="@_inputWidth">
<input @ref="ParentSelect._inputRef"
@oninput="OnInputChange"
@oninput="@OnInputChange"
@onkeyup="OnKeyUp"
@onkeydown="OnKeyDown"
@attributes=@AdditonalAttributes()
@bind-value="@SearchValue"
id="@ParentSelect.Id"
id="@(ParentSelect.Id)_list"
type="search"
readonly="@(!ParentSelect.IsSearchEnabled)"
unselectable="@(ParentSelect.IsSearchEnabled ? false : "on")"
@ -224,25 +39,218 @@ else //ParentSelect.SelectMode != SelectMode.Default
aria-autocomplete="list"
aria-controls="@(ParentSelect.Id)_list"
aria-haspopup="listbox"
accesskey="@ParentSelect?.AccessKey"
style="@_inputStyle" />
<span class="@Prefix-selection-search-mirror" aria-hidden="true">&nbsp;</span>
</span>
@if (ShowPlaceholder)
{
<span class="@Prefix-selection-placeholder">@Placeholder</span>
}
else
{
var selectedItem = ParentSelect.SelectedOptionItems.FirstOrDefault();
if (string.IsNullOrEmpty(SearchValue) && selectedItem != null)
{
@if (ParentLabelTemplate != null)
{
<CascadingValue Value="this" Name="SelectContent">
<CascadingValue Value="@selectedItem" Name="SelectOption">
@ParentLabelTemplate(selectedItem.Item)
</CascadingValue>
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
@getLabel(selectedItem)
</span>
}
}
}
</div>
<SelectSuffixIcon
TItem="TItem"
TItemValue="TItemValue"
IsOverlayShow="@IsOverlayShow"
OnClearClick="@OnClearClick"
ShowArrowIcon="@ShowArrowIcon"
ShowSearchIcon="@ShowArrowIcon"/>
}
else //ParentSelect.SelectMode != SelectMode.Default
{
<div class="@Prefix-selector" style="@(ParentSelect.PrefixIcon is null?"":"padding-left: 4px;")">
<div class="@Prefix-selection-overflow" @ref="@_overflow">
@if (ParentSelect.PrefixIcon != null)
{
<div class="@Prefix-selection-overflow-item" style="opacity: 1;order:-100;">
<span @ref="@_prefixRef" class="@Prefix-prefix" unselectable="on" aria-hidden="true" style="user-select: none;display:flex;align-items: center;padding-right: 4px;min-height:28px;">
@ParentSelect.PrefixIcon
</span>
</div>
}
@if (!ShowPlaceholder)
{
var selectedItems = ParentSelect.SelectedOptionItems;
@if (ParentSelect.HasTagCount)
{
@for (int i = 0; i < Math.Min(ParentSelect.MaxTagCount.AsT0, selectedItems.Count); i++)
{
var selectedOption = selectedItems[i];
<div class="@Prefix-selection-overflow-item" style="@OverflowStyle(i)">
@if (ParentLabelTemplate != null)
{
<CascadingValue Value="this" Name="SelectContent">
<CascadingValue Value="@selectedOption" Name="SelectOption">
@ParentLabelTemplate(selectedOption.Item)
</CascadingValue>
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
<span class="@Prefix-selection-item-content">@getTagLabel(this,selectedOption)</span>
<span unselectable="on" aria-hidden="true" style="user-select: none;" class="@Prefix-selection-item-remove"
@onmousedown="(e)=> RemoveClicked(e, selectedOption)" onclick="event.stopPropagation()">
<Icon Type="close"></Icon>
</span>
</span>
}
</div>
}
@if (selectedItems.Count > ParentSelect.MaxTagCount.AsT0)
{
<div class="@Prefix-selection-overflow-item @Prefix-selection-overflow-item-rest" style="opacity: 1; order: @(selectedItems.Count-1);">
@if (ParentMaxTagPlaceholerTemplate != null)
{
<CascadingValue Value="this" Name="SelectContent">
@ParentMaxTagPlaceholerTemplate(selectedItems.Skip(ParentSelect.MaxTagCount.AsT0).Select(i => i.Item))
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
<span class="@Prefix-selection-item-content">+ @(selectedItems.Count - ParentSelect.MaxTagCount.AsT0)@Ellipse</span>
</span>
}
</div>
}
}
else if (ParentSelect.IsResponsive)
{
@for (int i = 0; i < selectedItems.Count; i++)
{
var selectedOption = selectedItems[i];
<div class="@Prefix-selection-overflow-item" @key="@selectedOption.InternalId" @ref="@selectedOption.SelectedTagRef" style="@OverflowStyle(i)">
@if (ParentLabelTemplate != null)
{
<CascadingValue Value="this" Name="SelectContent">
<CascadingValue Value="@selectedOption" Name="SelectOption">
@ParentLabelTemplate(selectedOption.Item)
</CascadingValue>
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
<span class="@Prefix-selection-item-content">@getTagLabel(this,selectedOption)</span>
<span unselectable="on" aria-hidden="true" style="user-select: none;" class="@Prefix-selection-item-remove"
@onmousedown="(e)=> RemoveClicked(e, selectedOption)" onclick="event.stopPropagation()">
<Icon Type="close"></Icon>
</span>
</span>
}
</div>
}
@if (selectedItems.Count > _calculatedMaxCount)
{
<div class="@Prefix-selection-overflow-item @Prefix-selection-overflow-item-rest" @key="@_internalId" @ref="@_aggregateTag" style="opacity 1; order: @(_calculatedMaxCount-1);">
@if (ParentMaxTagPlaceholerTemplate != null)
{
<CascadingValue Value="this" Name="SelectContent">
@ParentMaxTagPlaceholerTemplate(selectedItems.Skip(_calculatedMaxCount).Select(i => i.Item))
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
<span class="@Prefix-selection-item-content">+ @(selectedItems.Count-_calculatedMaxCount)@Ellipse</span>
</span>
}
</div>
}
}
else
{
string firstAfterPrefix = $"max-width: {GetFirstItemMaxWidth()}%;";
@foreach (var selectedOption in selectedItems)
{
<div class="@Prefix-selection-overflow-item" style="opacity: 1;@firstAfterPrefix">
@if (ParentLabelTemplate != null)
{
<CascadingValue Value="this" Name="SelectContent">
<CascadingValue Value="@selectedOption" Name="SelectOption">
@ParentLabelTemplate(selectedOption.Item)
</CascadingValue>
</CascadingValue>
}
else
{
<span class="@Prefix-selection-item">
<span class="@Prefix-selection-item-content">@getTagLabel(this,selectedOption)</span>
<span unselectable="on" aria-hidden="true" style="user-select: none;" class="@Prefix-selection-item-remove"
@onmousedown="(e)=> RemoveClicked(e, selectedOption)" onclick="event.stopPropagation()">
<Icon Type="close"></Icon>
</span>
</span>
}
</div>
firstAfterPrefix = "max-width: 98%;";
}
}
}
<div class="@Prefix-selection-overflow-item @Prefix-selection-overflow-item-suffix" style="opacity: 1; order: @(ParentSelect.IsResponsive?_calculatedMaxCount-1:ParentSelect.SelectedOptionItems.Count)">
<div class="@Prefix-selection-search" style="@_inputWidth">
<input @ref="ParentSelect._inputRef"
@oninput="OnInputChange"
@onkeyup="OnKeyUp"
@onkeydown="OnKeyDown"
@attributes=@AdditonalAttributes()
@bind-value="@SearchValue"
id="@ParentSelect.Id"
type="search"
readonly="@(!ParentSelect.IsSearchEnabled)"
unselectable="@(ParentSelect.IsSearchEnabled ? false : "on")"
role="combobox"
class="@Prefix-selection-search-input"
autocomplete="off"
aria-owns="@(ParentSelect.Id)_list"
aria-expanded="@IsOverlayShow"
aria-autocomplete="list"
aria-controls="@(ParentSelect.Id)_list"
aria-haspopup="listbox"
style="@_inputStyle" />
<span class="@Prefix-selection-search-mirror" aria-hidden="true">&nbsp;</span>
</div>
</div>
@if (ShowPlaceholder)
{
<span class="@Prefix-selection-placeholder" style="@(ParentSelect.PrefixIcon != null?"margin-left: 7px;":"")">@Placeholder</span>
}
</div>
</div>
@if (ShowPlaceholder)
{
<span class="@Prefix-selection-placeholder" style="@(ParentSelect.PrefixIcon != null?"margin-left: 7px;":"")">@Placeholder</span>
}
</div>
</div>
<SelectSuffixIcon
TItem="TItem"
TItemValue="TItemValue"
IsOverlayShow="@IsOverlayShow"
OnClearClick="@OnClearClick"
ShowArrowIcon="@ShowArrowIcon"
ShowSearchIcon="@ShowArrowIcon"/>
}
<SelectSuffixIcon
TItem="TItem"
TItemValue="TItemValue"
IsOverlayShow="@IsOverlayShow"
OnClearClick="@OnClearClick"
ShowArrowIcon="@ShowArrowIcon"
ShowSearchIcon="@ShowArrowIcon"/>
}
</div>
@code {
RenderFragment getLabel(SelectOptionItem<TItemValue, TItem> option) =>

View File

@ -423,11 +423,6 @@ namespace AntDesign.Select.Internal
}
}
private async Task OnClearClickAsync(MouseEventArgs args)
{
await OnClearClick.InvokeAsync(args);
}
private async Task RemoveClicked(MouseEventArgs e, SelectOptionItem<TItemValue, TItem> selectedOption)
{
if (e.Button == 0)

View File

@ -4,7 +4,7 @@
@if (ParentSelect.SuffixIcon != null)
{
<span class="ant-select-arrow" unselectable="on" aria-hidden="true" style="user-select: none;" @onclick="@ParentSelect.OnArrowClick" @onclick:stopPropagation="true">
<span class="ant-select-arrow" unselectable="on" aria-hidden="true" style="user-select: none;" >
@ParentSelect.SuffixIcon
</span>
}
@ -18,7 +18,7 @@ else
{
if (ShowArrowIcon || ParentSelect.FeedbackIcon != null)
{
<span class="ant-select-arrow" unselectable="on" aria-hidden="true" style="user-select: none;" @onclick="@ParentSelect.OnArrowClick" @onclick:stopPropagation="true">
<span class="ant-select-arrow" unselectable="on" aria-hidden="true" style="user-select: none;" >
@if (ParentSelect.IsSearchEnabled && IsOverlayShow)
{
if (ShowSearchIcon)
@ -39,7 +39,7 @@ else
}
else
{
<span class="ant-select-arrow" unselectable="on" aria-hidden="true" style="user-select: none;" @onclick="@ParentSelect.OnArrowClick" @onclick:stopPropagation="true">
<span class="ant-select-arrow" unselectable="on" aria-hidden="true" style="user-select: none;" >
@if (ParentSelect.IsSearchEnabled && IsOverlayShow)
{
if (ShowSearchIcon)
@ -52,7 +52,11 @@ else
@if (!ParentSelect.Disabled && ParentSelect.AllowClear && ParentSelect.HasValue)
{
<span class="ant-select-clear" unselectable="on" aria-hidden="true" style="user-select: none;" @onclick="OnClearClick" @onclick:stopPropagation="true">
<span class="ant-select-clear"
unselectable="on"
aria-hidden="true"
style="user-select: none;"
@ref="@_clearRef">
<Icon Type="close-circle" Theme="fill"></Icon>
</span>
}

View File

@ -1,4 +1,8 @@
using Microsoft.AspNetCore.Components;
using System;
using System.Text.Json;
using System.Threading.Tasks;
using AntDesign.JsInterop;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace AntDesign.Select.Internal
@ -20,5 +24,40 @@ namespace AntDesign.Select.Internal
public EventCallback<MouseEventArgs> OnClearClick { get; set; }
[CascadingParameter(Name = "ParentSelect")] internal SelectBase<TItemValue, TItem> ParentSelect { get; set; }
[Inject] private IDomEventListener DomEventListener { get; set; }
private string _id = "";
private ElementReference _clearRef;
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_id = _clearRef.Id;
if (!ParentSelect.Disabled && ParentSelect.AllowClear && ParentSelect.HasValue)
{
DomEventListener.AddExclusive<JsonElement>(_clearRef, "click", OnClear, true, true);
}
}
if (_clearRef.Id != _id)
{
_id = _clearRef.Id;
if (!ParentSelect.Disabled && ParentSelect.AllowClear && ParentSelect.HasValue)
{
DomEventListener.AddExclusive<JsonElement>(_clearRef, "click", OnClear, true, true);
}
}
return base.OnAfterRenderAsync(firstRender);
}
private async void OnClear(JsonElement jsonElement)
{
if (OnClearClick.HasDelegate)
{
await OnClearClick.InvokeAsync(null);
}
}
}
}

View File

@ -21,7 +21,7 @@ namespace AntDesign.Tests
return;
}
public void AddExclusive<T>(object dom, string eventName, Action<T> callback, bool preventDefault = false)
public void AddExclusive<T>(object dom, string eventName, Action<T> callback, bool preventDefault = false, bool stopPropagation = false)
{
return;
}

View File

@ -1,138 +0,0 @@
@inherits AntDesignTestBase
@code {
record Person(int Id, string Name);
List<Person> _persons = new()
{
new(1, "John"),
new(2, "Lucy"),
new(3, "Jack"),
new(4, "Emily"),
};
[Fact]
public void DefaultSelectWithoutAllowClear()
{
JSInterop.Setup<DomRect>(JSInteropConstants.GetBoundingClientRect, _ => true)
.SetResult(new DomRect());
var cut = Render<Select<int, Person>>(
@<AntDesign.Select DataSource="@_persons"
LabelName="@nameof(Person.Name)"
ValueName="@nameof(Person.Id)"
Value="0">
</AntDesign.Select>);
cut.Invoking(c => c.Find("span.ant-select-clear")).Should().Throw<ElementNotFoundException>();
}
[Fact]
public void DefaultSelectWithAllowClear()
{
JSInterop.Setup<DomRect>(JSInteropConstants.GetBoundingClientRect, _ => true)
.SetResult(new DomRect());
int? value = 1;
var cut = Render<Select<int?, Person>>(
@<AntDesign.Select
DataSource="@_persons"
ValueName="@nameof(Person.Id)"
LabelName="@nameof(Person.Name)"
@bind-Value="@value"
AllowClear/>
);
var clear = cut.Find("span.ant-select-clear");
clear.Click();
value.Should().Be(default(int?));
}
[Fact]
public void DefaultNoSelectWithAllowClear()
{
JSInterop.Setup<DomRect>(JSInteropConstants.GetBoundingClientRect, _ => true)
.SetResult(new DomRect());
int? value = 5;
var cut = Render<Select<int?, Person>>(
@<AntDesign.Select DataSource="@_persons"
ValueName="@nameof(Person.Id)"
LabelName="@nameof(Person.Name)"
@bind-Value="@value"
DefaultValue="5"
AllowClear />
);
var input = cut.Find("input");
value.Should().Be(default(int?));
input.GetAttribute("value").Should().Be("");
}
IEnumerable<string> _datasource = new List<string>{ "Lucy", "John", "Jack", "Emily" };
[Fact]
public void MultipleSelectWithoutAllowClear()
{
JSInterop.Setup<DomRect>(JSInteropConstants.GetBoundingClientRect, _ => true)
.SetResult(new DomRect());
IEnumerable<string> values = new List<string>();
var cut = Render<Select<string, string>>(
@<Select
Mode="multiple"
DataSource="@_datasource"
@bind-Values="@values">
<SelectOptions>
@foreach (var data in _datasource)
{
<SelectOption TItemValue="string" TItem="string" Value="@data" Label="@data"/>
}
</SelectOptions>
</Select>
);
cut.Invoking(c => c.Find("span.ant-select-clear")).Should().Throw<ElementNotFoundException>();
}
[Fact]
public void MultipleSelectWithAllowClearAndEmptyValues()
{
JSInterop.Setup<DomRect>(JSInteropConstants.GetBoundingClientRect, _ => true)
.SetResult(new DomRect());
IEnumerable<string> values = new List<string>();
var cut = Render<Select<string, string>>(
@<Select
Mode="multiple"
DataSource="@_datasource"
@bind-Values="@values"
AllowClear>
<SelectOptions>
@foreach (var data in _datasource)
{
<SelectOption TItemValue="string" TItem="string" Value="@data" Label="@data"/>
}
</SelectOptions>
</Select>
);
cut.Invoking(c => c.Find("span.ant-select-clear")).Should().Throw<ElementNotFoundException>();
}
[Fact]
public void MultipleSelectWithAllowClearAndNotEmptyValues()
{
JSInterop.Setup<DomRect>(JSInteropConstants.GetBoundingClientRect, _ => true)
.SetResult(new DomRect());
IEnumerable<string> values = new List<string>{ "Lucy" };
var cut = Render<Select<string, string>>(
@<Select
Mode="multiple"
DataSource="@_datasource"
@bind-Values="@values"
AllowClear>
<SelectOptions>
@foreach (var data in _datasource)
{
<SelectOption TItemValue="string" TItem="string" Value="@data" Label="@data"/>
}
</SelectOptions>
</Select>
);
cut.Render();
var clear = cut.Find("span.ant-select-clear");
clear.Click();
values.Should().BeEquivalentTo(default);
}
}

View File

@ -45,62 +45,6 @@
arrow.Should().NotBeNull();
}
[Fact] //covers scenario from issue #1989
public async Task Render_select_dropdown_on_arrow_click()
{
#if !NET6_0_OR_GREATER
#pragma warning disable CS0618 // Type or member is obsolete
JSInterop.SetupVoid(JSInteropConstants.Focus, _ => true).SetVoidResult();
#pragma warning restore CS0618 // Type or member is obsolete
#endif
JSInterop.SetupVoid(JSInteropConstants.ScrollTo, _ => true).SetVoidResult();
JSInterop.Setup<OverlayPosition>(JSInteropConstants.OverlayComponentHelper.AddOverlayToContainer, _ => true)
.SetResult(new OverlayPosition() { Top = 0, Left = 0, ZIndex = 5000, Placement = Placement.BottomLeft });
var mock = base.MockedDomEventListener;
Action<JsonElement>? onTriggerMouseUpCallback = default;
Action<JsonElement>? onTriggerClickCallback = default;
mock.Setup(s => s.AddShared<JsonElement>("document", "mouseup", It.IsAny<Action<JsonElement>>(), It.IsAny<bool>()))
.Callback((object dom, string eventName, Action<JsonElement> callback, bool preventDefault) =>
{
if (callback.Target is AntDesign.Internal.OverlayTrigger)
{
onTriggerMouseUpCallback = callback;
}
});
mock.Setup(s => s.AddExclusive<JsonElement>(It.IsAny<ElementReference>(), "click", It.IsAny<Action<JsonElement>>(), It.IsAny<bool>()))
.Callback((object dom, string eventName, Action<JsonElement> callback, bool preventDefault) =>
{
if (callback.Target is AntDesign.Internal.OverlayTrigger)
{
onTriggerClickCallback = callback;
}
});
var plannedInvocation = JSInterop.Setup<string>(JSInteropConstants.AddDomEventListener, _ => true);
var cut = Render<EnumSelect<City>>(@<EnumSelect TEnum="City"></EnumSelect>);
//Act
var arrow = cut.Find("span.ant-select-arrow");
onTriggerMouseUpCallback.Should().NotBeNull();
onTriggerClickCallback.Should().NotBeNull();
await cut.InvokeAsync(() => arrow.Click());
onTriggerMouseUpCallback?.Invoke(JsonDocument.Parse("{}").RootElement);
onTriggerClickCallback?.Invoke(default);
cut.WaitForState(() => !cut.Find("div.ant-select-dropdown").ClassList.Contains("ant-select-dropdown-hidden"));
//This is probably not the best solution. However this test is awkward - we are testing
//for something that should not appear. In failing scenarios it appears after some
//time (due to multiple async/await and re-renderings). That actually points to
//a probable wrong design in the way the OverlayTrigger & Overlay are functioning.
//So here we wait and hope the delay is going be long enough for the component
//to finish all its renderings.
await Task.Delay(500);
//Assert
var dropdown = cut.Find("div.ant-select-dropdown");
dropdown.ClassList.Should().NotContain("ant-select-dropdown-hidden");
}
[Fact]
public void AllowClear_button_not_rendered_for_no_data()
{

View File

@ -120,92 +120,6 @@
// cut.Instance.Value.Should().Be(10);
// }
[Theory]
[MemberData(nameof(AllowClearWithoutValueOnClearTheory))]
public void AllowClear_button_behavior_without_ValueOnClear_set_with_DataSource(List<PersonNullable> dataSource,
string? initialValue, bool defaultActiveFirstOption, string? expectedValue, string? expectedLabel)
{
string? value = "-1"; //set initial value to a not-possible value
Action<string?> ValueChanged = (v) =>value = v;
var cut = Render<AntDesign.Select<string?, PersonNullable>>(
@<AntDesign.Select DataSource="@dataSource"
LabelName="@nameof(PersonNullable.Name)"
ValueName="@nameof(PersonNullable.Id)"
Value="@initialValue"
ValueChanged="@ValueChanged"
DefaultActiveFirstOption="@defaultActiveFirstOption"
AllowClear>
</AntDesign.Select>);
//Act
//normally blazor would rerender and in Select.OnParametersSet()
//would load newly set value into the SelectContent, but bUnit does
//not rerender, so it has to be forced. This could probably be fixed
//by forcing StateHasChanged on the Select component, but requires
//investigation if it won't cause multiple re-renders.
cut.Render();
cut.Find("span.ant-select-clear").Click();
if (expectedLabel == string.Empty)
{
cut.Invoking(c => c.Find("span.ant-select-selection-item"))
.Should().Throw<Bunit.ElementNotFoundException>();
}
else
{
var selectContent = cut.Find("span.ant-select-selection-item");
selectContent.TextContent.Trim().Should().Be(expectedLabel);
}
value.Should().Be(expectedValue);
cut.Instance.Value.Should().Be(expectedValue);
}
[Theory]
[MemberData(nameof(AllowClearWithoutValueOnClearTheory))]
public void AllowClear_button_behavior_without_ValueOnClear_set_with_SelectOption(List<PersonNullable> dataSource,
string? initialValue, bool defaultActiveFirstOption, string? expectedValue, string? expectedLabel)
{
string? value = "-1"; //set initial value to a not-possible value
Action<string?> ValueChanged = (v) =>value = v;
var cut = Render<AntDesign.Select<string?, string>>(
@<AntDesign.Select
TItemValue="string?"
TItem="string"
Value="@initialValue"
ValueChanged="@ValueChanged"
DefaultActiveFirstOption="@defaultActiveFirstOption"
AllowClear>
<SelectOptions>
@foreach(var item in dataSource)
{
<SelectOption TItemValue="string?" TItem="string" Value="@item.Id" Label="@item.Name" />
}
</SelectOptions>
</AntDesign.Select>);
//Act
//normally blazor would rerender and in Select.OnParametersSet()
//would load newly set value into the SelectContent, but bUnit does
//not rerender, so it has to be forced. This could probably be fixed
//by forcing StateHasChanged on the Select component, but requires
//investigation if it won't cause multiple re-renders.
cut.Render();
cut.Find("span.ant-select-clear").Click();
if (expectedLabel == string.Empty)
{
cut.Invoking(c => c.Find("span.ant-select-selection-item"))
.Should().Throw<Bunit.ElementNotFoundException>();
}
else
{
var selectContent = cut.Find("span.ant-select-selection-item");
selectContent.TextContent.Trim().Should().Be(expectedLabel);
}
value.Should().Be(expectedValue);
cut.Instance.Value.Should().Be(expectedValue);
}
public static List<PersonNullable> GetAllowClearDataSource(string? id = null, string? title = null)
{
var baseDatasource = new List<PersonNullable>()
@ -235,94 +149,6 @@
}
[Theory]
[MemberData(nameof(AllowClearWithValueOnClearTheory))]
public void AllowClear_button_behavior_with_ValueOnClear_set_with_DataSource(List<PersonNullable> dataSource,
string? initialValue, bool defaultActiveFirstOption, string? valueOnClear, string? expectedValue, string? expectedLabel)
{
string? value = "-1"; //set initial value to a not-possible value
Action<string?> ValueChanged = (v) =>value = v;
var cut = Render<AntDesign.Select<string?, PersonNullable>>(
@<AntDesign.Select DataSource="@dataSource"
LabelName="@nameof(PersonNullable.Name)"
ValueName="@nameof(PersonNullable.Id)"
Value="@initialValue"
ValueChanged="@ValueChanged"
DefaultActiveFirstOption="@defaultActiveFirstOption"
ValueOnClear="@valueOnClear"
AllowClear>
</AntDesign.Select>);
//Act
//normally blazor would rerender and in Select.OnParametersSet()
//would load newly set value into the SelectContent, but bUnit does
//not rerender, so it has to be forced. This could probably be fixed
//by forcing StateHasChanged on the Select component, but requires
//investigation if it won't cause multiple re-renders.
cut.Render();
cut.Find("span.ant-select-clear").Click();
if (expectedLabel == string.Empty)
{
cut.Invoking(c => c.Find("span.ant-select-selection-item"))
.Should().Throw<Bunit.ElementNotFoundException>();
}
else
{
var selectContent = cut.Find("span.ant-select-selection-item");
selectContent.TextContent.Trim().Should().Be(expectedLabel);
}
value.Should().Be(expectedValue);
cut.Instance.Value.Should().Be(expectedValue);
}
[Theory]
[MemberData(nameof(AllowClearWithValueOnClearTheory))]
public void AllowClear_button_behavior_with_ValueOnClear_set_with_SelectOption(List<PersonNullable> dataSource,
string? initialValue, bool defaultActiveFirstOption, string? valueOnClear, string? expectedValue, string? expectedLabel)
{
string? value = "-1"; //set initial value to a not-possible value
Action<string?> ValueChanged = (v) =>value = v;
var cut = Render<AntDesign.Select<string?, string>>(
@<AntDesign.Select
TItemValue="string?"
TItem="string"
Value="@initialValue"
ValueChanged="@ValueChanged"
DefaultActiveFirstOption="@defaultActiveFirstOption"
ValueOnClear="@valueOnClear"
AllowClear>
<SelectOptions>
@foreach(var item in dataSource)
{
<SelectOption TItemValue="string?" TItem="string" Value="@item.Id" Label="@item.Name" />
}
</SelectOptions>
</AntDesign.Select>);
//Act
//normally blazor would rerender and in Select.OnParametersSet()
//would load newly set value into the SelectContent, but bUnit does
//not rerender, so it has to be forced. This could probably be fixed
//by forcing StateHasChanged on the Select component, but requires
//investigation if it won't cause multiple re-renders.
cut.Render();
cut.Find("span.ant-select-clear").Click();
if (expectedLabel == string.Empty)
{
cut.Invoking(c => c.Find("span.ant-select-selection-item"))
.Should().Throw<Bunit.ElementNotFoundException>();
}
else
{
var selectContent = cut.Find("span.ant-select-selection-item");
selectContent.TextContent.Trim().Should().Be(expectedLabel);
}
value.Should().Be(expectedValue);
cut.Instance.Value.Should().Be(expectedValue);
}
public static IEnumerable<object[]> AllowClearWithValueOnClearTheory()
{
return new List<object[]>