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
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 |
|---|---|---|
|
|
@@ -890,7 +890,28 @@ export class Virtualizer< | |
|
|
||
| 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 = this.scrollRect !== null ? this.getSize() : null | ||
| const wasAtEnd = | ||
| prevSize !== null && | ||
| this.options.anchorTo === 'end' && | ||
| this.scrollState?.behavior !== 'smooth' && | ||
| this.getVirtualDistanceFromEnd() <= this.options.scrollEndThreshold | ||
|
Comment on lines
+899
to
+903
Collaborator
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. A reader scrolling up gets pulled back to the end. If the viewport shrinks while the user is scrolling up but still within scrollEndThreshold, this scrolls them back down. With a window scroller on iOS that happens on every scroll-up gesture near the end: the URL bar reappears, innerHeight shrinks, and the adjustment is deferred and applied after touch-end, pulling them down by the bar's height. Scrolling up means they're leaving the end, so I'd skip it: const wasAtEnd = I only added this to the rect path and left resizeItem as it is. Let me know if you see a case where this is wrong, or if you think resizeItem should get the same guard for consistency. If you keep it, the docs line could note that scrolling up opts out. |
||
| this.scrollRect = rect | ||
| if (wasAtEnd && prevSize > this.getSize()) { | ||
| // 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() | ||
| }), | ||
| ) | ||
|
|
||
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.
Swapping the scroll element carries the pin over to the new element, cleanup() doesn't reset scrollRect or scrollOffset, and observeElementRect reports the new element's size right away, before observeElementOffset is set up.
So when getScrollElement() returns a new, shorter element, this compares the old element's size and offset, and writes scrollTo(300, { adjustments: 50 }) to the new one.
The first report for each element has nothing to compare against, so I'd skip it: