Skip to content

fix(virtual-core): sync scrollOffset in applyScrollAdjustment so end-anchored resize is not lost to browser clamp#1209

Merged
piecyk merged 1 commit into
TanStack:mainfrom
mds-ant:fix/resize-adjust-scrolloffset-sync
Jun 26, 2026
Merged

fix(virtual-core): sync scrollOffset in applyScrollAdjustment so end-anchored resize is not lost to browser clamp#1209
piecyk merged 1 commit into
TanStack:mainfrom
mds-ant:fix/resize-adjust-scrolloffset-sync

Conversation

@mds-ant

@mds-ant mds-ant commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

With anchorTo: 'end' and a dynamically growing last item (e.g. a streaming chat reply measured via measureElement), the viewport drifts away from the end starting on the second resize tick.

resizeItem's wasAtEnd branch runs in this order:

  1. update itemSizeCachegetTotalSize() is now larger
  2. applyScrollAdjustment(newTotal − prevTotal)_scrollToOffset(getScrollOffset(), {adjustments: delta})el.scrollTo({top: scrollOffset + delta})
  3. notify(false) → consumer re-renders → sets the sizer style.height to the new getTotalSize()

Step 2 writes scrollTop while the container is still at the old total height (the consumer hasn't re-rendered yet), so the browser clamps the write back to the old max. Because scrollTop didn't move, no scroll event fires, this.scrollOffset stays stale, and the next tick's getVirtualDistanceFromEnd() reads newTotal − viewport − staleOffset > scrollEndThresholdwasAtEnd = false → no further adjustments.

Fix: in applyScrollAdjustment, after _scrollToOffset has recorded _intendedScrollOffset, also bake the adjustment into this.scrollOffset and zero scrollAdjustments (so their sum stays invariant for callers like the itemStart < getScrollOffset() + scrollAdjustments predicate). The DOM write may still be clamped, but scrollOffset now carries the intended position so the next wasAtEnd check stays true and the adjustment lands once the consumer has grown the sizer. This is the same idea as #1176 (eager scrollOffset adjustment for prepend in setOptions), applied to the resize-adjust path.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

…end-anchored resize is not lost to browser clamp

With `anchorTo: 'end'` and a dynamically growing last item, `resizeItem`
calls `applyScrollAdjustment` before `notify()`, so the `scrollTop` write
lands while the sizer is still at the old `getTotalSize()`. The browser
clamps the write, no scroll event fires, and `scrollOffset` stays stale.
The next tick's `getVirtualDistanceFromEnd()` then exceeds
`scrollEndThreshold`, `wasAtEnd` flips false, and the viewport drifts
away from the end from the second resize onward.

Carry the intended target in `scrollOffset` (and zero `scrollAdjustments`
to keep their sum invariant) the same way the prepend path in `setOptions`
does (TanStack#1176), so the next `wasAtEnd` check sees the post-adjustment
position regardless of whether the DOM write was clamped.
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

applyScrollAdjustment now copies pending scroll adjustments into scrollOffset and clears the pending delta. A regression test covers consecutive end-anchored resizes when the first scroll write is clamped. A changeset entry documents the patch.

Changes

End-anchored scroll adjustment sync

Layer / File(s) Summary
Scroll adjustment sync and regression
packages/virtual-core/src/index.ts, packages/virtual-core/tests/index.test.ts, .changeset/resize-adjust-scrolloffset-sync.md
applyScrollAdjustment now transfers pending adjustments into scrollOffset before clearing them; the regression test covers consecutive anchorTo: 'end' resizes with a clamped scroll write; the changeset records the patch release.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • TanStack/virtual#1173: Introduces the end-anchored virtualizer behavior that this scroll-offset sync change targets.
  • TanStack/virtual#1176: Also updates virtual-core to sync internal scroll state during anchor-to-end handling.
  • TanStack/virtual#1199: Changes resize-driven scroll-compensation logic and related tests in the same package.

Suggested reviewers

  • tannerlinsley

Poem

A rabbit hopped by the scroll bar bright,
and tucked the offset in just right.
End-anchored pages held their place,
no drifting off in browser space.
🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the virtual-core scrollOffset fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description matches the required template and includes the changes, checklist, and release impact sections with relevant details.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@mds-ant mds-ant marked this pull request as ready for review June 25, 2026 16:21
@mds-ant

mds-ant commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@piecyk Does this look good to you?

@piecyk piecyk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice fix 👏 Let's 🚢 🇮🇹

Just a side note, and non-blocking: the iOS path isn’t covered here. The isIOSWebKit() && scrolling branch defers and never reaches the new code, so the same drift could still happen there.

There are a few broader issues around isIOSWebKit, so I’ll add this there.

@nx-cloud

nx-cloud Bot commented Jun 26, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 75a5a2b

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

☁️ Nx Cloud last updated this comment at 2026-06-26 10:05:18 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jun 26, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-virtual

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

@tanstack/lit-virtual

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

@tanstack/react-virtual

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

@tanstack/solid-virtual

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

@tanstack/svelte-virtual

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

@tanstack/virtual-core

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

@tanstack/vue-virtual

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

commit: 75a5a2b

@piecyk piecyk merged commit 37be284 into TanStack:main Jun 26, 2026
10 checks passed
@piecyk

piecyk commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Thanks @mds-ant

@github-actions github-actions Bot mentioned this pull request Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants