Skip to content

Commit

Permalink
[DataGrid] Respecting control state of Loading parameter (#3064)
Browse files Browse the repository at this point in the history
* Add some basic FluentDataGrid tests to isolate bad behavior

* Add EffectiveLoadingValue, default Loading to null, refresh data if Loading is set back to null.

* Remove newly-incorrect loading param from data grid example

* Fix example, needs explicit loading state change to false

* Remove redundant initializer

---------

Co-authored-by: Adam Ratzman <[email protected]>
Co-authored-by: Vincent Baaij <[email protected]>
  • Loading branch information
3 people authored Dec 19, 2024
1 parent 94a6757 commit 5d92bcf
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2001,7 +2001,8 @@
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.Loading">
<summary>
Gets or sets a value indicating whether the grid is in a loading data state.
Gets or sets a value to indicate the grid loading data state.
If not set and a <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.ItemsProvider"/> is present, the grid will show <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.LoadingContent"/> until the provider's first return.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.LoadingContent">
Expand Down Expand Up @@ -4405,6 +4406,8 @@
</summary>
<remarks>
This method requires dynamic access to code. This code may be removed by the trimmer.
If the assembly is not yet loaded, it will be loaded by the method `Assembly.Load`.
To avoid any issues, the assembly must be loaded before calling this method (e.g. adding an emoji in your code).
</remarks>
<returns></returns>
<exception cref="T:System.ArgumentException">Raised when the <see cref="P:Microsoft.FluentUI.AspNetCore.Components.EmojiInfo.Name"/> is not found in predefined emojis.</exception>
Expand All @@ -4422,6 +4425,9 @@
<member name="P:Microsoft.FluentUI.AspNetCore.Components.EmojiExtensions.AllEmojis">
<summary />
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.EmojiExtensions.GetAssembly(System.String)">
<summary />
</member>
<member name="T:Microsoft.FluentUI.AspNetCore.Components.EmojiInfo">
<summary>
FluentUI Emoji meta-data.
Expand Down Expand Up @@ -4992,6 +4998,8 @@
<param name="icon">The <see cref="T:Microsoft.FluentUI.AspNetCore.Components.IconInfo"/> to instantiate.</param>
<remarks>
This method requires dynamic access to code. This code may be removed by the trimmer.
If the assembly is not yet loaded, it will be loaded by the method `Assembly.Load`.
To avoid any issues, the assembly must be loaded before calling this method (e.g. adding an icon in your code).
</remarks>
<returns></returns>
<exception cref="T:System.ArgumentException">Raised when the <see cref="P:Microsoft.FluentUI.AspNetCore.Components.IconInfo.Name"/> is not found in predefined icons.</exception>
Expand All @@ -5009,6 +5017,9 @@
<member name="P:Microsoft.FluentUI.AspNetCore.Components.IconsExtensions.AllIcons">
<summary />
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.IconsExtensions.GetAssembly(System.String)">
<summary />
</member>
<member name="T:Microsoft.FluentUI.AspNetCore.Components.FluentInputFile">
<summary />
</member>
Expand Down Expand Up @@ -5232,6 +5243,11 @@
Gets the content type of the current file in an upload process.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentInputFileEventArgs.LastModified">
<summary>
Gets the last modified date of the current file in an upload process.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentInputFileEventArgs.ProgressTitle">
<summary>
Gets the label to display in an upload process.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

<div style="height: 434px; overflow:auto;" tabindex="-1">
<FluentDataGrid
Loading="@(numResults == null)"
ItemsProvider="foodRecallProvider"
OnRowDoubleClick="@(()=>DemoLogger.WriteLine("Row double clicked!"))"
Virtualize="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,6 @@
await Task.Delay(1500);

items = GenerateSampleGridData(5000);
grid?.SetLoadingState(false);
}
}
2 changes: 1 addition & 1 deletion src/Core/Components/DataGrid/FluentDataGrid.razor
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
</thead>
}
<tbody>
@if (Loading)
@if (EffectiveLoadingValue)
{
@_renderLoadingContent
}
Expand Down
23 changes: 15 additions & 8 deletions src/Core/Components/DataGrid/FluentDataGrid.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,11 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
public RenderFragment? EmptyContent { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the grid is in a loading data state.
/// Gets or sets a value to indicate the grid loading data state.
/// If not set and a <see cref="ItemsProvider"/> is present, the grid will show <see cref="LoadingContent"/> until the provider's first return.
/// </summary>
[Parameter]
public bool Loading { get; set; }
public bool? Loading { get; set; }

/// <summary>
/// Gets or sets the content to render when <see cref="Loading"/> is true.
Expand Down Expand Up @@ -287,7 +288,11 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
/// Gets or sets a value indicating whether the grids' first cell should be focused.
/// </summary>
[Parameter]
public bool AutoFocus{ get; set; } = false;
public bool AutoFocus { get; set; } = false;

// Returns Loading if set (controlled). If not controlled,
// we assume the grid is loading until the next data load completes
internal bool EffectiveLoadingValue => Loading ?? ItemsProvider is not null;

private ElementReference? _gridReference;
private Virtualize<(int, TGridItem)>? _virtualizeComponent;
Expand Down Expand Up @@ -403,7 +408,7 @@ protected override Task OnParametersSetAsync()
Pagination?.ItemsPerPage != _lastRefreshedPaginationState?.ItemsPerPage
|| Pagination?.CurrentPageIndex != _lastRefreshedPaginationState?.CurrentPageIndex;

var mustRefreshData = dataSourceHasChanged || paginationStateHasChanged;
var mustRefreshData = dataSourceHasChanged || paginationStateHasChanged || Loading is null;

// We don't want to trigger the first data load until we've collected the initial set of columns,
// because they might perform some action like setting the default sort order, so it would be wasteful
Expand Down Expand Up @@ -643,7 +648,7 @@ public Task ShowColumnResizeAsync(int index)
return (index >= 0 && index < _columns.Count) ? ShowColumnResizeAsync(_columns[index]) : Task.CompletedTask;
}

public void SetLoadingState(bool loading)
public void SetLoadingState(bool? loading)
{
Loading = loading;
}
Expand Down Expand Up @@ -733,9 +738,10 @@ private async Task RefreshDataCoreAsync()
_internalGridContext.TotalViewItemCount = Pagination?.ItemsPerPage ?? providerResult.TotalItemCount;

Pagination?.SetTotalItemCountAsync(_internalGridContext.TotalItemCount);
if (_internalGridContext.TotalItemCount > 0)
if (_internalGridContext.TotalItemCount > 0 && Loading is null)
{
Loading = false;
StateHasChanged();
}

// We're supplying the row _index along with each row's data because we need it for aria-rowindex, and we have to account for
Expand All @@ -757,9 +763,10 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
if (ItemsProvider is not null)
{
var gipr = await ItemsProvider(request);
if (gipr.Items is not null)
if (gipr.Items is not null && Loading is null)
{
Loading = false;
StateHasChanged();
}
return gipr;
}
Expand Down Expand Up @@ -791,7 +798,7 @@ private string AriaSortValue(ColumnBase<TGridItem> column)
private string? StyleValue => new StyleBuilder(Style)
.AddStyle("grid-template-columns", _internalGridTemplateColumns, !string.IsNullOrWhiteSpace(_internalGridTemplateColumns))
.AddStyle("grid-template-rows", "auto 1fr", _internalGridContext.Items.Count == 0 || Items is null)
.AddStyle("height", $"calc(100% - {(int)RowSize}px)", _internalGridContext.TotalItemCount == 0 || Loading)
.AddStyle("height", $"calc(100% - {(int)RowSize}px)", _internalGridContext.TotalItemCount == 0 || EffectiveLoadingValue)
.Build();

private string? ColumnHeaderClass(ColumnBase<TGridItem> column)
Expand Down
8 changes: 4 additions & 4 deletions src/Core/Components/DataGrid/FluentDataGridCell.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ public partial class FluentDataGridCell<TGridItem> : FluentComponentBase
.Build();

protected string? StyleValue => new StyleBuilder(Style)
.AddStyle("grid-column", GridColumn.ToString(), () => (!Grid.Loading && Grid.Items is not null) || Grid.Virtualize)
.AddStyle("grid-column", GridColumn.ToString(), () => (!Grid.EffectiveLoadingValue && Grid.Items is not null) || Grid.Virtualize)
.AddStyle("text-align", "center", Column is SelectColumn<TGridItem>)
.AddStyle("align-content", "center", Column is SelectColumn<TGridItem>)
.AddStyle("padding-inline-start", "calc(((var(--design-unit)* 3) + var(--focus-stroke-width) - var(--stroke-width))* 1px)", Column is SelectColumn<TGridItem> && Owner.RowType == DataGridRowType.Default)
.AddStyle("padding-top", "calc(var(--design-unit) * 2.5px)", Column is SelectColumn<TGridItem> && (Grid.RowSize == DataGridRowSize.Medium || Owner.RowType == DataGridRowType.Header))
.AddStyle("padding-top", "calc(var(--design-unit) * 1.5px)", Column is SelectColumn<TGridItem> && Grid.RowSize == DataGridRowSize.Small && Owner.RowType == DataGridRowType.Default)
.AddStyle("height", $"{Grid.ItemSize:0}px", () => !Grid.Loading && Grid.Virtualize && Owner.RowType == DataGridRowType.Default)
.AddStyle("height", $"{(int)Grid.RowSize}px", () => !Grid.Loading && !Grid.Virtualize && Grid.Items is not null && !Grid.MultiLine)
.AddStyle("height", "100%", InternalGridContext.TotalItemCount == 0 || (Grid.Loading && Grid.Items is null) || Grid.MultiLine)
.AddStyle("height", $"{Grid.ItemSize:0}px", () => !Grid.EffectiveLoadingValue && Grid.Virtualize && Owner.RowType == DataGridRowType.Default)
.AddStyle("height", $"{(int)Grid.RowSize}px", () => !Grid.EffectiveLoadingValue && !Grid.Virtualize && Grid.Items is not null && !Grid.MultiLine)
.AddStyle("height", "100%", InternalGridContext.TotalItemCount == 0 || (Grid.EffectiveLoadingValue && Grid.Items is null) || Grid.MultiLine)
.AddStyle("min-height", "44px", Owner.RowType != DataGridRowType.Default)
.AddStyle(Owner.Style)
.Build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

<table id="xxx" class="fluent-data-grid" style="grid-template-columns: 1fr;" aria-rowcount="4" blazor:onclosecolumnoptions="1" blazor:onclosecolumnresize="2" b-ppmhrkw1mj="" blazor:elementreference="">
<thead b-ppmhrkw1mj="">
<tr data-row-index="0" role="row" blazor:onkeydown="3" blazor:onclick="4" blazor:ondblclick="5" row-type="header" b-upi3f9mbnn="">
<th col-index="1" class="column-header col-justify-start col-sort-desc" style="grid-column: 1; height: 32px; min-height: 44px;" blazor:oncontextmenu:preventdefault="" blazor:onkeydown="18" blazor:onclick="19" scope="col" aria-sort="none" b-w6qdxfylwy="">
<div class="col-title" style="width: calc(100% - 20px);" b-pxhtqoo8qd="">
<div class="col-title-text" b-pxhtqoo8qd="">Name</div>
</div>
</th>
</tr>
</thead>
<tbody b-ppmhrkw1mj="">
<tr data-row-index="2" role="row" blazor:onkeydown="9" blazor:onclick="10" blazor:ondblclick="11" aria-rowindex="2" b-upi3f9mbnn="">
<td col-index="1" class="col-justify-start" style="grid-column: 1; height: 32px;" role="gridcell" tabindex="0" blazor:onkeydown="20" blazor:onclick="21" b-w6qdxfylwy="">Denis Voituron</td>
</tr>
<tr data-row-index="3" role="row" blazor:onkeydown="12" blazor:onclick="13" blazor:ondblclick="14" aria-rowindex="3" b-upi3f9mbnn="">
<td col-index="1" class="col-justify-start" style="grid-column: 1; height: 32px;" role="gridcell" tabindex="0" blazor:onkeydown="22" blazor:onclick="23" b-w6qdxfylwy="">Vincent Baaij</td>
</tr>
<tr data-row-index="4" role="row" blazor:onkeydown="15" blazor:onclick="16" blazor:ondblclick="17" aria-rowindex="4" b-upi3f9mbnn="">
<td col-index="1" class="col-justify-start" style="grid-column: 1; height: 32px;" role="gridcell" tabindex="0" blazor:onkeydown="24" blazor:onclick="25" b-w6qdxfylwy="">Bill Gates</td>
</tr>
</tbody>
</table>
156 changes: 156 additions & 0 deletions tests/Core/DataGrid/FluentDataGridTests.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
@using FluentAssertions
@using Xunit
@inherits TestContext

