fix(react): drain released tail spacer after manual scroll during anchored stream - #11465
fix(react): drain released tail spacer after manual scroll during anchored stream#11465gonzoblasco wants to merge 9 commits into
Conversation
|
@gonzoblasco is attempting to deploy a commit to the shadcn-pro Team on Vercel. A member of the Team first needs to authorize it. |
Addresses review feedback: a manual scroll during an anchored stream can leave the reserved tail spacer stranded if the reply stops growing before it drains (no more resizes to drive the drain). Extract the drain into drainReleasedSpacer and run it on user scroll too, so a scroll to the real bottom reclaims the dead space and re-arms following once the spacer hits zero. Document why setTailSpacerHeight lives on the commands surface.
|
Thanks for the careful review - both points are valid, and the edge case is real. On the The fix extracts the drain into
So if the stream cuts short, a scroll to the real bottom - contentBottom plus the remaining spacer - finishes the job: it reclaims the dead space and re-arms following once the spacer hits zero. I added a test that exercises exactly that scenario (stream ends mid-drain, then a scroll to the bottom drains the residual and re-engages following). On Both changes are in the updated branch (62 message-scroller tests pass, typecheck clean). |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@gonzoblasco thanks for the PR. Unfortunately the new drainReleasedSpacer has a problem that the tests can't see: syncAfterScroll runs on every scroll event, including the programmatic one that places an anchored turn. In a real browser with autoScroll on, every anchored turn gets yanked to the bottom, which is the exact bug we're trying to fix. The jsdom tests pass because jsdom doesn't fire scroll events for programmatic scrolls. Two smaller issues in the same function: On resize, if the reader has scrolled up past the spacer zone, consumed is 0, so the spacer collapses and scrollToEnd yanks them mid-transcript. To get this mergeable, the drain needs to skip the anchored-to-message/settling-jump modes and programmatic scrolls, only hand off to scrollToEnd when the reader is actually at the bottom, use idempotent math on the scroll path — and add a browser-mode test with autoScroll on, since jsdom structurally can't catch this class of bug. Happy to look again after an update! This review was written partly with the help of Fable 5. |
…tently The drain ran on every scroll event, including the programmatic one that places an anchored turn. In a real browser that yanked every anchored turn to the bottom with autoScroll on. Skip the drain while a programmatic scroll is in flight (anchored-to-message, settling-jump, autoscrolling) and recompute the remaining spacer room from absolute geometry on every event so inertial scrolling converges instead of over-draining. Hand off to scrollToEnd only when the reader reaches the real bottom; leave the spacer untouched while the reader is above the spacer zone. Adds browser-mode regression tests: anchored placement does not yank, and a manual scroll during an anchored stream drains the spacer without moving the reader.
|
@shadcn all three points are addressed in 1. Programmatic scrolls no longer drain. 2. Idempotent math. The drain now recomputes the remaining room from absolute geometry on every event - 3. Hand-off only at the real bottom. The drain returns early when the reader is above the spacer zone ( 4. Browser-mode regression tests. Two tests in Verification: jsdom 51/51, browser mode 48/48, typecheck clean. |
Fixes #11125
Problem
With
autoScroll+scrollAnchor(themessage-scroller-streamingexample), a manual scroll during an anchored streaming turn releases the anchor and strands the reserved tail spacer. What happens next depends onautoScroll:autoScrollon: the scroll releases the anchor, follow-mode re-arms, andscrollToEndzeroes the spacer in a single frame - the reserved room collapses and the viewport is yanked to the live edge, even when the reply is still small.autoScrolloff: nothing re-arms follow, so the spacer freezes at its current height - a viewport of dead space is stranded below the reply and never resolves.Either way, a normal reader action (scrolling while a reply streams) destroys the reserved room.
Root cause
Two gaps in the controller:
The re-arm has no spacer condition. In
reconcileFollowMode, the branch that armsfollowing-bottomonly excludessettling-jumpandanchored-to-message. A commit arriving with the mode infree-scrollingand!scrollable.endre-arms anyway. And sincescrollable.endmeasures againstcontentBottom(which excludes the tail spacer), a reader sitting inside the spacer zone readsend: falseand their own gesture flips them back into following. The next chunk callsscrollToEnd-> collapse in a single frame -> the yank.A released spacer is never drained. The tail spacer height is only consumed by
scrollToEndin the follow-output path. When follow releases during a manual scroll, nothing reconciles the spacer back to zero.Fix
spacerHeight === 0inreconcileFollowMode, so a reader still inside the spacer zone cannot re-arm following with their own gesture.handleResize: when not in follow-mode and a spacer is reserved, shrink it toward zero as the reply grows (spacerHeight = max(0, scrollTop + clientHeight - contentBottom)), so the reply fills the reserved room without moving the reader. Follow re-engages once the spacer hits zero at the real bottom.This mirrors how
reanchorToAnchoredMessagealready consumes the spacer in the hold mode, and reuses the existingsetTailSpacerHeight(now exposed fromuseMessageScrollerCommands).Testing