Repository navigation
fix(virtual-core): keep an end-pinned viewport pinned when the scroll element shrinks #1298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
aaaa7bf
df83a48
215d5a9
c2b291c
9dabb08
a225d15
db1f91b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@tanstack/virtual-core': patch | ||
| --- | ||
|
|
||
| With `anchorTo: 'end'`, keep an end-pinned viewport pinned when the scroll element shrinks, for example when the window is resized or the app changes its height. Previously the browser kept `scrollTop`, leaving the last items below the fold. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1116,9 +1116,45 @@ export class Virtualizer< | |
| this.observer.observe(cached) | ||
| }) | ||
|
|
||
| // The first report for a new element has nothing to compare against: | ||
| // `scrollRect` and `scrollOffset` still describe the previous element, | ||
| // and its offset observer is not set up yet. | ||
| let isFirstRect = true | ||
| this.unsubs.push( | ||
| this.options.observeElementRect(this, (rect) => { | ||
| // With anchorTo: 'end', keep an end-pinned viewport pinned when the | ||
| // scroll element shrinks, as resizeItem does when an item grows. The | ||
| // browser keeps scrollTop, so the end would otherwise drop below the | ||
| // fold. Judge "pinned" against the size before this change, and | ||
| // never scroll past the element's real end. | ||
| const prevSize = | ||
| !isFirstRect && this.scrollRect !== null ? this.getSize() : null | ||
| isFirstRect = false | ||
| // A reader scrolling up is leaving the end: don't pull them back | ||
| // (e.g. the iOS toolbar reappearing on scroll-up shrinks a window | ||
| // scroller). | ||
| const wasAtEnd = | ||
| prevSize !== null && | ||
| this.scrollDirection !== 'backward' && | ||
| this.isPinnedToEnd() | ||
| this.scrollRect = rect | ||
| // Before a command's read-back, leave the shrink to the command, as | ||
| // resizeItem does: reconcile retargets an index or end command from | ||
| // the new layout, and an offset command lands where it was written. | ||
| if ( | ||
| wasAtEnd && | ||
| prevSize > this.getSize() && | ||
| !this._awaitingCommandReadBack() | ||
| ) { | ||
| // While a clamped write waits for the sizer to grow, the DOM end | ||
| // is stale; apply the whole shrink and let the retry land it. | ||
| const shrink = prevSize - this.getSize() | ||
| this.applyScrollAdjustment( | ||
| this._clampedAdjustment !== null | ||
| ? shrink | ||
| : Math.min(shrink, this.getDistanceFromEnd()), | ||
| ) | ||
| } | ||
| this.maybeNotify() | ||
|
Comment on lines
+1152
to
1158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }), | ||
| ) | ||
|
|
@@ -2078,10 +2114,7 @@ export class Virtualizer< | |
| const delta = size - itemSize | ||
|
|
||
| if (delta !== 0) { | ||
| const wasAtEnd = | ||
| this.options.anchorTo === 'end' && | ||
| !isSmoothScroll(this.scrollState) && | ||
| this.getVirtualDistanceFromEnd() <= this.options.scrollEndThreshold | ||
| const wasAtEnd = this.isPinnedToEnd() | ||
| const prevTotalSize = wasAtEnd ? this._getLayoutTotalSize() : 0 | ||
| // Default anchoring predicate (used unless the consumer supplies a | ||
| // custom shouldAdjustScrollPositionOnItemSizeChange). | ||
|
|
@@ -2355,6 +2388,13 @@ export class Virtualizer< | |
| return target > maxAtWrite + 0.5 ? { target, maxAtWrite } : null | ||
| } | ||
|
|
||
| // Whether an end-anchored viewport should follow size changes to stay at | ||
| // the end. Shared by item growth (`resizeItem`) and scroll element shrink. | ||
| private isPinnedToEnd = () => | ||
| this.options.anchorTo === 'end' && | ||
| !isSmoothScroll(this.scrollState) && | ||
| this.getVirtualDistanceFromEnd() <= this.options.scrollEndThreshold | ||
|
|
||
| private getVirtualDistanceFromEnd = () => { | ||
| // `_getLayoutTotalSize()` measures the sizer, which starts `scrollMargin` | ||
| // into the scroll content, while `getScrollOffset()` is measured from the | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: TanStack/virtual
Length of output: 15902
Script executed:
Repository: TanStack/virtual
Length of output: 44163
Script executed:
Repository: TanStack/virtual
Length of output: 36161
Script executed:
Repository: TanStack/virtual
Length of output: 22969
Script executed:
Repository: TanStack/virtual
Length of output: 9505
Preserve the end state while the sizer is pending.
If
paddingEnddecreases before the sizer updates, the virtual distance can mark a reader as pinned even when they are away from the DOM end. A simultaneous viewport shrink then moves the reader toward the end. Track the prior end state across pending sizer updates.