@@ -610,15 +610,16 @@ export class Virtualizer<
610610 // further by the time it reports.
611611 // - `smooth`: the write animates, so until it lands the element is
612612 // somewhere between the old offset and `target`.
613- // - `max`, `lowMaxes`: for a command's instant write, the scroll max as
614- // rows measured since the write moved it (`resizeItem`), and each value
615- // it took below `target`. The browser clamps the write to one of them
616- // at a layout, and the sizer can grow back before the report, so the
617- // read-back is that max rather than the element's current one.
613+ // - `lowMaxes`: for a command's instant write, each scroll max below
614+ // `target` the element had since the write. A command's target is
615+ // clamped to the max at the write, but the sizer can shrink under it
616+ // before the report (rows measured smaller, a count change) and grow
617+ // back, so the read-back is the max the browser clamped to then rather
618+ // than the element's current one. Read wherever the sizer can have
619+ // been committed (`_noteClampMax`).
618620 private pendingWrite : {
619621 target : number
620622 smooth : boolean
621- max ?: number
622623 lowMaxes ?: Array < number >
623624 } | null = null
624625 // A compensation write from `applyScrollAdjustment` whose target exceeded
@@ -673,6 +674,7 @@ export class Virtualizer<
673674 this . _resizeBatch = null
674675 if ( batch . notify ) {
675676 this . notify ( batch . sync )
677+ this . _noteClampMax ( )
676678 this . _flushHoldIfReady ( true )
677679 this . _retryClampedAdjustment ( )
678680 }
@@ -1094,8 +1096,8 @@ export class Virtualizer<
10941096 } else if (
10951097 kind === 'clampedReadBack' &&
10961098 this . scrollState ?. index != null &&
1097- elementMax !== null &&
1098- offset < elementMax + this . _holdOffset - 1.5
1099+ // Not ` elementMax`: an adapter can report the read-back at rest.
1100+ offset < this . getMaxScrollOffset ( ) - 1.5
10991101 ) {
11001102 // Our write, clamped at a max the sizer has since grown back
11011103 // from: reconcile writes the target again, even when the layout
@@ -1212,6 +1214,9 @@ export class Virtualizer<
12121214 this . _scrollToOffset ( this . getScrollOffset ( ) )
12131215 }
12141216
1217+ // The consumer has committed the sizer.
1218+ this . _noteClampMax ( )
1219+
12151220 const anchor = this . pendingScrollAnchor
12161221 this . pendingScrollAnchor = null
12171222
@@ -2048,30 +2053,6 @@ export class Virtualizer<
20482053 this . itemSizeCache . set ( key , size )
20492054 this . itemSizeCacheVersion ++
20502055
2051- // Follow the scroll max through the sizes measured before a command's
2052- // instant write reports back (see `pendingWrite`). The element's max
2053- // is read once, at the first resize after the write, before that
2054- // resize reaches the sizer; it may already be below the target. With
2055- // lanes, a row's delta is not the total's, so nothing is followed.
2056- const write =
2057- this . options . lanes === 1 && this . _awaitingCommandReadBack ( )
2058- ? this . pendingWrite !
2059- : null
2060- const followMax = ( max : number ) => {
2061- write ! . max = max
2062- // The element cannot clamp below its own 0, which is the hold in
2063- // layout coordinates; the raw max stays for the next delta.
2064- const reachable = Math . max ( max , this . _holdOffset )
2065- if ( reachable < write ! . target ) {
2066- ; ( write ! . lowMaxes ??= [ ] ) . push ( reachable )
2067- }
2068- }
2069- if ( write !== null && write . max === undefined ) {
2070- const elementMax = this . _elementMaxScrollOffset ( )
2071- if ( elementMax !== null ) followMax ( elementMax + this . _holdOffset )
2072- }
2073- if ( write ?. max !== undefined ) followMax ( write . max + delta )
2074-
20752056 // Before a command's read-back, an index or end target is retargeted
20762057 // from fresh measurements by reconcileScroll next frame; an offset
20772058 // target lands where it was written and the delta is not applied.
@@ -2104,6 +2085,7 @@ export class Virtualizer<
21042085 return
21052086 }
21062087 this . notify ( adjustedSync )
2088+ this . _noteClampMax ( )
21072089 // A consumer that grows the sizer synchronously inside `onChange`
21082090 // (direct DOM updates) may never re-render when the range is
21092091 // unchanged, so retry a clamped write here rather than only in
@@ -2164,6 +2146,23 @@ export class Virtualizer<
21642146 return notUndefined ( measurements [ idx ] )
21652147 }
21662148
2149+ // Record the element's max, in layout coordinates, as one a command's
2150+ // pending instant write may be clamped to (see `lowMaxes` on
2151+ // `pendingWrite`). Called where the sizer can have been committed: after
2152+ // a notify, for a consumer that sizes it in `onChange`, and in
2153+ // `_willUpdate`. The element cannot clamp below its own 0, which is the
2154+ // hold in layout coordinates.
2155+ private _noteClampMax = ( ) => {
2156+ if ( ! this . _awaitingCommandReadBack ( ) ) return
2157+ const elementMax = this . _elementMaxScrollOffset ( )
2158+ if ( elementMax === null ) return
2159+ const write = this . pendingWrite !
2160+ const max = Math . max ( elementMax , 0 ) + this . _holdOffset
2161+ if ( max >= write . target ) return
2162+ const lowMaxes = ( write . lowMaxes ??= [ ] )
2163+ if ( lowMaxes [ lowMaxes . length - 1 ] !== max ) lowMaxes . push ( max )
2164+ }
2165+
21672166 // The element's scroll range in layout coordinates: while a correction is
21682167 // held the sizer is short by the hold, so the element's own max is too.
21692168 private getMaxScrollOffset = ( ) =>
0 commit comments