@@ -394,6 +394,8 @@ type ScrollState = {
394394
395395 // settling
396396 stableFrames : number
397+ // Keep arrival across measurement-driven retargets so external scrolls can cancel.
398+ hasReachedTarget : boolean
397399}
398400
399401type PendingScrollAnchor = [
@@ -926,6 +928,18 @@ export class Virtualizer<
926928 Math . abs ( offset - this . _intendedScrollOffset ) < 1.5
927929 ) {
928930 offset = this . _intendedScrollOffset
931+ } else if (
932+ this . scrollState ?. index != null &&
933+ this . scrollState . hasReachedTarget &&
934+ ! approxEqual ( offset , this . scrollState . lastTargetOffset ) &&
935+ ! approxEqual ( offset , this . getScrollStateTarget ( this . scrollState ) ) &&
936+ ( this . _clampedAdjustment === null ||
937+ Math . abs ( offset - this . _clampedAdjustment . maxAtWrite ) >= 1.5 )
938+ ) {
939+ // The index scroll reached its target, but an external scroll moved
940+ // away while we waited for measurements. Our own measurement updates
941+ // (including clamped writes) still need to reconcile.
942+ this . scrollState = null
929943 }
930944 this . _intendedScrollOffset = null
931945
@@ -1157,6 +1171,14 @@ export class Virtualizer<
11571171 this . reconcileScroll ( )
11581172 } )
11591173 }
1174+ private getScrollStateTarget ( state : ScrollState ) {
1175+ const offsetInfo = state . toEnd
1176+ ? ( [ Math . max ( this . getMaxScrollOffset ( ) , 0 ) ] as const )
1177+ : state . index != null
1178+ ? this . getOffsetForIndex ( state . index , state . align )
1179+ : undefined
1180+ return offsetInfo ? offsetInfo [ 0 ] : state . lastTargetOffset
1181+ }
11601182 private reconcileScroll ( ) {
11611183 if ( ! this . scrollState ) return
11621184
@@ -1170,23 +1192,23 @@ export class Virtualizer<
11701192 return
11711193 }
11721194
1173- const offsetInfo = this . scrollState . toEnd
1174- ? ( [ Math . max ( this . getMaxScrollOffset ( ) , 0 ) ] as const )
1175- : this . scrollState . index != null
1176- ? this . getOffsetForIndex ( this . scrollState . index , this . scrollState . align )
1177- : undefined
1178- const targetOffset = offsetInfo
1179- ? offsetInfo [ 0 ]
1180- : this . scrollState . lastTargetOffset
1195+ const targetOffset = this . getScrollStateTarget ( this . scrollState )
11811196
1182- // Require one stable frame where target matches scroll offset.
1183- // approxEqual() already tolerates minor fluctuations, so one frame is sufficient
1184- // to confirm scroll has reached its target without premature cleanup.
1185- const STABLE_FRAMES = 1
1197+ // Index targets can still depend on cached sizes during the first rAF,
1198+ // which runs before ResizeObserver delivers pending measurements. Wait
1199+ // another frame (two when measurements are themselves deferred to rAF)
1200+ // before retiring an index scroll. Absolute offsets don't need this wait.
1201+ const STABLE_FRAMES =
1202+ this . scrollState . index == null
1203+ ? 1
1204+ : this . options . useAnimationFrameWithResizeObserver
1205+ ? 3
1206+ : 2
11861207
11871208 const targetChanged = targetOffset !== this . scrollState . lastTargetOffset
11881209
11891210 if ( ! targetChanged && approxEqual ( targetOffset , this . getScrollOffset ( ) ) ) {
1211+ this . scrollState . hasReachedTarget = true
11901212 this . scrollState . stableFrames ++
11911213 if ( this . scrollState . stableFrames >= STABLE_FRAMES ) {
11921214 // Final-pass exact landing. The reconcile-stable check uses a 1.01px
@@ -1221,7 +1243,11 @@ export class Virtualizer<
12211243 this . scrollState . behavior === 'smooth' && distance > viewport
12221244
12231245 this . scrollState . lastTargetOffset = targetOffset
1224- if ( ! keepSmooth ) {
1246+ if ( keepSmooth ) {
1247+ // A smooth retarget travels again, so its intermediate scroll
1248+ // events must not read as an external scroll away from the target.
1249+ this . scrollState . hasReachedTarget = false
1250+ } else {
12251251 this . scrollState . behavior = 'auto'
12261252 }
12271253
@@ -1955,6 +1981,7 @@ export class Virtualizer<
19551981 startedAt : now ,
19561982 lastTargetOffset : offset ,
19571983 stableFrames : 0 ,
1984+ hasReachedTarget : false ,
19581985 }
19591986
19601987 this . _scrollToOffset ( offset , { adjustments : undefined , behavior } )
@@ -1989,6 +2016,7 @@ export class Virtualizer<
19892016 startedAt : now ,
19902017 lastTargetOffset : offset ,
19912018 stableFrames : 0 ,
2019+ hasReachedTarget : approxEqual ( offset , this . getScrollOffset ( ) ) ,
19922020 }
19932021
19942022 this . _scrollToOffset ( offset , { adjustments : undefined , behavior } )
@@ -2010,6 +2038,7 @@ export class Virtualizer<
20102038 startedAt : now ,
20112039 lastTargetOffset : offset ,
20122040 stableFrames : 0 ,
2041+ hasReachedTarget : false ,
20132042 }
20142043
20152044 this . _scrollToOffset ( offset , { adjustments : undefined , behavior } )
@@ -2033,6 +2062,7 @@ export class Virtualizer<
20332062 startedAt : this . now ( ) ,
20342063 lastTargetOffset : offset ,
20352064 stableFrames : 0 ,
2065+ hasReachedTarget : approxEqual ( offset , this . getScrollOffset ( ) ) ,
20362066 }
20372067
20382068 this . _scrollToOffset ( offset , { adjustments : undefined , behavior } )
0 commit comments