!3537 refactor(#I61GRJ): redesign search box in table reuse card component

* chore: 更新单元测试
* test: 更新单元测试
* doc: 更新示例
* feat: 更新 Search 键盘响应移除 js 代码
* refactor: 更新 Table 搜索组件复用 Card 组件
* fix: 更新 card-header 样式增加 flex: 1
This commit is contained in:
Argo 2022-11-16 06:49:58 +00:00
parent 60d41b8a75
commit 2b0621f14f
9 changed files with 91 additions and 212 deletions

View File

@ -78,7 +78,7 @@
<TableColumn @bind-Field="@context.DateTime" Width="180" Filterable="true" />
<TableColumn @bind-Field="@context.Name" Searchable="true" />
<TableColumn @bind-Field="@context.Address" Searchable="true" />
<TableColumn @bind-Field="@context.Education" Searchable="true" />
<TableColumn @bind-Field="@context.Education" />
</TableColumns>
</Table>
</DemoBlock>

View File

@ -113,12 +113,16 @@ public sealed partial class TablesSearch
// 设置记录总数
var total = items.Count();
if (!string.IsNullOrEmpty(SearchModel.Name))
if (!string.IsNullOrEmpty(options.SearchText))
{
items = items.Where(i => (i.Name?.Contains(options.SearchText, StringComparison.OrdinalIgnoreCase) ?? false)
|| (i.Address?.Contains(options.SearchText, StringComparison.OrdinalIgnoreCase) ?? false));
}
else if (!string.IsNullOrEmpty(SearchModel.Name))
{
items = items.Where(i => i.Name == SearchModel.Name);
}
if (!string.IsNullOrEmpty(SearchModel.Address))
else if (!string.IsNullOrEmpty(SearchModel.Address))
{
items = items.Where(i => i.Address == SearchModel.Address);
}

View File

@ -21,12 +21,17 @@
.card-header .card-collapse-bar {
cursor: pointer;
margin-right: var(--bb-card-collapse-bar-margin-right);
flex: 1;
}
.card-header .tag {
line-height: var(--bb-card-header-tag-height);
}
.card-header + .card-body {
border-top: var(--bs-card-border-width) solid var(--bs-card-border-color);
}
.card-shadow {
box-shadow: var(--bb-card-shadow);
transition: box-shadow .3s linear;
@ -40,10 +45,6 @@
transform: rotate( 90deg);
}
.card-body {
border-top: var(--bs-card-border-width) solid var(--bs-card-border-color);
}
.is-collapsable > .card-body {
padding: 0;
}

View File

@ -8,6 +8,7 @@
--bb-table-header-icon-active-color: #409eff;
--bb-table-header-icon-hover-bg: #ddd;
--bb-table-header-icon-hover-color: #606266;
--bb-table-header-min-height: 37px;
--bb-table-footer-font-weight: blod;
--bb-table-footer-border-top: 2px solid #dee2e6;
--bb-table-loader-bg: #f8f9fa;
@ -603,63 +604,28 @@ form .table .table-cell > textarea {
}
.table-search .card-header {
padding: 0;
--bs-card-cap-padding-y: 0;
min-height: var(--bb-table-header-min-height);
}
.table-search .card-header .table-search-title {
padding: .5rem 1rem;
flex: 1;
cursor: pointer;
.table-search .card-header .table-search-buttons .btn {
padding: .275rem .5rem;
}
.table-search .card-header .table-search-buttons {
padding: .25rem 1rem;
.table-search .card-header .input-group {
width: auto;
}
.table-search .card-header .table-search-buttons .btn {
padding: .275rem .5rem;
.table-search .card-header .input-group .table-toolbar-search,
.table-search .card-header .input-group .btn {
height: calc(var(--bb-table-header-min-height) - 4px);
}
.table-search-collapse {
flex: 1;
display: flex;
align-items: center;
padding: 0 1rem;
white-space: nowrap;
}
.table-search-collapse .table-search-collapse-arrow {
cursor: pointer;
transition: .3s transform linear;
transform: rotate(0);
.table-search .card-header [aria-expanded="true"] + .input-group,
.table-search .card-header [aria-expanded="false"] + .input-group + .table-search-buttons {
display: none;
}
.table-search-collapse[aria-expanded="true"] .table-search-collapse-arrow {
transform: rotate(90deg);
}
.table-search-collapse .table-search-collapse-title {
cursor: pointer;
margin-left: .5rem;
flex: 1;
padding: .5rem 0;
}
.table-search .card-header .input-group {
width: auto;
padding: 2px;
}
.table-search .card-header .input-group .table-toolbar-search,
.table-search .card-header .input-group .btn {
height: 33px;
}
.table-search .card-header [aria-expanded="true"] + .input-group,
.table-search .card-header [aria-expanded="false"] + .input-group + .table-search-buttons {
display: none;
}
.table-excel-toolbar {
display: none;
position: absolute;

View File

@ -123,7 +123,7 @@
@if (ShowSearchTextTooltip)
{
<Tooltip Placement="Placement.Top" Title="@SearchTooltip" Sanitize="false" IsHtml="true">
<BootstrapInput class="table-toolbar-search" placeholder="@SearchPlaceholderText" @bind-Value="@SearchText" ShowLabel="false" />
<BootstrapInput class="table-toolbar-search" @onkeyup="OnSearchKeyup" placeholder="@SearchPlaceholderText" @bind-Value="@SearchText" ShowLabel="false" />
</Tooltip>
}
else
@ -834,75 +834,67 @@ RenderFragment<TItem> RenderRowExtendButtons => item =>
</td>;
RenderFragment RenderSearch =>
@<div class="card is-collapsable">
<div class="card-header">
<div class="d-flex">
<div class="table-search-collapse" data-bs-toggle="collapse" data-bs-target="#@TopSearchBodyId" aria-expanded="@TopSearchCollapsedString">
<i class="table-search-collapse-arrow fa-solid fa-circle-chevron-right"></i>
<div class="table-search-collapse-title">@SearchModalTitle</div>
</div>
@if (ShowSearchText)
{
<div class="input-group">
@if (ShowSearchTextTooltip)
{
<Tooltip Placement="Placement.Top" Title="@SearchTooltip" Sanitize="false" IsHtml="true">
<BootstrapInput class="table-toolbar-search" placeholder="@SearchPlaceholderText" @bind-Value="@SearchText">
</BootstrapInput>
</Tooltip>
}
else
{
<BootstrapInput class="table-toolbar-search" placeholder="@SearchPlaceholderText" @bind-Value="@SearchText">
@<Card IsCollapsible="true" HeaderText="@SearchModalTitle">
<HeaderTemplate>
@if (ShowSearchText)
{
<div class="input-group">
@if (ShowSearchTextTooltip)
{
<Tooltip Placement="Placement.Top" Title="@SearchTooltip" Sanitize="false" IsHtml="true">
<BootstrapInput class="table-toolbar-search" placeholder="@SearchPlaceholderText" @onkeyup="OnSearchKeyup" @bind-Value="@SearchText">
</BootstrapInput>
}
@if (ShowResetButton)
{
<Button Color="Color.Secondary" Icon="fa-solid fa-trash-can" OnClickWithoutRender="ClearSearchClick">
<span class="d-none d-sm-inline-block">@ResetSearchButtonText</span>
</Button>
}
<Button Color="Color.Secondary" Icon="fa-solid fa-magnifying-glass" OnClickWithoutRender="SearchClick">
<span class="d-none d-sm-inline-block">@SearchButtonText</span>
</Button>
</div>
}
<div class="table-search-buttons">
</Tooltip>
}
else
{
<BootstrapInput class="table-toolbar-search" placeholder="@SearchPlaceholderText" @onkeyup="OnSearchKeyup" @bind-Value="@SearchText" >
</BootstrapInput>
}
@if (ShowResetButton)
{
<Button Color="Color.Secondary" Icon="fa-solid fa-trash-can" class="btn-xs ms-2" OnClickWithoutRender="ClearSearchClick">
<Button Color="Color.Secondary" Icon="fa-solid fa-trash-can" OnClickWithoutRender="ClearSearchClick">
<span class="d-none d-sm-inline-block">@ResetSearchButtonText</span>
</Button>
}
@if (ShowSearchButton)
{
<Button Color="Color.Primary" Icon="fa-solid fa-magnifying-glass" class="btn-xs ms-2" OnClickWithoutRender="SearchClick">
<span class="d-none d-sm-inline-block">@SearchButtonText</span>
</Button>
}
<Button Color="Color.Secondary" Icon="fa-solid fa-magnifying-glass" OnClickWithoutRender="SearchClick">
<span class="d-none d-sm-inline-block">@SearchButtonText</span>
</Button>
</div>
</div>
</div>
<div class="@TopSearchBodyClassString" id="@TopSearchBodyId">
<div class="card-body-wrapper">
@if(CustomerSearchModel != null && CustomerSearchTemplate != null)
}
<div class="table-search-buttons">
@if (ShowResetButton)
{
@CustomerSearchTemplate(CustomerSearchModel)
<Button Color="Color.Secondary" Icon="fa-solid fa-trash-can" class="btn-xs ms-2" OnClickWithoutRender="ClearSearchClick">
<span class="d-none d-sm-inline-block">@ResetSearchButtonText</span>
</Button>
}
else if(SearchTemplate != null)
@if (ShowSearchButton)
{
@SearchTemplate(SearchModel)
}
else
{
<CascadingValue Value="true" IsFixed="true" Name="IsSearch">
<EditorForm Model="SearchModel" Items="GetSearchColumns()" ItemsPerRow="SearchDialogItemsPerRow" RowType="SearchDialogRowType" LabelAlign="SearchDialogLabelAlign">
</EditorForm>
</CascadingValue>
<Button Color="Color.Primary" Icon="fa-solid fa-magnifying-glass" class="btn-xs ms-2" OnClickWithoutRender="SearchClick">
<span class="d-none d-sm-inline-block">@SearchButtonText</span>
</Button>
}
</div>
</div>
</div>;
</HeaderTemplate>
<BodyTemplate>
@if(CustomerSearchModel != null && CustomerSearchTemplate != null)
{
@CustomerSearchTemplate(CustomerSearchModel)
}
else if(SearchTemplate != null)
{
@SearchTemplate(SearchModel)
}
else
{
<CascadingValue Value="true" IsFixed="true" Name="IsSearch">
<EditorForm Model="SearchModel" Items="GetSearchColumns()" ItemsPerRow="SearchDialogItemsPerRow" RowType="SearchDialogRowType" LabelAlign="SearchDialogLabelAlign">
</EditorForm>
</CascadingValue>
}
</BodyTemplate>
</Card>;
RenderFragment RenderPageInfo =>
@<div class="table-page-info">

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using Microsoft.AspNetCore.Components.Web;
namespace BootstrapBlazor.Components;
public partial class Table<TItem>
@ -120,14 +122,6 @@ public partial class Table<TItem>
[Parameter]
public Func<TItem, Task>? OnResetSearchAsync { get; set; }
private string? TopSearchBodyClassString => CssBuilder.Default("card-body collapse")
.AddClass("show", !CollapsedTopSearch)
.Build();
private string TopSearchBodyId => $"{Id}_search_body";
private string TopSearchCollapsedString => CollapsedTopSearch ? "false" : "true";
/// <summary>
/// 重置查询方法
/// </summary>
@ -270,6 +264,18 @@ public partial class Table<TItem>
/// <returns></returns>
protected List<IFilterAction> GetSearchs() => Columns.Where(col => col.Searchable).ToSearchs(SearchText);
private async Task OnSearchKeyup(KeyboardEventArgs args)
{
if (args.Key == "Enter")
{
await SearchClick();
}
else if (args.Key == "Escape")
{
await ClearSearchClick();
}
}
/// <summary>
/// 重置搜索按钮调用此方法
/// </summary>
@ -284,18 +290,4 @@ public partial class Table<TItem>
/// </summary>
/// <returns></returns>
private IEnumerable<ITableColumn> GetSearchColumns() => Columns.Where(c => c.Searchable);
/// <summary>
/// 客户端 SearchTextbox 文本框内按回车时调用此方法
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task OnSearch() => await SearchClick();
/// <summary>
/// 客户端 SearchTextbox 文本框内按 ESC 时调用此方法
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task OnClearSearch() => await ClearSearchClick();
}

File diff suppressed because one or more lines are too long

View File

@ -35,8 +35,6 @@ export class Table extends BlazorComponent {
if (this._isResizeColumn) {
this._setResizeListener()
}
this._setSearchListener()
}
_execute(args) {
@ -281,16 +279,6 @@ export class Table extends BlazorComponent {
})
}
_setSearchListener() {
EventHandler.on(this._element, 'keyup', '.table-toolbar-search', e => {
if (e.key === 'Enter') {
this._invoker.invokeMethodAsync("OnSearch");
} else if (e.key === 'Escape') {
this._invoker.invokeMethodAsync("OnClearSearch");
}
})
}
_dispose() {
if (this._fixedHeader) {
EventHandler.off(this._body, 'scroll')
@ -300,8 +288,6 @@ export class Table extends BlazorComponent {
EventHandler.off(this._element, 'keydown')
}
EventHandler.off(this._element, 'keyup', '.table-toolbar-search')
this._columns.forEach(col => {
EventHandler.off(col, 'mousedown')
EventHandler.off(col, 'touchstart')

View File

@ -370,33 +370,6 @@ public class TableTest : TableTestBase
await cut.InvokeAsync(() => searchButton.Click());
}
[Fact]
public async Task Search_JSInvoke()
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var cut = Context.RenderComponent<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<Foo>>(pb =>
{
pb.Add(a => a.ShowSearch, true);
pb.Add(a => a.SearchMode, SearchMode.Top);
pb.Add(a => a.OnQueryAsync, OnQueryAsync(localizer));
pb.Add(a => a.TableColumns, foo => builder =>
{
builder.OpenComponent<TableColumn<Foo, string>>(0);
builder.AddAttribute(1, "Field", "Name");
builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string)));
builder.AddAttribute(3, "Searchable", true);
builder.CloseComponent();
});
});
});
var table = cut.FindComponent<Table<Foo>>();
await cut.InvokeAsync(() => table.Instance.OnSearch());
await cut.InvokeAsync(() => table.Instance.OnClearSearch());
}
[Fact]
public void ShowToolbar_Ok()
{
@ -2491,41 +2464,6 @@ public class TableTest : TableTestBase
Assert.Null(table.Instance.SearchModel.Name);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CollapsedTopSearch_Ok(bool collapsed)
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var cut = Context.RenderComponent<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<Foo>>(pb =>
{
pb.Add(a => a.ShowSearch, true);
pb.Add(a => a.SearchMode, SearchMode.Top);
pb.Add(a => a.RenderMode, TableRenderMode.Table);
pb.Add(a => a.CollapsedTopSearch, collapsed);
pb.Add(a => a.OnQueryAsync, OnQueryAsync(localizer));
pb.Add(a => a.TableColumns, foo => builder =>
{
builder.OpenComponent<TableColumn<Foo, string>>(0);
builder.AddAttribute(1, "Field", "Name");
builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, "Name", typeof(string)));
builder.CloseComponent();
});
});
});
if (collapsed)
{
cut.Contains("aria-expanded=\"false\"");
}
else
{
cut.Contains("aria-expanded=\"true\"");
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]