From 133e9377720442c41d46bedd4f085222b29f5290 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 3 Mar 2026 10:53:58 +0000 Subject: [PATCH] fix(ui): pin follow list to bottom on resize --- .../ui/src/components/virtual-follow-list.tsx | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/components/virtual-follow-list.tsx b/packages/ui/src/components/virtual-follow-list.tsx index 945b83ca..f173f92d 100644 --- a/packages/ui/src/components/virtual-follow-list.tsx +++ b/packages/ui/src/components/virtual-follow-list.tsx @@ -449,6 +449,29 @@ export default function VirtualFollowList(props: VirtualFollowListProps) { }) } + let pendingAutoPin = false + function scheduleAutoPinToBottom() { + if (!containerRef) return + if (pendingAutoPin) return + pendingAutoPin = true + const gen = scrollCompensationGen + + // Flush in a microtask so adjustments land before the next paint. + queueMicrotask(() => { + if (gen !== scrollCompensationGen) return + pendingAutoPin = false + if (!containerRef) return + if (!autoScroll()) return + if (anchorLock()) return + + const maxScrollTop = Math.max(containerRef.scrollHeight - containerRef.clientHeight, 0) + if (containerRef.scrollTop !== maxScrollTop) { + containerRef.scrollTop = maxScrollTop + lastKnownScrollTop = maxScrollTop + } + }) + } + function setShellRef(element: HTMLDivElement | null) { shellRef = element || undefined setShellElement(shellRef) @@ -508,14 +531,23 @@ export default function VirtualFollowList(props: VirtualFollowListProps) { let lastActiveState = false createEffect(() => { const active = isActive() - if (active) { - resolvePendingActiveScroll() - if (!lastActiveState && autoScroll()) { - requestScrollToBottom(true) + if (active) { + resolvePendingActiveScroll() + if (!lastActiveState && autoScroll()) { + requestScrollToBottom(true) + + // When switching back to a cached session pane, items can mount/measure + // after the initial scroll jump. Re-pin once layout settles so the + // viewport stays at the bottom. + requestAnimationFrame(() => { + requestAnimationFrame(() => { + scheduleAutoPinToBottom() + }) + }) + } + } else if (autoScroll()) { + pendingActiveScroll = true } - } else if (autoScroll()) { - pendingActiveScroll = true - } lastActiveState = active }) @@ -746,6 +778,13 @@ export default function VirtualFollowList(props: VirtualFollowListProps) { onHeightChange={(nextHeight, previousHeight) => { const delta = nextHeight - previousHeight + // Follow mode: keep the viewport pinned to the bottom as + // items mount/measure and change height. + if (delta && autoScroll() && !anchorLock()) { + scheduleAutoPinToBottom() + return + } + // Key-anchored mode: keep the target key in view when // items above it mount/measure and shift layout. if (anchorLock() && !autoScroll()) {