fix(virtual-core): clamp tracked scrollOffset at 0 in end-anchor compensation by piecyk · Pull Request #1230 · TanStack/virtual · GitHub
Skip to content

fix(virtual-core): clamp tracked scrollOffset at 0 in end-anchor compensation - #1230

Merged
piecyk merged 1 commit into
TanStack:mainfrom
piecyk:fix/end-anchor-negative-scroll-offset
Jul 17, 2026
Merged

piecyk merged 1 commit into
TanStack:mainfrom
piecyk:fix/end-anchor-negative-scroll-offset

Conversation

@piecyk

@piecyk piecyk commented Jul 17, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1229

Problem

With anchorTo: 'end' and element scrolling, the tracked scrollOffset was written without clamping to the element's reachable range in two places:

  1. applyScrollAdjustment() — the eager this.scrollOffset += this.scrollAdjustments write
  2. the setOptions() edge-key re-anchor block — this.scrollOffset = anchorItem.start + anchorOffset

When items measure smaller than their estimates and the content is shorter than the viewport, the end-anchor compensation applies a negative delta. The DOM scrollTop write is clamped to 0 by the browser, but the tracked offset keeps the negative value — and since an unscrollable element never fires a scroll event, nothing ever corrects it. For the lifetime of the instance:

  • getDistanceFromEnd() reports a constant phantom distance and isAtEnd() stays false, so anything steering scrollToEnd() toward convergence spins forever
  • _flushIosDeferredIfReady() early-returns while getScrollOffset() < 0 (the rubber-band guard), so on iOS WebKit deferred measurement corrections stay queued forever and items freeze at their estimated positions

Fix

Clamp only the lower bound in both writes. Upper-bound overflow is deliberately left alone — it is transiently legitimate mid-prepend while the consumer's sizer catches up, but a negative element offset is invalid unconditionally.

Tests

Two regression tests reproducing the issue's minimal conditions (unscrollable element, anchorTo: 'end', estimates larger than measured sizes). Both fail on main (tracked offset lands at a stable -10, exactly as reported) and pass with the clamps.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed end-anchored scrolling when content is shorter than the viewport.
    • Prevented negative scroll offsets from causing incorrect distance-from-end calculations or leaving measurements out of sync.
    • Improved scroll correction after item resizing and re-anchoring, including on iOS.
  • Tests

    • Added regression coverage for unscrollable elements and end-anchor adjustments.

…ensation

With anchorTo: 'end' and element scrolling, measurement compensation
(applyScrollAdjustment) and the setOptions edge-key re-anchor wrote the
tracked scrollOffset without clamping to the reachable range. When items
measured smaller than their estimates and the content was shorter than
the viewport, the offset went negative with no scroll event ever able to
correct it (an unscrollable element emits none), permanently skewing
getDistanceFromEnd()/isAtEnd() and wedging _flushIosDeferredIfReady so
iOS deferred measurement corrections never flushed.

Clamp only the lower bound: upper-bound overflow is transiently
legitimate mid-prepend while the consumer's sizer catches up, but a
negative element offset is invalid unconditionally.

Fixes TanStack#1229

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

@nx-cloud

nx-cloud Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 3f963ae

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 2m 57s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 20s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-17 10:46:31 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 17, 2026

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

1 participant