fix(react): message-scroller - prevent scroll clamp jitter while an anchored turn streams#11183
Open
devnomic wants to merge 1 commit into
Open
Conversation
Contributor
|
Someone is attempting to deploy a commit to the shadcn-pro Team on Vercel. A member of the Team first needs to authorize it. |
Contributor
|
Can you sign the commits please? See https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits. Thank you. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
While a reply streams below an anchored turn (
defaultScrollPosition="last-anchor"/ a newly appendedscrollAnchoritem), the pinned turn visibly jitters: it shifts down a few pixels and snaps back, repeatedly.The mechanism, confirmed with frame-by-frame instrumentation in a real chat app:
scrollTopsits at the very bottom of the scroll range (scrollTop === scrollHeight - clientHeight).scrollHeightbelow the pinnedscrollTop. The browser clampsscrollTopand the whole transcript shifts down.requestAnimationFrame, so the clamped frame always paints first.Captured trace of one wiggle (2px dip on a pending-marker swap):
The fix
Make it impossible for a dip to clamp, instead of correcting after it does:
ANCHORED_TAIL_SLACK = 128): an anchored placement pads the tail spacer beyond the exact fit, so the pinned position is never at max scroll. The slack is blank space below the fold, invisible at the pinned position. A turn placed with no spacer (it already fills the viewport) stays unpadded: it has natural slack below the fold, and a padded spacer would read as "placed with spacer room" to the autoScroll handoff.max(padded, current)), keepingscrollHeightmonotonic for the whole stream.relaxTailSpacer: leaving the anchor hold via user scroll intent trims the spacer back to the exact fit, so the reader cannot scroll into blank space the stream no longer needs. The held height is always >= the exact one, so the trim itself never clamps.handleResizenow keys off the exact remaining height (recomputed viagetTailSpacerHeight) instead of the styled spacer hitting 0, which a held spacer never does. Same handoff timing as before.Tests
Two new tests:
pads and holds the anchored tail spacer so a transient dip cannot clamp the pinned turnandtrims the held spacer slack on user scroll intent. Full message-scroller suite passes (62/62).Independent of #11182 (different code paths; no conflicts).
Related
One footgun worth knowing about that this PR does not touch: putting
content-visibility: auto+contain-intrinsic-sizeon scroller items breaks anchored placement, because a freshly inserted item is measured at its intrinsic-size placeholder before first paint, so the anchor scroll target and spacer are computed from a wrong layout. Consumers should keep those styles off the last turn's items.