Skip to content

Commit df83a48

Browse files
committed
fix(virtual-core): apply the whole shrink while a clamped end write is pending
If an end-pinned item grew before the sizer did, its compensation write is clamped and waits to be retried once the sizer grows. A scroll element shrink in that window was capped at the DOM distance to the end, which is stale until the sizer commits, so the shrink was dropped and the retry landed short of the new end. Apply the whole shrink while a clamped write is pending; the retry lands it.
1 parent aaaa7bf commit df83a48

2 files changed

Lines changed: 22 additions & 1 deletion

File tree

‎packages/virtual-core/src/index.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -903,8 +903,13 @@ export class Virtualizer<
903903
this.getVirtualDistanceFromEnd() <= this.options.scrollEndThreshold
904904
this.scrollRect = rect
905905
if (wasAtEnd && prevSize > this.getSize()) {
906+
// While a clamped write waits for the sizer to grow, the DOM end
907+
// is stale; apply the whole shrink and let the retry land it.
908+
const shrink = prevSize - this.getSize()
906909
this.applyScrollAdjustment(
907-
Math.min(prevSize - this.getSize(), this.getDistanceFromEnd()),
910+
this._clampedAdjustment !== null
911+
? shrink
912+
: Math.min(shrink, this.getDistanceFromEnd()),
908913
)
909914
}
910915
this.maybeNotify()

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3318,6 +3318,22 @@ test('anchorTo:end keeps the distance from the end when pinned within the thresh
33183318
expect(virtualizer.getVirtualDistanceFromEnd()).toBe(5)
33193319
})
33203320

3321+
test('anchorTo:end reaches the end when the scroll element shrinks while a clamped write waits for the sizer', () => {
3322+
const messages = Array.from({ length: 10 }, (_, i) => ({ id: `m-${i}` }))
3323+
const { virtualizer, scrollElement, scrollToFn, resizeViewport } =
3324+
createChatVirtualizer({ messages, offset: 300 })
3325+
3326+
// The last item grows before the sizer does, so the end write is clamped.
3327+
virtualizer.resizeItem(9, 120)
3328+
resizeViewport(150)
3329+
// The sizer commits and the clamped write is retried.
3330+
;(scrollElement as any).scrollHeight = 570
3331+
virtualizer._willUpdate()
3332+
3333+
const [offset, { adjustments }] = scrollToFn.mock.calls.at(-1)!
3334+
expect(offset + (adjustments ?? 0)).toBe(420)
3335+
})
3336+
33213337
test.each(['scroll element first', 'item first'])(
33223338
'anchorTo:end stays pinned when an item above the viewport shrinks in the same frame as the scroll element (%s)',
33233339
(order) => {

0 commit comments

Comments
 (0)