Fix InitialItemIndex viewport underfill for small items in big container or on window resize - #67936
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a Blazor Virtualize/QuickGrid startup issue where small InitialItemIndex values could leave the viewport bottom underfilled until the user scrolls. It does this by deferring “leading overscan” (items reserved above the target) until the component has received its first spacer/measurement feedback, and adds E2E coverage to prevent regressions.
Changes:
- Add a
_hasSpacerFeedbackflag to detect when initial spacer observer feedback has occurred and use it to avoid reserving pre-target overscan during the initial unmeasured window seed. - Mark spacer feedback as received when
OnBeforeSpacerVisible/OnAfterSpacerVisiblerun, restoring the normal overscan-before-target behavior after measurement. - Add E2E tests asserting that both
QuickGridandVirtualizefill the viewport for smallInitialItemIndexvalues (including a small overscan scenario), plus minor test-asset updates to support the scenario.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/Components/Web/src/Virtualization/Virtualize.cs | Defers leading overscan until spacer feedback exists to prevent initial viewport underfill. |
| src/Components/test/testassets/BasicTestApp/VirtualizationAnchorMode.razor | Adds a UI toggle to set a small overscan to reproduce/validate the scenario. |
| src/Components/test/testassets/BasicTestApp/QuickGridTest/QuickGridScrollComponent.razor | Adjusts rendered item styling in the scroll/initial-index test component used by E2E coverage. |
| src/Components/test/E2ETest/Tests/VirtualizationTest.cs | Adds E2E assertions ensuring initial-index loads cover the viewport bottom with real items for both QuickGrid and Virtualize. |
MayaKirova
left a comment
There was a problem hiding this comment.
Not sure if this is something that needs an outsider's review, but I did spend some time looking into the Virtualization Component (partly out of curiosity because we do also have virtualization in the IgniteUI components) so decided to give it a go. :)
|
Thank you for the review. I will draft it now since this PR will wait for #68114 to get in first - this PR changes are stacked on 68114. |
fb04ab7 to
79dfd31
Compare
79dfd31 to
4b76966
Compare
4b76966 to
a59b5d4
Compare
180bfd1 to
cb511d9
Compare
I found a near-end case that still underfills on the current head ( Using the real Interactive Server browser path in Chrome (not synthetic callback injection):
The provider is called once with The trace does not show the same-batch Can we extend the fix so that when initial positioning clamps near the end and forward growth cannot fill the viewport, it expands or shifts the window backward while keeping the target visible? I think the regression coverage should include the 2,000-item/index-1990 case paired with index 1950 as a positive control, asserting that both viewport edges are covered without user scrolling and that requests terminate. It would also be worth covering both |
Thanks for iterating on this. The latest change fixes the original initial-load underfill in my exact 2,000-item / I did find a deterministic resize regression on the same path though. With fixed 50px items and a 2,500px container, the first visible item starts at 1950. After resizing the container to 2,000px and back to 2,500px, it moves to 1362 and stays there. The index-1950 control similarly moves to 1751. On the previous head, that control stayed at 1950 through both resizes. The instrumented trace shows an out-of-order rendered-content measurement:
Can we preserve the rendered-window identity with the measurement, or otherwise reject/supersede a measurement once the rendered slice has changed? I tried awaiting the measurement as a diagnostic, but tiny floating-point changes caused repeated alignment work, so I don't think that is the right production fix by itself. I think the regression assertion should verify that the initial anchor remains stable through resize smaller and larger, not only that both viewport edges are covered. It should keep the 1990 case paired with the 1950 control and cover |
That's a resize of container scenario after the requested item is correctly aligned to the top of the viewport and the alignment process finishes. I think it's out of scope for this PR and maybe in general for virtualize component. We don't promise that jump will survive the resize. The CI failure of |
|
/backport to release/11.0-rc1 |
|
Started backporting to |
Thanks, that's a useful distinction. I agree The list initially puts item 1950 at the top for The resize is exposing state that was corrupted during initial alignment:
So I'm not asking for a new promise that the exact jump target always survives resize. I'm asking that geometry from an older rendered slice not recalibrate a newer slice. That mismatch affects the component's internal size model and future virtualization behavior. I repeated the same candidate-independent browser assertion in detached worktrees. The frozen I agree the QuickGrid CI failure should be handled as a separate flake investigation. I'm not relying on that failure for this request. I see this merged while I was finishing the current-head validation, and the |
* [SignalR] Reject duplicate SignalR upload stream IDs (#68525) (#68638) * Reject duplicate SignalR upload stream IDs * Simplify upload stream ownership cleanup * Avoid upload stream ownership allocations * Simplify upload stream registration ownership * Defer upload stream reader creation * Dispose cancellation source after binding failure * Reuse upload stream test helper --------- Co-authored-by: Javier Calvarro Nelson <jacalvar@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 82e97f5a-a052-4dbe-9cf1-b62f45cf7ee2 * Honor all sign-in confirmation requirements after registration (#68631) (#68655) Co-authored-by: Brennan <brecon@microsoft.com> * Preserve BadHttpRequestException status codes (#68632) (#68649) * Preserve BadHttpRequestException status codes * Preserve exception handler 404 safeguard --------- Co-authored-by: Stephen Halter <halter73@gmail.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * SignInManager: return SignInResult.Failed for expired passkey session challenge (#67539) (#68654) Co-authored-by: Grant Totinov <granttotinov604@gmail.com> * Don't apply the CSRF verdict to remote authentication callbacks (#68669) * Don't apply the CSRF verdict to remote authentication callbacks A remote provider's callback (OIDC response_mode=form_post, WS-Federation) is a cross-site form POST by protocol design, so the auto-injected CSRF protection records an invalid IAntiforgeryValidationFeature verdict for it. The handler then throws while reading its own callback body, before any of its events can run, so apps have no way to opt out. Suppress the verdict while a remote handler owns the request, and restore it if the handler declines so the rest of the pipeline still sees it. Fixes #68666 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cb987098-3301-465b-9a3d-2e63aabf43bd * test both antiforgery & csrf --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cb987098-3301-465b-9a3d-2e63aabf43bd * Use model display names in Blazor input parsing errors (#68667) (#68688) * Use display attributes in input parsing errors * Address test coverage feedback from review. * Apply dedup cleanup from feedback. Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> * [release/11.0-rc1] Extract IsAuthenticated helper method (#68658) * Extract IsAuthenticated helper method Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> * Reorder using Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> * Use SecurityHelper for authentication revalidation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> --------- Co-authored-by: Youssef1313 <youssefvictor00@gmail.com> Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> Co-authored-by: Milos Kotlar <kotlarmilos@gmail.com> * Fix InitialItemIndex viewport underfill for small items in big container or on window resize (#67936) (#68689) Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> * fix nullable<union> for openapi gen (#68665) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Javier Calvarro Nelson <jacalvar@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Brennan <brecon@microsoft.com> Co-authored-by: Stephen Halter <halter73@gmail.com> Co-authored-by: Grant Totinov <granttotinov604@gmail.com> Co-authored-by: Korolev Dmitry <dmkorolev@microsoft.com> Co-authored-by: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Youssef1313 <youssefvictor00@gmail.com> Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> Co-authored-by: Milos Kotlar <kotlarmilos@gmail.com> Co-authored-by: William Godbe <wigodbe@microsoft.com> Copilot-Session: 82e97f5a-a052-4dbe-9cf1-b62f45cf7ee2 Copilot-Session: cb987098-3301-465b-9a3d-2e63aabf43bd
Fixes #67933
Problem
Virtualize<TItem>lets you load a list form a givenInitialItemIndexinstead of index 0. The component seeds a window of items around that index using a rough guess of item height, then relies on the browser'sIntersectionObserver/spacer callbacks to detect whether the viewport is actually filled and grow the window if not.The bug: once the initial guess placed the window near the end of the list (or items were smaller than assumed, or the container was unusually tall/resized), the "did we fill the viewport" check could be satisfied or short-circuited prematurely. The result: only a small strip of real rows rendered, with visible empty gaps above and/or below the viewport that a user scroll (not a resize/re-render) was needed to fix.
Fix
AlignToItemAsyncnow returns the post-alignment viewport state: covered, before spacer visible, or after spacer visible.Virtualize<TItem>uses that browser-measured result to grow the rendered window geometrically in the required direction while preserving target alignment.Initial-index positioning repeats this align-and-grow cycle until real content covers the viewport or no further growth is possible.
ScrollToItemAsyncperforms the same immediate viewport-growth check after scrolling, with subsequent filling handled by the existing spacer observers. Growth remains bounded by the list size andMaxItemCountplus overscan.viewport-fill.mp4