Skip to content

Fix Virtualize End anchoring after initial provider load - #69310

Open
ilonatommy wants to merge 4 commits into
mainfrom
fix-69302
Open

ilonatommy wants to merge 4 commits into
mainfrom
fix-69302

Conversation

@ilonatommy

@ilonatommy ilonatommy commented Sep 15, 2026

Copy link
Copy Markdown
Member

Virtualize<TItem> with AnchorMode.End could remain at the beginning of the list after the initial ItemsProvider result populated an empty viewport.

Description

  • An initially empty End-mode viewport is already at the bottom - wasAtBottomLastRender should be set conditionally.
  • Apply pending spacer styles before reading scrollHeight to pin the viewport. This is required for WebAssembly render mode, where observer callbacks can run after the synchronous refresh.

For test reviewers:

The tests need to be deterministic, so using observer gate for timing the actions was necessary. Without the gate, the first IntersectionObserver callback does two things:

  1. Records that the empty viewport is at the bottom.
  2. Starts the initial provider request.

This creates a timing dependency. If the callback records the empty viewport's bottom state before the provider result is rendered, it can mask the bug and make an ordinary E2E test pass even on the unfixed implementation.

The test therefore temporarily holds Virtualize spacer observer callbacks and invokes RefreshDataAsync() explicitly. This produces the relevant initial 0 -> N provider transition before any observer callback can establish the bottom state. It then verifies that AnchorMode.End pins the populated viewport by itself.

Fixes #69302

@ilonatommy ilonatommy self-assigned this Sep 15, 2026
@ilonatommy ilonatommy added the area-blazor Includes: Blazor, Razor Components label Sep 15, 2026
@ilonatommy
ilonatommy marked this pull request as ready for review September 16, 2026 12:19
Copilot AI lite review requested due to automatic review settings September 16, 2026 12:19
@ilonatommy
ilonatommy requested a review from a team as a code owner September 16, 2026 12:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The E2E test setup must be corrected to exercise the intended transition safely and prevent callbacks from referencing deleted state.

Pull request overview

Fixes Virtualize<TItem> AnchorMode.End anchoring after the initial ItemsProvider load populates an empty viewport.

Changes:

  • Corrects initial bottom-state tracking.
  • Applies pending spacer styles before measuring scroll geometry.
  • Adds refresh support and deterministic E2E coverage.
File summaries
File Summary
src/Components/Web.JS/src/Virtualize.ts Updates End anchoring and layout synchronization.
src/Components/test/testassets/BasicTestApp/VirtualizationAnchorMode.razor Adds a refresh-data test control.
src/Components/test/E2ETest/Tests/VirtualizationTest.cs Adds initial-load anchoring coverage; two moderate findings remain, each with 1 vote, regarding zero-count requests and observer callback cleanup.
Review details

Suppressed comments (2)

src/Components/test/E2ETest/Tests/VirtualizationTest.cs:5502

  • Because the wrapper is installed before this reload, no OnSpacerBeforeVisible callback reaches Virtualize to initialize _visibleItemCapacity. The test provider honors request.Count with Take(request.Count), so this explicit refresh starts with a zero-count request and renders no items; scrollHeight - clientHeight remains zero and the assertion cannot exercise the intended 0→N transition. Seed a nonzero request capacity without allowing the bottom-tracking callback to run, or otherwise adjust the setup before calling RefreshDataAsync.
            Browser.Exists(By.Id("reload-with-initial-index")).Click();
            Browser.True(() => Convert.ToInt64(js.ExecuteScript(
                "return window.__virtualizeObserverCallbacks.length;"), CultureInfo.InvariantCulture) > 0);

            Browser.Exists(By.Id("refresh-data")).Click();
            Browser.Contains("Refreshed data", () => Browser.Exists(By.Id("status")).Text);
            Browser.True(() => GetMaximumScrollTop(js, container) > 0);

src/Components/test/E2ETest/Tests/VirtualizationTest.cs:5528

  • Restoring the global constructor does not change the observer instance created from this wrapper. Its callback still calls window.__virtualizeObserverCallbacks.push(...), so deleting that property can make any later spacer notification throw a TypeError while the mounted Virtualize is still observing (and can pollute subsequent tests). Disable the wrapper for existing observers or keep a safe queue until the observer/page is disposed.
                window.IntersectionObserver = window.__nativeIntersectionObserver;
                delete window.__nativeIntersectionObserver;
                delete window.__virtualizeObserverCallbacks;
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@ilonatommy

Copy link
Copy Markdown
Member Author
  • Because the wrapper is installed before this reload, no OnSpacerBeforeVisible callback reaches Virtualize to initialize _visibleItemCapacity. The test provider honors request.Count with Take(request.Count), so this explicit refresh starts with a zero-count request and renders no items; scrollHeight - clientHeight remains zero and the assertion cannot exercise the intended 0→N transition. Seed a nonzero request capacity without allowing the bottom-tracking callback to run, or otherwise adjust the setup before calling RefreshDataAsync.

Not true, request.Count might be but TotalItemCount=1000 still created large spacer. The test fails without the fix with the exactly issue reported.
The test does not render real rows before its first assertion, but that is intentional.

  • Restoring the global constructor does not change the observer instance created from this wrapper. Its callback still calls window.__virtualizeObserverCallbacks.push(...), so deleting that property can make any later spacer notification throw a TypeError while the mounted Virtualize is still observing (and can pollute subsequent tests). Disable the wrapper for existing observers or keep a safe queue until the observer/page is disposed.

Looks valid.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Lifecycle-sensitive observer ordering across Server and WebAssembly warrants final human validation.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Virtualize AnchorMode.End does not position at the end after the initial ItemsProvider result

2 participants