Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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 @@ -79,7 +79,9 @@
<tr aria-rowindex="@(placeholderContext.Index + 1)">
@foreach (var col in _columns)
{
<td class="grid-cell-placeholder @ColumnClass(col)" @key="@col">@{ col.RenderPlaceholderContent(__builder, placeholderContext); }</td>
var hasCustomPlaceholder = col.PlaceholderTemplate is not null;
var placeholderClass = hasCustomPlaceholder ? "" : "grid-cell-placeholder";
<td class="@placeholderClass @ColumnClass(col)" @key="@col">@{ col.RenderPlaceholderContent(__builder, placeholderContext); }</td>
Copy link

Copilot AI Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider trimming the combined class string to avoid leading whitespace when placeholderClass is empty, for example using class="@($"{placeholderClass} {ColumnClass(col)}".Trim())".

Suggested change
<td class="@placeholderClass @ColumnClass(col)" @key="@col">@{ col.RenderPlaceholderContent(__builder, placeholderContext); }</td>
<td class="@($"{placeholderClass} {ColumnClass(col)}".Trim())" @key="@col">@{ col.RenderPlaceholderContent(__builder, placeholderContext); }</td>

Copilot uses AI. Check for mistakes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This not only affects the ... but also the opacity. (See https://github.com/dotnet/aspnetcore/blob/main/src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Themes/Default.css).

I think that rather than conditionally adding/removing the class (which will make things harder for us in the future), we should keep the class and add an extra class (let's say "default" for the discussion but find a better name).

Something like

.quickgrid[theme=default] > tbody > tr > td.grid-cell-placeholder.default:after {
    content: '\2026';
    opacity: 0.75;
}

This more closely follows "BEM" (Block-Element-Modifier) and essentially still allows people to target the placeholder indistinctively if they want to.

}
</tr>
}
Expand Down
32 changes: 32 additions & 0 deletions src/Components/test/E2ETest/Tests/VirtualizationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,38 @@ public void AlwaysFillsVisibleCapacity_Async()
int GetPlaceholderCount() => Browser.FindElements(By.Id("async-placeholder")).Count;
}

[Fact]
public void PlaceholdersHaveCorrectValue_Async()
{
var component = Browser.MountTestComponent<VirtualizationQuickGrid>();

var finishLoadingButton = Browser.Exists(By.Id("finish-loading-button"));
var startLoadingButton = Browser.Exists(By.Id("start-loading-button"));
//Load the initial data.
finishLoadingButton.Click();

Browser.True(() => GetItemCount() > 0);
Browser.Equal(0, () => GetPlaceholderCount());

//Start loading the second set of data to check for placeholders.
startLoadingButton.Click();
Browser.ExecuteJavaScript("const container = document.getElementById('async-container'); container.scrollTop = container.scrollHeight * 0.5;");

Browser.Equal(0, () => GetItemCount());
Browser.True(() => GetPlaceholderCount() > 0);

Assert.Equal("\"…\"", Browser.ExecuteJavaScript<string>(@"
const p = document.querySelector('td.async-id');
return p ? getComputedStyle(p, '::after').content : null;"));
Assert.Equal("none", Browser.ExecuteJavaScript<string>(@"
const p = document.querySelector('td.async-second');
return p ? getComputedStyle(p, '::after').content : null;"));
Browser.Equal("LOADING DATA", () => Browser.Exists(By.CssSelector(".async-second .async-placeholder")).Text);

int GetItemCount() => Browser.FindElements(By.CssSelector("#async-container tbody td.async-id:not(.grid-cell-placeholder)")).Count;
int GetPlaceholderCount() => Browser.FindElements(By.CssSelector("#async-container tbody .async-id.grid-cell-placeholder")).Count;
}

[Fact]
public void RerendersWhenItemSizeShrinks_Sync()
{
Expand Down
1 change: 1 addition & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
<option value="BasicTestApp.VirtualizationMaxItemCount">Virtualization MaxItemCount</option>
<option value="BasicTestApp.VirtualizationMaxItemCount_AppContext">Virtualization MaxItemCount (via AppContext)</option>
<option value="BasicTestApp.VirtualizationTable">Virtualization HTML table</option>
<option value="BasicTestApp.VirtualizationQuickGrid">Virtualization QuickGrid component</option>
<option value="BasicTestApp.HotReload.RenderOnHotReload">Render on hot reload</option>
<option value="BasicTestApp.SectionsTest.ParentComponentWithTwoChildren">Sections test</option>
<option value="BasicTestApp.SectionsTest.SectionsWithCascadingParameters">Sections with Cascading parameters test</option>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
@using Microsoft.AspNetCore.Components.QuickGrid


@if (RendererInfo.IsInteractive)
{
<button id="finish-loading-button" @onclick="@(() => FinishLoading(200))">Finish loading</button>
<button id="start-loading-button" @onclick="@(() => StartNewAsyncLoad())">Start new load</button>

<br/>

<div id="async-container" style="height: 500px; overflow-y: auto;">
<QuickGrid @ref="asyncGrid" TGridItem="DataItem" ItemsProvider="GetItemsAsync" Virtualize="true" ItemSize="25">
<PropertyColumn Property="@(p => p.Id)" class="async-id">
</PropertyColumn>
<PropertyColumn Property="@(p => p.SecondNum)" class="async-second">
<PlaceholderTemplate>
<strong class="async-placeholder">LOADING DATA</strong>
</PlaceholderTemplate>
</PropertyColumn>
</QuickGrid>
</div>



}

@code {
record DataItem(int Id, int SecondNum);
QuickGrid<DataItem> asyncGrid;

int asyncTotalItemCount = 200;
int asyncCancellationCount = 0;

TaskCompletionSource asyncTcs = new TaskCompletionSource();

private async ValueTask<GridItemsProviderResult<DataItem>> GetItemsAsync(GridItemsProviderRequest<DataItem> request)
{
var loadingTask = asyncTcs.Task;
var registration = request.CancellationToken.Register(() => CancelLoading(request.CancellationToken));

try
{
await loadingTask.WaitAsync(request.CancellationToken);

var items = Enumerable.Range(request.StartIndex, request.Count ?? 200)
.Select(i => new DataItem(i, i * 2))
.ToArray();

return GridItemsProviderResult.From(items, asyncTotalItemCount);
}
catch (OperationCanceledException)
{
throw;
}
finally
{
registration.Dispose();
}
}


void StartNewAsyncLoad()
{
asyncTcs = new TaskCompletionSource();
StateHasChanged();
}

void FinishLoading(int totalItemCount)
{
asyncTotalItemCount = totalItemCount;
asyncTcs.SetResult();
StateHasChanged();
}

void CancelLoading(System.Threading.CancellationToken cancellationToken)
{
asyncTcs.TrySetCanceled(cancellationToken);
asyncTcs = new TaskCompletionSource();
asyncCancellationCount++;
StateHasChanged();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
@using Microsoft.AspNetCore.Components.Forms

@if(RendererInfo.IsInteractive) {
<p id="is-interactive"></p>
<p id="is-interactive">111</p>
}

@if (_invalid)
Expand Down
Loading