@code {
public FluentDataGridTests()
{
var dataGridModule = JSInterop.SetupModule("./_content/Microsoft.FluentUI.AspNetCore.Components/Components/DataGrid/FluentDataGrid.razor.js");
dataGridModule.SetupModule("init", _ => true);

// Register services
Services.AddSingleton(LibraryConfiguration.ForUnitTests);
Services.AddScoped<IKeyCodeService>(factory => new KeyCodeService());
}

[Fact]
public void FluentDataGrid_Default()
{
// Arrange && Act
var cut = Render<FluentDataGrid<Customer>>(
@<FluentDataGrid Items="@GetCustomers().AsQueryable()">
<ChildContent>
<PropertyColumn Property="@(x => x.Name)" />
</ChildContent>
<EmptyContent><p>empty content</p></EmptyContent>
</FluentDataGrid>);

// Assert
cut.Verify();
}

[Fact]
public void FluentDataGrid_With_Empty_Items_Stays_Loading_Until_Changed()
{
// Arrange && Act
var cut = Render<FluentDataGrid<Customer>>(
@<FluentDataGrid Items="@(Array.Empty<Customer>().AsQueryable())" Loading="true">
<EmptyContent><p id="empty-content">empty content</p></EmptyContent>
<LoadingContent><p id="loading-content">loading content</p></LoadingContent>
<ChildContent>
<PropertyColumn Property="@(i => i.Name)" />
</ChildContent>
</FluentDataGrid>);

// Assert
cut.Find("#loading-content").Should().NotBeNull();
Assert.Throws<ElementNotFoundException>(() => cut.Find("#empty-content"));

cut.SetParametersAndRender(parameters => parameters
.Add(p => p.Loading, false));

Assert.Throws<ElementNotFoundException>(() => cut.Find("#loading-content"));
cut.Find("#empty-content").Should().NotBeNull();
}

[Fact]
public async Task FluentDataGrid_With_ItemProvider_Stays_Loading_Until_ChangedAsync()
{
ValueTask<GridItemsProviderResult<Customer>> GetItems(GridItemsProviderRequest<Customer> request)
{
return ValueTask.FromResult(GridItemsProviderResult.From(
Array.Empty<Customer>(),
0));
}

var cut = Render<FluentDataGrid<Customer>>(
@<FluentDataGrid TGridItem="Customer" ItemsProvider="@GetItems" Loading="true">
<EmptyContent><p id="empty-content">empty content</p></EmptyContent>
<LoadingContent><p id="loading-content">loading content</p></LoadingContent>
<ChildContent>
<TemplateColumn Title="Name">
<p class="customer-name">@context.Name</p>
</TemplateColumn>
</ChildContent>
</FluentDataGrid>);

// Assert
var dataGrid = cut.Instance;
cut.Find("#loading-content").Should().NotBeNull();

// should stay loading even after data refresh
await cut.InvokeAsync(() => dataGrid.RefreshDataAsync());
cut.Find("#loading-content").Should().NotBeNull();

// now not loading but still with 0 items, should render empty content
cut.SetParametersAndRender(parameters => parameters
.Add(p => p.Loading, false));

cut.Find("#empty-content").Should().NotBeNull();
}

[Fact]
public async Task FluentDataGrid_With_ItemProvider_And_Uncontrolled_Loading_Starts_Loading()
{
var tcs = new TaskCompletionSource();
async ValueTask<GridItemsProviderResult<Customer>> GetItems(GridItemsProviderRequest<Customer> request)
{
await tcs.Task;
var numberOfItems = 1;
return GridItemsProviderResult.From(
GetCustomers().Take(numberOfItems).ToArray(),
numberOfItems);
}

var cut = Render<FluentDataGrid<Customer>>(
@<FluentDataGrid TGridItem="Customer" ItemsProvider="@GetItems">
<EmptyContent><p id="empty-content">empty content</p></EmptyContent>
<LoadingContent><p id="loading-content">loading content</p></LoadingContent>
<ChildContent>
<TemplateColumn Title="Name">
<p class="customer-name">@context.Name</p>
</TemplateColumn>
</ChildContent>
</FluentDataGrid>);

// Assert
var dataGrid = cut.Instance;

// Data is still loading, so loading content should be displayed
cut.Find("#loading-content").Should().NotBeNull();

tcs.SetResult();

// Data is no longer loading, so loading content should not be displayed after re-render
// wait for re-render here
cut.WaitForState(() => cut.Find("p").TextContent == GetCustomers().First().Name);

Assert.Throws<ElementNotFoundException>(() => cut.Find("#loading-content"));

// should stay not loading even after data refresh
await cut.InvokeAsync(() => dataGrid.RefreshDataAsync());
Assert.Throws<ElementNotFoundException>(() => cut.Find("#loading-content"));

// if we explicitly set Loading back to null, we should see the same behaviors because data should
// be refreshed
tcs = new TaskCompletionSource();
cut.SetParametersAndRender(parameters => parameters
.Add(p => p.Loading, null));
cut.Find("#loading-content").Should().NotBeNull();

tcs.SetResult();

cut.WaitForState(() => cut.Find("p").TextContent == GetCustomers().First().Name);
Assert.Throws<ElementNotFoundException>(() => cut.Find("#loading-content"));
}

// Sample data...
private IEnumerable<Customer> GetCustomers()
{
yield return new Customer(1, "Denis Voituron");
yield return new Customer(2, "Vincent Baaij");
yield return new Customer(3, "Bill Gates");
}

private record Customer(int Id, string Name);
}
Loading

0 comments on commit 5d92bcf

Please sign in to comment.