Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading