Skip to content

Commit 62ade0b

Browse files
ArgoZhang8FAway
andauthored
fix(Select): ShowSearch not work when set IsVirtualize to true (#5022)
* chore: bump version 9.2.2-beta01 Co-Authored-By: 8FAway <[email protected]> * chore: bump version 9.2.2-beta02 * refactor: 更新搜索栏 * style: 调整图标位置 * feat: 增加 Loading 图标 * doc: 代码格式化 * refactor: 增加滚动行为参数 * refactor: 增加搜索动画 * refactor: 精简代码 * chore: bump version 9.2.2 * test: 增加单元测试 --------- Co-authored-by: 8FAway <[email protected]>
1 parent f9660cb commit 62ade0b

File tree

8 files changed

+142
-60
lines changed

8 files changed

+142
-60
lines changed

src/BootstrapBlazor/BootstrapBlazor.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
4-
<Version>9.2.1</Version>
4+
<Version>9.2.2</Version>
55
</PropertyGroup>
66

77
<ItemGroup>

src/BootstrapBlazor/Components/Select/Select.razor

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
{
99
<BootstrapLabel required="@Required" for="@InputId" ShowLabelTooltip="ShowLabelTooltip" Value="@DisplayText" />
1010
}
11-
<div @attributes="AdditionalAttributes" id="@Id" class="@ClassString">
11+
<div @attributes="AdditionalAttributes" id="@Id" class="@ClassString" data-bb-scroll-behavior="@ScrollIntoViewBehaviorString">
1212
<CascadingValue Value="this" IsFixed="true">
1313
@Options
1414
</CascadingValue>
@@ -31,13 +31,11 @@
3131
<span class="@ClearClassString" @onclick="OnClearValue"><i class="@ClearIcon"></i></span>
3232
}
3333
<div class="dropdown-menu">
34-
@if (ShowSearch)
35-
{
36-
<div class="@SearchClassString">
37-
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" aria-label="Search">
38-
<i class="@SearchIconString"></i>
39-
</div>
40-
}
34+
<div class="@SearchClassString">
35+
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" aria-label="Search">
36+
<i class="@SearchIconString"></i>
37+
<i class="@SearchLoadingIconString"></i>
38+
</div>
4139
@if (IsVirtualize)
4240
{
4341
<div class="dropdown-virtual">
@@ -93,7 +91,7 @@
9391
}
9492
else if (IsMarkupString)
9593
{
96-
@((MarkupString)item.Text)
94+
@((MarkupString)item.Text)
9795
}
9896
else
9997
{

src/BootstrapBlazor/Components/Select/Select.razor.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,21 @@ public partial class Select<TValue> : ISelect, ILookup
5555
.Build();
5656

5757
private string? SearchClassString => CssBuilder.Default("search")
58+
.AddClass("show", ShowSearch)
5859
.AddClass("is-fixed", IsFixedSearch)
5960
.Build();
6061

62+
/// <summary>
63+
/// 获得 SearchLoadingIcon 图标字符串
64+
/// </summary>
65+
private string? SearchLoadingIconString => CssBuilder.Default("icon searching-icon")
66+
.AddClass(SearchLoadingIcon)
67+
.Build();
68+
6169
private readonly List<SelectedItem> _children = [];
6270

71+
private string? ScrollIntoViewBehaviorString => ScrollIntoViewBehavior == ScrollIntoViewBehavior.Smooth ? null : ScrollIntoViewBehavior.ToDescriptionString();
72+
6373
/// <summary>
6474
/// 获得/设置 右侧清除图标 默认 fa-solid fa-angle-up
6575
/// </summary>
@@ -298,7 +308,7 @@ private SelectedItem? SelectedRow
298308
{
299309
var item = Rows.Find(i => i.Value == CurrentValueAsString)
300310
?? Rows.Find(i => i.Active)
301-
?? Rows.Where(i => !i.IsDisabled).FirstOrDefault()
311+
?? Rows.FirstOrDefault(i => !i.IsDisabled)
302312
?? GetVirtualizeItem(CurrentValueAsString);
303313

304314
if (item != null)

src/BootstrapBlazor/Components/Select/Select.razor.js

Lines changed: 59 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,55 +26,51 @@ export function init(id, invoke, options) {
2626
}
2727

2828
const keydown = e => {
29-
if (popover.toggleElement.classList.contains('show')) {
30-
const items = popover.toggleMenu.querySelectorAll('.dropdown-item:not(.search, .disabled)')
31-
let activeItem = popover.toggleMenu.querySelector('.dropdown-item.preActive')
32-
if (activeItem == null) activeItem = popover.toggleMenu.querySelector('.dropdown-item.active')
33-
34-
if (activeItem) {
35-
if (items.length > 1) {
36-
activeItem.classList.remove('preActive')
37-
if (e.key === "ArrowUp") {
38-
do {
39-
activeItem = activeItem.previousElementSibling
40-
}
41-
while (activeItem && !activeItem.classList.contains('dropdown-item'))
42-
if (!activeItem) {
43-
activeItem = items[items.length - 1]
44-
}
45-
activeItem.classList.add('preActive')
46-
scrollToActive(popover.toggleMenu, activeItem)
47-
e.preventDefault()
48-
e.stopPropagation()
49-
}
50-
else if (e.key === "ArrowDown") {
51-
do {
52-
activeItem = activeItem.nextElementSibling
53-
}
54-
while (activeItem && !activeItem.classList.contains('dropdown-item'))
55-
if (!activeItem) {
56-
activeItem = items[0]
57-
}
58-
activeItem.classList.add('preActive')
59-
scrollToActive(popover.toggleMenu, activeItem)
60-
e.preventDefault()
61-
e.stopPropagation()
62-
}
29+
const menu = popover.toggleMenu;
30+
const key = e.key;
31+
if (key === "Enter" || key === 'NumpadEnter') {
32+
menu.classList.remove('show')
33+
let index = indexOf(el, activeItem)
34+
invoke.invokeMethodAsync(confirmMethodCallback, index)
35+
}
36+
else if (key === 'ArrowUp' || key === 'ArrowDown') {
37+
e.preventDefault();
38+
39+
if (menu.querySelector('.dropdown-virtual')) {
40+
return;
41+
}
42+
43+
const items = [...menu.querySelectorAll('.dropdown-item:not(.disabled)')];
44+
if (items.length > 0) {
45+
let current = menu.querySelector('.active');
46+
if (current !== null) {
47+
current.classList.remove('active');
6348
}
6449

65-
if (e.key === "Enter") {
66-
popover.toggleMenu.classList.remove('show')
67-
let index = indexOf(el, activeItem)
68-
invoke.invokeMethodAsync(confirmMethodCallback, index)
50+
let index = current === null ? -1 : items.indexOf(current);
51+
index = key === 'ArrowUp' ? index - 1 : index + 1;
52+
if (index < 0) {
53+
index = items.length - 1;
6954
}
55+
else if (index > items.length - 1) {
56+
index = 0;
57+
}
58+
items[index].classList.add('active');
59+
const top = getTop(menu, index);
60+
const hehavior = el.getAttribute('data-bb-scroll-behavior') ?? 'smooth';
61+
menu.scrollTo({ top: top, left: 0, behavior: hehavior });
7062
}
7163
}
7264
}
7365

7466
EventHandler.on(el, 'shown.bs.dropdown', shown);
7567
EventHandler.on(el, 'keydown', keydown)
7668

77-
const onSearch = debounce(v => invoke.invokeMethodAsync(searchMethodCallback, v));
69+
const onSearch = debounce(async v => {
70+
search.parentElement.classList.add('l');
71+
await invoke.invokeMethodAsync(searchMethodCallback, v);
72+
search.parentElement.classList.remove('l');
73+
});
7874
if (search) {
7975
Input.composition(search, onSearch);
8076
}
@@ -87,6 +83,19 @@ export function init(id, invoke, options) {
8783
Data.set(id, select);
8884
}
8985

86+
const getTop = (menu, index) => {
87+
const styles = getComputedStyle(menu)
88+
const maxHeight = parseInt(styles.maxHeight) / 2
89+
const itemHeight = getHeight(menu.querySelector('.dropdown-item'))
90+
const height = itemHeight * index
91+
const count = Math.floor(maxHeight / itemHeight);
92+
let top = 0;
93+
if (height > maxHeight) {
94+
top = itemHeight * (index > count ? index - count : index)
95+
}
96+
return top;
97+
}
98+
9099
export function show(id) {
91100
const select = Data.get(id)
92101
if (select) {
@@ -122,20 +131,26 @@ export function dispose(id) {
122131
}
123132

124133
function scrollToActive(el, activeItem) {
125-
if (!activeItem) {
126-
activeItem = el.querySelector('.dropdown-item.active')
127-
}
134+
const virtualEl = el.querySelector('.dropdown-virtual');
135+
136+
activeItem ??= el.querySelector('.dropdown-item.active')
128137

129138
if (activeItem) {
130139
const innerHeight = getInnerHeight(el)
131140
const itemHeight = getHeight(activeItem);
132141
const index = indexOf(el, activeItem)
133142
const margin = itemHeight * index - (innerHeight - itemHeight) / 2;
143+
const hehavior = el.getAttribute('data-bb-scroll-behavior') ?? 'smooth';
144+
145+
const search = el.querySelector('.search');
146+
if (search.classList.contains('show')) {
147+
148+
}
134149
if (margin >= 0) {
135-
el.scrollTo(0, margin);
150+
el.scrollTo({ top: margin, left: 0, behavior: hehavior });
136151
}
137152
else {
138-
el.scrollTo(0, 0);
153+
el.scrollTo({ top: margin, left: 0, behavior: hehavior });
139154
}
140155
}
141156
}

src/BootstrapBlazor/Components/Select/Select.razor.scss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,24 @@
212212
position: relative;
213213
border-block-end: var(--bs-border-width) solid var(--bb-select-search-border-color);
214214
margin-block-end: var(--bb-select-search-margin-bottom);
215+
216+
&:not(.show) {
217+
display: none;
218+
}
219+
220+
&.l {
221+
.search-icon {
222+
display: none;
223+
}
224+
225+
.searching-icon {
226+
display: block;
227+
}
228+
}
229+
230+
.searching-icon {
231+
display: none;
232+
}
215233
}
216234

217235
.dropdown-menu .search.is-fixed {

src/BootstrapBlazor/Components/Select/SelectBase.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ public abstract class SelectBase<TValue> : PopoverSelectBase<TValue>
2828
[Parameter]
2929
public string? SearchIcon { get; set; }
3030

31+
/// <summary>
32+
/// 获得/设置 设置正在搜索图标
33+
/// </summary>
34+
[Parameter]
35+
public string? SearchLoadingIcon { get; set; }
36+
3137
/// <summary>
3238
/// 获得/设置 右侧下拉箭头图标 默认 fa-solid fa-angle-up
3339
/// </summary>
@@ -42,7 +48,7 @@ public abstract class SelectBase<TValue> : PopoverSelectBase<TValue>
4248
public bool IsMarkupString { get; set; }
4349

4450
/// <summary>
45-
/// 获得/设置 字符串比较规则 默认 StringComparison.OrdinalIgnoreCase 大小写不敏感
51+
/// 获得/设置 字符串比较规则 默认 StringComparison.OrdinalIgnoreCase 大小写不敏感
4652
/// </summary>
4753
[Parameter]
4854
public StringComparison StringComparison { get; set; } = StringComparison.OrdinalIgnoreCase;
@@ -53,6 +59,12 @@ public abstract class SelectBase<TValue> : PopoverSelectBase<TValue>
5359
[Parameter]
5460
public RenderFragment<string>? GroupItemTemplate { get; set; }
5561

62+
/// <summary>
63+
/// 获得/设置 滚动行为 默认 <see cref="ScrollIntoViewBehavior.Smooth"/>
64+
/// </summary>
65+
[Parameter]
66+
public ScrollIntoViewBehavior ScrollIntoViewBehavior { get; set; } = ScrollIntoViewBehavior.Smooth;
67+
5668
/// <summary>
5769
/// 获得/设置 IIconTheme 服务实例
5870
/// </summary>
@@ -69,7 +81,7 @@ public abstract class SelectBase<TValue> : PopoverSelectBase<TValue>
6981
/// <summary>
7082
/// 获得 SearchIcon 图标字符串 默认增加 icon 样式
7183
/// </summary>
72-
protected string? SearchIconString => CssBuilder.Default("icon")
84+
protected string? SearchIconString => CssBuilder.Default("icon search-icon")
7385
.AddClass(SearchIcon)
7486
.Build();
7587

@@ -98,6 +110,7 @@ protected override void OnParametersSet()
98110
base.OnParametersSet();
99111

100112
SearchIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectSearchIcon);
113+
SearchLoadingIcon ??= IconTheme.GetIconByKey(ComponentIcons.SearchButtonLoadingIcon);
101114
}
102115

103116
/// <summary>

src/BootstrapBlazor/wwwroot/scss/theme/bootstrapblazor.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -511,8 +511,8 @@ $bb-select-search-margin-bottom: .5rem;
511511
$bb-select-search-border-color: var(--bs-border-color);
512512
$bb-select-search-padding-right: 30px;
513513
$bb-select-search-icon-color: var(--bb-select-search-border-color);
514-
$bb-select-search-icon-right: 18px;
515-
$bb-select-search-icon-top: 18px;
514+
$bb-select-search-icon-right: 20px;
515+
$bb-select-search-icon-top: 15px;
516516
$bb-select-search-height: 60px;
517517
$bb-select-append-width: 30px;
518518
$bb-select-append-color: #c0c4cc;

test/UnitTest/Components/SelectTest.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,8 +489,36 @@ public void IsFixedSearch_Ok()
489489
pb.Add(a => a.ShowSearch, true);
490490
pb.Add(a => a.IsFixedSearch, true);
491491
});
492+
Assert.Contains("search show is-fixed", cut.Markup);
493+
Assert.Contains("class=\"icon search-icon", cut.Markup);
494+
495+
cut.SetParametersAndRender(pb =>
496+
{
497+
pb.Add(a => a.ShowSearch, false);
498+
});
492499
Assert.Contains("search is-fixed", cut.Markup);
493-
Assert.Contains("class=\"icon", cut.Markup);
500+
}
501+
502+
[Fact]
503+
public void ScrollIntoViewBehavior_Ok()
504+
{
505+
var cut = Context.RenderComponent<Select<string>>(pb =>
506+
{
507+
pb.Add(a => a.Items, new SelectedItem[]
508+
{
509+
new("1", "Test1"),
510+
new("2", "Test2")
511+
});
512+
pb.Add(a => a.Value, "2");
513+
pb.Add(a => a.ScrollIntoViewBehavior, ScrollIntoViewBehavior.Auto);
514+
});
515+
Assert.Contains("data-bb-scroll-behavior=\"auto\"", cut.Markup);
516+
517+
cut.SetParametersAndRender(pb =>
518+
{
519+
pb.Add(a => a.ScrollIntoViewBehavior, ScrollIntoViewBehavior.Smooth);
520+
});
521+
Assert.DoesNotContain("data-bb-scroll-behavior", cut.Markup);
494522
}
495523

496524
[Fact]

0 commit comments

Comments
 (0)