fix(virtual-core): sync scrollOffset in applyScrollAdjustment so end-anchored resize is not lost to browser clamp#1209
Conversation
…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.
📝 WalkthroughWalkthrough
ChangesEnd-anchored scroll adjustment sync
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@piecyk Does this look good to you? |
piecyk
left a comment
There was a problem hiding this comment.
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.
|
View your CI Pipeline Execution ↗ for commit 75a5a2b
☁️ Nx Cloud last updated this comment at |
|
Thanks @mds-ant |
🎯 Changes
With
anchorTo: 'end'and a dynamically growing last item (e.g. a streaming chat reply measured viameasureElement), the viewport drifts away from the end starting on the second resize tick.resizeItem'swasAtEndbranch runs in this order:itemSizeCache→getTotalSize()is now largerapplyScrollAdjustment(newTotal − prevTotal)→_scrollToOffset(getScrollOffset(), {adjustments: delta})→el.scrollTo({top: scrollOffset + delta})notify(false)→ consumer re-renders → sets the sizerstyle.heightto the newgetTotalSize()Step 2 writes
scrollTopwhile 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. BecausescrollTopdidn't move, no scroll event fires,this.scrollOffsetstays stale, and the next tick'sgetVirtualDistanceFromEnd()readsnewTotal − viewport − staleOffset > scrollEndThreshold→wasAtEnd = false→ no further adjustments.Fix: in
applyScrollAdjustment, after_scrollToOffsethas recorded_intendedScrollOffset, also bake the adjustment intothis.scrollOffsetand zeroscrollAdjustments(so their sum stays invariant for callers like theitemStart < getScrollOffset() + scrollAdjustmentspredicate). The DOM write may still be clamped, butscrollOffsetnow carries the intended position so the nextwasAtEndcheck stays true and the adjustment lands once the consumer has grown the sizer. This is the same idea as #1176 (eagerscrollOffsetadjustment for prepend insetOptions), applied to the resize-adjust path.✅ Checklist
pnpm run test:pr.🚀 Release Impact