Skip to content

refactor(scroll): content anchor as the single source of truth for restore#774

Merged
mremond merged 1 commit into
mainfrom
mr/keen-yonath-b71aaa
Jun 30, 2026
Merged

refactor(scroll): content anchor as the single source of truth for restore#774
mremond merged 1 commit into
mainfrom
mr/keen-yonath-b71aaa

Conversation

@mremond

@mremond mremond commented Jun 30, 2026

Copy link
Copy Markdown
Member

Makes the message-list scroll restore rely on the rendering-independent content anchor ({ messageId, fraction }, re-derived from each row's current measured height) as the single source of truth, and removes the exact-saved-scrollTop "fast-path".

Why

The old fast-path restored the saved scrollTop directly when the saved scrollHeight (±4px) and clientWidth were unchanged. That pixel proxy only stood in for "layout byte-identical" — fragile. Font-size and view-density changes were handled only incidentally (caught by the total-height delta), and a relayout that left the height coincidentally unchanged could mis-fire onto a stale pixel. The fraction anchor is correct in every case, including identical layout.

Changes

  • restoreSavedPosition: remove the fast-path; the anchor is tried first and unconditionally. Saved scrollTop is demoted to a last-resort fallback used only when there is no usable anchor. (Integrates with main's pinVirtualizedAnchor / requestAnchorAroundLoad.)
  • scrollStateManager: drop the now-dead stored scrollHeight/clientWidth and the getSavedScrollHeight/getSavedClientWidth getters; scrollHeight/clientHeight remain only as transient inputs for the wasAtBottom decision.
  • Rewrite the affected virtualized restore unit tests to assert the restore consults the anchor (getOffsetForMessageId/scrollToIndex) rather than a saved pixel — the fraction can't be unit-tested in jsdom (no layout).
  • Add scroll-invariants invariant-12: a viewport-width + view-density change while away holds the reading anchor on return (does not snap to bottom; same message stays at the fold).

Verification

  • npm run typecheck
  • app vitest ✅ (one unrelated pre-existing FloatingDateHeader flake, confirmed on clean main)
  • npm run test:scroll (chromium): 22 passed

…for restore

Remove the exact-saved-scrollTop fast-path from restoreSavedPosition so the
content anchor (message id + fraction, re-derived from each row's CURRENT
measured height) governs every restore. Font-size, view-density and viewport
-width changes are now handled by the same correct path instead of being caught
only incidentally by a total-height delta; the saved scrollTop is demoted to a
last-resort fallback used only when there is no usable anchor.

- Drop the now-dead stored scrollHeight/clientWidth from ScrollState and remove
  getSavedScrollHeight/getSavedClientWidth; saveScrollPosition keeps
  scrollHeight/clientHeight only as transient inputs for the wasAtBottom decision.
- Rewrite the affected virtualized restore unit tests to assert the restore
  CONSULTS the anchor (getOffsetForMessageId / scrollToIndex) rather than a pixel.
- Add scroll-invariants invariant-12: a width + density change while away holds
  the reading anchor on return (chromium).
@mremond mremond added this to the 0.17.0 milestone Jun 30, 2026
@mremond mremond merged commit 1df545e into main Jun 30, 2026
3 checks passed
@mremond mremond deleted the mr/keen-yonath-b71aaa branch June 30, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant