Fix Virtualize table-mode scrolling in CSS Grid layouts (e.g. Aspire Dashboard)#66246
Fix Virtualize table-mode scrolling in CSS Grid layouts (e.g. Aspire Dashboard)#66246ilonatommy wants to merge 1 commit intomainfrom
Conversation
…akes it invisible regardless of scroll position -> no new callbacks called -> no data loaded.
There was a problem hiding this comment.
Pull request overview
This PR fixes Blazor Virtualize behavior in table mode when the table is styled with CSS Grid / tr { display: contents } (e.g., FluentDataGrid / Aspire Dashboard), preventing blank/placeholder-only rendering after scrolling/jumping far down the list.
Changes:
- Re-applies
display: table-rowto the spacer<tr>elements inside the IntersectionObserver callback to close a rerender race. - Adds a table-mode measurement fallback that derives
spacerSeparationfrom the first/last rendered cell bounds instead ofRange.getBoundingClientRect(). - Adds an Aspire-like QuickGrid test component plus a new E2E regression test for “thumb scroll to end should not leave a blank tail”.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/Components/Web.JS/src/Virtualize.ts |
Makes table-mode spacers resilient to rerendered style overwrites and improves item height measurement in CSS Grid table scenarios. |
src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridAspireLikeComponent.razor |
Introduces a new BasicTestApp component intended to mimic an Aspire-like large QuickGrid scenario for E2E validation. |
src/Components/test/E2ETest/Tests/VirtualizationTest.cs |
Adds an E2E regression test that scrolls a large QuickGrid to the bottom and asserts the tail isn’t blank. |
| #aspire-like-grid { | ||
| height: 476px; | ||
| overflow: auto; | ||
| } |
There was a problem hiding this comment.
The component is meant to reproduce the CSS Grid table scenario described in the PR (notably tr { display: contents; } as used by FluentDataGrid/Aspire). Currently the only styling is height/overflow on the container, so the Virtualize table-mode regression may not be exercised and the E2E could pass even without the JS fix. Consider adding Aspire-like table/grid CSS (e.g., apply display: grid to table.quickgrid and display: contents to tbody/tr, plus a simple grid-template-columns) so the test actually covers the reported layout.
| } | |
| } | |
| #aspire-like-grid table.quickgrid { | |
| display: grid; | |
| grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 120px); | |
| width: 100%; | |
| } | |
| #aspire-like-grid table.quickgrid thead, | |
| #aspire-like-grid table.quickgrid tbody, | |
| #aspire-like-grid table.quickgrid tr { | |
| display: contents; | |
| } | |
| #aspire-like-grid table.quickgrid th, | |
| #aspire-like-grid table.quickgrid td { | |
| min-width: 0; | |
| } |
| "var c = arguments[0];" + | ||
| "var spacers = c.querySelectorAll('[aria-hidden]');" + | ||
| "var rows = c.querySelectorAll('tbody tr:not([aria-hidden])');" + | ||
| "var firstId = -1;" + |
There was a problem hiding this comment.
The JS in this test uses c.querySelectorAll('[aria-hidden]') to find Virtualize spacers, but that selector can match unrelated elements inside the grid (QuickGrid uses aria-hidden in other places). This can make spacerBefore/spacerAfter measurements unreliable. Restrict the selector to the expected spacer rows, e.g. tbody tr[aria-hidden] (and ideally assert there are exactly 2).
|
Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime. |
Problem
When Virtualize is used inside a
<table>with CSS Grid layout (as FluentDataGrid / Aspire Dashboard does), scrolling past the initial viewport renders a completely blank grid. Thumb-dragging the scrollbar to the middle or bottom shows no content at all.Root cause:
FluentDataGridappliestr { display: contents }so that<td>cells participate directly in the CSS Grid. The spacer<tr>elements get this too. The JSinit()setsspacerAfter.style.display = 'table-row'to give the spacer a real box, but Blazor's next re-render overwrites the entire style attribute, stripping the display override. If anIntersectionObservercallback fires beforerefreshObservedElementsre-applies it, the spacer hasoffsetHeight === 0and the OnSpacerAfterVisible callback is skipped — so the virtual window never shifts and no new items load.spacerSeparationvalue (used to derive_itemSize) is measured viaRange.getBoundingClientRect().heightbetween the two spacer elements. In CSS Grid tables where<tr>hasdisplay: contents, the Range endpoints resolve to slightly wrong positions, producing a value ~1.7% shorter than the actual sum of cell heights (e.g. 755px instead of 768px for 24 × 32px rows). This underestimate compounds across large item counts (1185 × 31.46 ≈ 37,280 vs actual 37,920), causingitemsBeforeto oscillate on every render and trapping the component in a placeholder-only loop after a thumb jump.Fix
Two targeted changes in Virtualize.ts, table-mode (isTable) only. No C# changes, no API changes, no new DOM structure.
Re-apply display: table-row in processIntersectionEntries before any offsetHeight checks, closing the race window between Blazor's style overwrite and refreshObservedElements.
Cell-based measurement fallback. When
isTable, measurespacerSeparationfrom the first rendered cell's top to the last rendered cell's bottom, instead of relying on Range endpoints that misalign in CSS Grid layouts. This produces the exact item height and eliminates the redistribution oscillation.Testing
QuickGrid_AspireLikeThumbScrollToEnd_DoesNotLeaveBlankTail: 10,000-row QuickGrid withItemSize=32andMaxItemCount=10000, scrolls to bottom and verifies rows 9950–10000 are rendered with no blank tail.