From f03dbf0f5e9f3b78fae4fc923b098f8d12b5fe89 Mon Sep 17 00:00:00 2001 From: Prayag Gaikwad Date: Tue, 20 Oct 2026 17:00:45 +0530 Subject: [PATCH 1/6] fix(virtual-core): cancel stale anchor work on explicit scroll --- packages/virtual-core/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/virtual-core/src/index.ts b/packages/virtual-core/src/index.ts index 949eb20d..39e6a440 100644 --- a/packages/virtual-core/src/index.ts +++ b/packages/virtual-core/src/index.ts @@ -2429,6 +2429,12 @@ export class Virtualizer< behavior: ScrollBehavior, kind: 'absolute' | 'relative', ) { + // A new explicit command supersedes layout-driven work queued before it. + // Clear both before any iOS deferral so a later commit/retry cannot + // overwrite the user's newer scroll intent. + this.pendingScrollAnchor = null + this._clampedAdjustment = null + const tail = this._iosGesture.tail() if (kind === 'absolute' && tail !== null) { if (tail === 'fling' && iosMomentumOverridesWrites()) { From f09904000572f867dcbb6e93e8c06058287d6a11 Mon Sep 17 00:00:00 2001 From: Prayag Gaikwad Date: Tue, 20 Oct 2026 17:00:48 +0530 Subject: [PATCH 2/6] test(virtual-core): cover explicit scroll superseding pending anchor --- .../virtual-core/tests/scroll-commands.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/virtual-core/tests/scroll-commands.test.ts b/packages/virtual-core/tests/scroll-commands.test.ts index 35ca5ab6..278deb28 100644 --- a/packages/virtual-core/tests/scroll-commands.test.ts +++ b/packages/virtual-core/tests/scroll-commands.test.ts @@ -1882,3 +1882,21 @@ test('a write to a scroll element that went away is not adopted by the next one' expect(v.scrollOffset).toBe(1001) }) + +test('an explicit scroll cancels pending anchor work and a clamped retry', () => { + const { v, el } = setupHandDrivenScroll() + + // Model a prepend anchor sync and a clamped compensation still waiting + // for a later layout commit. A newer explicit command must supersede both. + v['pendingScrollAnchor'] = { kind: 'anchor', delta: 150 } + v['_clampedAdjustment'] = { target: 1200, maxAtWrite: 300 } + + v.scrollToOffset(100) + + expect(v['pendingScrollAnchor']).toBeNull() + expect(v['_clampedAdjustment']).toBeNull() + + // A later layout opportunity must not restore the stale target. + v['_retryClampedAdjustment']() + expect(el.scrollTop).toBe(100) +}) From a1558ef58cfcf21fe2d0256b2b41d97809bf6e4e Mon Sep 17 00:00:00 2001 From: Prayag Gaikwad Date: Tue, 20 Oct 2026 18:06:33 +0530 Subject: [PATCH 3/6] test(virtual-core): exercise real scroll cancellation paths --- .../tests/scroll-commands.test.ts | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/virtual-core/tests/scroll-commands.test.ts b/packages/virtual-core/tests/scroll-commands.test.ts index 278deb28..dbbc5f0f 100644 --- a/packages/virtual-core/tests/scroll-commands.test.ts +++ b/packages/virtual-core/tests/scroll-commands.test.ts @@ -1883,20 +1883,30 @@ test('a write to a scroll element that went away is not adopted by the next one' expect(v.scrollOffset).toBe(1001) }) -test('an explicit scroll cancels pending anchor work and a clamped retry', () => { - const { v, el } = setupHandDrivenScroll() +test('an explicit scroll cancels a pending anchor before layout commit', () => { + const { v, el, scroll } = setupHandDrivenScroll({ + anchorTo: 'end', + getItemKey: (i) => i, + }) - // Model a prepend anchor sync and a clamped compensation still waiting - // for a later layout commit. A newer explicit command must supersede both. - v['pendingScrollAnchor'] = { kind: 'anchor', delta: 150 } - v['_clampedAdjustment'] = { target: 1200, maxAtWrite: 300 } + el.scrollTop = 2000 + scroll(2000, false) + v.setOptions({ ...v.options, count: 112, getItemKey: (i) => i - 12 }) v.scrollToOffset(100) + v._willUpdate() + expect(el.scrollTop).toBe(100) +}) - expect(v['pendingScrollAnchor']).toBeNull() - expect(v['_clampedAdjustment']).toBeNull() +test('an explicit scroll cancels a pending clamped retry', () => { + const { v, el, scroll } = setupHandDrivenScroll() - // A later layout opportunity must not restore the stale target. - v['_retryClampedAdjustment']() + el.scrollTop = 4800 + scroll(4800, false) + v.resizeItem(0, 100) + v.scrollToOffset(100) + + el.scrollHeight = 5050 + v._willUpdate() expect(el.scrollTop).toBe(100) }) From edc9634d84466e7f64f7dd1866826798194f3306 Mon Sep 17 00:00:00 2001 From: Prayag Gaikwad Date: Tue, 20 Oct 2026 18:11:32 +0530 Subject: [PATCH 4/6] chore: add virtual-core patch changeset --- .changeset/dull-kings-stare.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dull-kings-stare.md diff --git a/.changeset/dull-kings-stare.md b/.changeset/dull-kings-stare.md new file mode 100644 index 00000000..f84b78b0 --- /dev/null +++ b/.changeset/dull-kings-stare.md @@ -0,0 +1,5 @@ +--- +"@tanstack/virtual-core": patch +--- + +fix(virtual-core): cancel stale anchor work on explicit scroll From 4d9172fe57094bae4c96a63e7919d4a2386d7802 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:14:59 +0000 Subject: [PATCH 5/6] ci: apply automated fixes --- .changeset/dull-kings-stare.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/dull-kings-stare.md b/.changeset/dull-kings-stare.md index f84b78b0..fbdcac7c 100644 --- a/.changeset/dull-kings-stare.md +++ b/.changeset/dull-kings-stare.md @@ -1,5 +1,5 @@ --- -"@tanstack/virtual-core": patch +'@tanstack/virtual-core': patch --- fix(virtual-core): cancel stale anchor work on explicit scroll From 6ab4bb4b68aa2c06aba87c21270d5abf7f2a3a60 Mon Sep 17 00:00:00 2001 From: Damian Pieczynski Date: Tue, 20 Oct 2026 20:17:04 +0200 Subject: [PATCH 6/6] test(virtual-core): update scroll-trace snapshots for explicit scroll cancellation An explicit scroll now drops a pending anchor and clamp retry, so the seeds where a stale anchor pulled a command back move. Co-Authored-By: Claude Opus 5.5 --- .../__snapshots__/scroll-trace.test.ts.snap | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/virtual-core/tests/__snapshots__/scroll-trace.test.ts.snap b/packages/virtual-core/tests/__snapshots__/scroll-trace.test.ts.snap index b390002b..fd2dc0b2 100644 --- a/packages/virtual-core/tests/__snapshots__/scroll-trace.test.ts.snap +++ b/packages/virtual-core/tests/__snapshots__/scroll-trace.test.ts.snap @@ -29,7 +29,7 @@ exports[`scroll behaviour snapshot > anchorEnd web 1`] = ` "20: 6616fe9d f780f908 13bd300f 30643489 ec2d1b0c ebb40e0f 63ed23da 68a18267 253fe61c 09a1874e", "30: 5e6688c5 009cbdfe 8c8bdd3e 46527f28 f7133699 9b9b908c e14b74d0 8f93c51a 552b4b5b 660f3603", "40: 93dd81a7 c9c39478 622e6d21 4d60c709 e721cb06 5b6d2738 868d9e7b f3ef7742 2aa59586 886aae03", - "50: d2429922 8cc11ea8 81d0357b cfd20837 8fcc29b1 3cea4ec5 27af96a0 e46aa4df 3c8d8225 18bcf75b", + "50: d2429922 8cc11ea8 81d0357b cfd20837 8fcc29b1 3cea4ec5 27af96a0 e46aa4df 4bc85495 18bcf75b", ] `; @@ -62,7 +62,7 @@ exports[`scroll behaviour snapshot > base web 1`] = ` "20: 7886a736 ceac4ab2 2c8bfacf 6865f6c2 d47af093 a7b02f04 63ed23da 715e0039 4ddf6da0 a5002541", "30: 5978f80e 0c5f12e2 79ef7be1 aec3dd30 1b99b475 46df8677 4e88df21 8f93c51a 5b30b633 66d94fc8", "40: 8249f4ed 2170911f c938e8f5 feacb1d8 826217e9 8a2b7146 f9ead96e d1dc687f 36e554f8 03c3bcc1", - "50: d2429922 07cac42c 78c9d354 cfd20837 b30f44cd 5f27e381 5c11a132 1841edc4 36b0937d 9ddde49c", + "50: d2429922 07cac42c 78c9d354 cfd20837 b30f44cd 5f27e381 5c11a132 1841edc4 d58fa408 9ddde49c", ] `; @@ -128,7 +128,7 @@ exports[`scroll behaviour snapshot > horizontal web 1`] = ` "20: 7886a736 ceac4ab2 2c8bfacf 6865f6c2 d47af093 a7b02f04 63ed23da 715e0039 4ddf6da0 a5002541", "30: 5978f80e 0c5f12e2 79ef7be1 aec3dd30 1b99b475 46df8677 4e88df21 8f93c51a 5b30b633 66d94fc8", "40: 8249f4ed 2170911f c938e8f5 feacb1d8 826217e9 8a2b7146 f9ead96e d1dc687f 36e554f8 03c3bcc1", - "50: d2429922 07cac42c 78c9d354 cfd20837 b30f44cd 5f27e381 5c11a132 1841edc4 36b0937d 9ddde49c", + "50: d2429922 07cac42c 78c9d354 cfd20837 b30f44cd 5f27e381 5c11a132 1841edc4 d58fa408 9ddde49c", ] `; @@ -138,8 +138,8 @@ exports[`scroll behaviour snapshot > lanes ios 1`] = ` "10: 514a82cf 25c644f3 7a8d2a09 2abee23f 5872f8d5 c103e623 c4d2fe64 554a1766 21de167e 487aeb4b", "20: f2245f58 81bd9a1e 79b54054 6e12648a 38539662 4b10cf13 0bddc3e4 11317c75 bbfdc970 d2432a43", "30: 12468132 20c7aff9 67dcb6d5 03291755 c9ffab63 5f0a007d ec1d6907 f41ff60d f7397d3b 701d9fb6", - "40: d7be12c4 972dd8d5 cd9e5629 bc8f344f 60dc134e 96523d8a d0d37a83 08867236 7da74ce4 bbba1406", - "50: 89573ef3 4d90e248 f737f69e c3acfdb6 39c42ebf 55ca59be e01db9de cedb1bda cc70c64d 00e88da4", + "40: d7be12c4 972dd8d5 cd9e5629 bc8f344f 3d61d1ac 96523d8a d0d37a83 08867236 7da74ce4 bbba1406", + "50: 89573ef3 1fa2a7b0 f737f69e c3acfdb6 39c42ebf 55ca59be e01db9de cedb1bda cc70c64d 00e88da4", ] `; @@ -149,19 +149,19 @@ exports[`scroll behaviour snapshot > lanes ios27 1`] = ` "10: 514a82cf ddf40a6b 7a8d2a09 2abee23f af7ac992 c103e623 00ca2427 29c40818 21de167e 487aeb4b", "20: f2245f58 2424b184 79b54054 6e12648a 38539662 4b10cf13 feb6fb5c 11317c75 42d7823b d2432a43", "30: 65523943 20c7aff9 67dcb6d5 473c2a8f 7d23b05f 5f0a007d ec1d6907 e86742e8 f7397d3b 701d9fb6", - "40: d7be12c4 95d3e050 cd9e5629 bc8f344f d553998f 96523d8a d0d37a83 08867236 f50ddbf7 bbba1406", - "50: 89573ef3 4d90e248 53dfd836 9b5a1c5a b5b1ab21 8dd98e7e e01db9de cedb1bda cc70c64d 00e88da4", + "40: d7be12c4 95d3e050 cd9e5629 bc8f344f 6df9343f 96523d8a d0d37a83 08867236 f50ddbf7 bbba1406", + "50: 89573ef3 1fa2a7b0 53dfd836 9b5a1c5a b5b1ab21 8dd98e7e e01db9de cedb1bda cc70c64d 00e88da4", ] `; exports[`scroll behaviour snapshot > lanes web 1`] = ` [ "0: 07b019d6 50ba0f48 2528ce0b 56751a0d f27c3816 3ad778af 510a0a14 de5f2a5e 8a8df520 9c4df1f6", - "10: 0e9abc72 9ab1beaa 7a8d2a09 cc167259 bfd342b3 c103e623 cb717084 554a1766 21de167e 487aeb4b", + "10: 0e9abc72 9ab1beaa 7a8d2a09 cc167259 bfd342b3 c103e623 d68fb624 554a1766 21de167e 487aeb4b", "20: 3fb995ae 81bd9a1e 79b54054 e99e680d f2c453bb 4b10cf13 588f804a bf451280 912642c1 d2432a43", - "30: 5a9c1d81 20c7aff9 9b7acc6c 03291755 c12620e1 5f0a007d ec1d6907 e64d3f41 f7397d3b 2e9ca3d4", - "40: 1bd23826 16a5fa48 410c3108 bc8f344f 149cffe2 49344e33 d0d37a83 d4926881 5860422c bf14dd85", - "50: 89573ef3 4d90e248 05101a4e 9886e68a 39c42ebf 55ca59be e01db9de cedb1bda 9a73528c b6ea452c", + "30: 5a9c1d81 20c7aff9 1fd7b2b0 03291755 c12620e1 5f0a007d ec1d6907 e64d3f41 f7397d3b 2e9ca3d4", + "40: 1bd23826 16a5fa48 410c3108 bc8f344f 9ec26077 8403f63f d0d37a83 d4926881 5860422c bf14dd85", + "50: 89573ef3 1fa2a7b0 05101a4e 9886e68a 39c42ebf 55ca59be e01db9de cedb1bda 632ce26a b6ea452c", ] `; @@ -194,7 +194,7 @@ exports[`scroll behaviour snapshot > margin web 1`] = ` "20: 9e4b77f5 cb7198cf 9af764d5 a3f06d61 167abfaf d0cffbec f4d9b186 d9d12542 13de058c 200d4ab4", "30: aadc45f7 37ce7a93 d1388d46 f4d2cc30 c28043e6 f2b45513 8801c1ac c8ac484a 94e08cb6 2171ac81", "40: 62dfcbaf 657f1b43 a3a841e4 22ee5368 d3e0e255 bfb3f87c 20a8933f 0d454d4f 70957626 87f80beb", - "50: 5baeb658 6ee3c4c0 cec3a144 d5b8753e 3371d972 2ad8996d 2f514fed 95424c35 c8a0843e ce539614", + "50: 5baeb658 6ee3c4c0 cec3a144 d5b8753e 3371d972 2ad8996d 2f514fed 95424c35 860fbdef ce539614", ] `; @@ -234,7 +234,7 @@ exports[`scroll behaviour snapshot > padding web 1`] = ` exports[`scroll behaviour snapshot > predicate ios 1`] = ` [ "0: 1503987b da28a13a 54033cbd 75995293 b092f4c8 d731f6c6 96911d82 a3c61d73 0b82d0ed 2b56adca", - "10: 75e68079 2bc31908 888425aa 5683ada3 2b4bbf15 89be8bb7 ebfa70d4 9e4371d0 8134fb03 750041da", + "10: 75e68079 2bc31908 888425aa 5683ada3 2b4bbf15 89be8bb7 4743c770 9e4371d0 8134fb03 750041da", "20: 9f915474 4ca88661 54fd2515 1bc77d21 0daaca4c b368ca7e 35aedf09 b8e64de7 675f9530 8caf71f9", "30: 65d2a727 4f062147 b61ba9ff 4c406d77 0c47a6ac 7cbdb0c8 d11197a6 caebb6d3 82cf0b2e 087a5d0c", "40: 079790e8 ae3a4374 d5dec481 ac3796cc a2874f32 2fba1d55 f5b117d9 384995f7 54142c26 d1dae953", @@ -245,7 +245,7 @@ exports[`scroll behaviour snapshot > predicate ios 1`] = ` exports[`scroll behaviour snapshot > predicate ios27 1`] = ` [ "0: 1503987b da28a13a 54033cbd 6c0916d9 b092f4c8 d731f6c6 96911d82 a3c61d73 0b82d0ed a68350d1", - "10: 75e68079 1ff40218 888425aa 671c1e2a 9d2e455d 89be8bb7 de3c22d4 ccdc126b 8134fb03 750041da", + "10: 75e68079 1ff40218 888425aa 671c1e2a 9d2e455d 89be8bb7 a182b468 ccdc126b 8134fb03 750041da", "20: 9f915474 4b896a03 54fd2515 1bc77d21 407334c6 b368ca7e 0727dcf4 b8e64de7 9effe82d 8caf71f9", "30: 60b29a8e 4f062147 7ea2c9cf 6fb5a6e2 0ecf3bcb 7cbdb0c8 d11197a6 d6c24b0e 82cf0b2e 087a5d0c", "40: 079790e8 7e7eb94d d5dec481 ac3796cc a3f65d93 2fba1d55 f5b117d9 384995f7 9c1a89ca d1dae953", @@ -256,11 +256,11 @@ exports[`scroll behaviour snapshot > predicate ios27 1`] = ` exports[`scroll behaviour snapshot > predicate web 1`] = ` [ "0: 1ae1fc6f 54daaa9d e2050bb4 704e0476 1a3b7240 b3620452 ea6b67f3 a3c61d73 09968012 d9597113", - "10: c449bc54 0a115668 20a0834c a6fbd596 bec39b4c 89be8bb7 dddeb6fa 9e4371d0 8134fb03 750041da", + "10: c449bc54 0a115668 20a0834c a6fbd596 bec39b4c 89be8bb7 79981adc 9e4371d0 8134fb03 750041da", "20: a7943819 4ca88661 48a2a876 9c67b567 48734ee1 b368ca7e e989bf83 ce41bea9 675f9530 8caf71f9", "30: f30525e8 7cab4af4 9250e3e4 4c406d77 7417e967 7cbdb0c8 9eeab09f ea061649 82cf0b2e 65080957", "40: 81e56faa a8750438 d5dec481 ac3796cc 1d279633 03a1759f 046db9c2 1e074853 a0c847de 842f2579", - "50: 274e92c7 5b749586 ad467091 da0b01ac 9274c4ab e0f3944f f21db6d8 0ab9d71e d02c95f3 0ef2236c", + "50: 274e92c7 3655a421 ad467091 da0b01ac 9274c4ab e0f3944f f21db6d8 0ab9d71e b798f2f5 0ef2236c", ] `;