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

fix(virtual-core): clamp tracked scrollOffset at 0 in end-anchor compensation#1230
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

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a424fb82-f2b1-45f9-ab78-adbd9088372d

📥 Commits

Reviewing files that changed from the base of the PR and between e2cb096 and 3f963ae.

📒 Files selected for processing (3)
  • .changeset/clamp-negative-end-anchor-scroll-offset.md
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts

📝 Walkthrough

Walkthrough

Virtualizer now clamps negative scroll offsets during end-anchor measurement compensation and setOptions re-anchoring. New regression tests cover unscrollable elements, end-distance metrics, and transient negative offsets.

Changes

End-anchor offset correction

Layer / File(s) Summary
Clamp offsets during correction and re-anchoring
packages/virtual-core/src/index.ts, .changeset/clamp-negative-end-anchor-scroll-offset.md
Computed re-anchor offsets and measurement-adjusted scroll offsets are clamped to 0; the changeset records a patch release.
Cover unscrollable end-anchor behavior
packages/virtual-core/tests/index.test.ts
Tests verify zero scrollTop, zero tracked offset, correct end metrics, and recovery from transient negative offsets.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: 2wheeh, tannerlinsley

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description explains the bug, fix, and tests, but it does not follow the required template sections. Add the required 🎯 Changes, ✅ Checklist, and 🚀 Release Impact sections, and fill in the checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main fix: clamping tracked scrollOffset at 0 for end-anchor compensation.
Linked Issues check ✅ Passed The code and tests match #1229 by clamping negative offsets in both compensation paths and covering the unscrollable end-anchor case.
Out of Scope Changes check ✅ Passed The changes are limited to the fix, its tests, and a changeset, with no clear unrelated code.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/virtual-core/tests/index.test.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): packages/virtual-core/tests/index.test.ts


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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
More templates

@tanstack/angular-virtual

npm i https://pkg.pr.new/@tanstack/angular-virtual@1230

@tanstack/lit-virtual

npm i https://pkg.pr.new/@tanstack/lit-virtual@1230

@tanstack/marko-virtual

npm i https://pkg.pr.new/@tanstack/marko-virtual@1230

@tanstack/react-virtual

npm i https://pkg.pr.new/@tanstack/react-virtual@1230

@tanstack/solid-virtual

npm i https://pkg.pr.new/@tanstack/solid-virtual@1230

@tanstack/svelte-virtual

npm i https://pkg.pr.new/@tanstack/svelte-virtual@1230

@tanstack/virtual-core

npm i https://pkg.pr.new/@tanstack/virtual-core@1230

@tanstack/vue-virtual

npm i https://pkg.pr.new/@tanstack/vue-virtual@1230

commit: 3f963ae

@piecyk
piecyk merged commit 1e3b908 into TanStack:main Jul 17, 2026
10 checks passed
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