Skip to content

Fix Virtualize user scroll during pending ScrollToItemAsync - #69288

Open
ilonatommy wants to merge 1 commit into
dotnet:mainfrom
ilonatommy:fix-68777
Open

ilonatommy wants to merge 1 commit into
dotnet:mainfrom
ilonatommy:fix-68777

Conversation

@ilonatommy

Copy link
Copy Markdown
Member

Fixes a race where manual user scrolling does not reliably cancel an in-progress Virtualize<TItem>.ScrollToItemAsync operation.

When the target item is still loading, the rendered viewport can consist primarily of a large spacer. Scrolling within that spacer does not necessarily change its intersection state, so the existing IntersectionObserver may not produce another callback. Without that callback, the pending provider request is not canceled and the original programmatic scroll can later override the user's position.

Description

  • Detect user input that interrupts a programmatic scroll, pending alignment, or scroll convergence.
  • Stop the active convergence and clear abandoned alignment state.
  • Create a fresh one-shot IntersectionObserver using the same configuration as the main observer.
  • Feed its initial result through the existing spacer callback path, which cancels the pending scroll and loads the user's viewport.
  • Cancel pending one-shot observation when superseded or disposed.
  • Remove the quarantine from ScrollToItem_UserScrollDuringProviderFetch_UserScrollWins.

Fixes #68777

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

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.

🟡 Changes recommended

Four unresolved moderate findings remain in the user-interruption and observer lifecycle paths.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Fixes a race where user scrolling could be overridden by an in-progress ScrollToItemAsync.

Changes:

  • Adds one-shot spacer observation for interrupted programmatic scrolling.
  • Cleans up observers and pending alignment state.
  • Removes quarantine from the regression E2E test.
File summaries
File Summary and final review comments
src/Components/Web.JS/src/Virtualize.ts Implements user-scroll interruption handling. Four unresolved moderate findings concern cancellation when spacers are outside the viewport (2 votes), stale pendingScrollCorrection (1 vote), uncleared pendingCallbacks (1 vote), and duplicate observers for Home/End paths (1 vote).
src/Components/test/E2ETest/Tests/VirtualizationTest.cs Re-enables and documents the regression E2E test.
Review details

Suppressed comments (3)

src/Components/Web.JS/src/Virtualize.ts:561

  • When interrupting RestoreSnapshot, restoreAnchorForShift may already have set pendingScrollCorrection. Leaving that flag set lets the next refreshObservedElements apply the old correction to the user's new position, so the abandoned self-scroll can still move the viewport. Clear pendingScrollCorrection here along with the other pending alignment state.
    pendingAlignLocalIndex = null;

src/Components/Web.JS/src/Virtualize.ts:558

  • Queued entries from the main observer are left in pendingCallbacks when switching to the one-shot observer. If user input arrives during the 50 ms throttle window, the old programmatic entry is flushed later and treated as a current viewport-fill event, potentially starting another provider refresh for the abandoned target. Clear both spacer entries here, as beginProgrammaticScroll already does.
    stopConvergenceObserving();

src/Components/Web.JS/src/Virtualize.ts:562

  • When End or Home interrupts an active programmatic scroll, this call creates a one-shot observer, but those handlers immediately call reobserveSpacers() and start their own jump/convergence. Both observers can then deliver the same initial spacer state as UserScroll, causing duplicate spacer callbacks and provider-refresh churn. Avoid scheduling the one-shot for these explicit reobserve paths, or cancel it before reobserving.
    observeSpacersAfterUserScroll();
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

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

Comment on lines +270 to +271
scrollActivity.source = ScrollSource.UserScroll;
processIntersectionEntries(entries);
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

2 participants