diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 340d9a9c8eb..718bdfb10f7 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -538,7 +538,10 @@ export default function TaskPage(): React.JSX.Element { useLayoutEffect(() => { const target = pendingGithubScrollRestoreRef.current - if (target === null || !pages[currentPage]) { + // Start observing as soon as the list mounts; its page rows may be committed + // by a later render, and the restore helper will retry when they appear. + // Keep the target armed while the detail route is transitioning and the list is still mounted. + if (target === null || pageData.openGitHubWorkItem) { return } return startGitHubListScrollRestore({ @@ -559,6 +562,7 @@ export default function TaskPage(): React.JSX.Element { currentPage, dialogWorkItem, githubResumeContextKey, + pageData.openGitHubWorkItem, pages, githubListScrollTopRef, pendingGithubScrollRestoreRef, diff --git a/src/renderer/src/components/task-page-github-list-scroll-restore.test.ts b/src/renderer/src/components/task-page-github-list-scroll-restore.test.ts index d094973170f..9f557a10b7d 100644 --- a/src/renderer/src/components/task-page-github-list-scroll-restore.test.ts +++ b/src/renderer/src/components/task-page-github-list-scroll-restore.test.ts @@ -106,6 +106,28 @@ describe('GitHub task list scroll restore', () => { expect(applied.at(-1)).toBe(360) }) + it('retries when the rows mount after the restore starts', async () => { + const list = createScrollList(0) + list.element.removeChild(list.rows) + const pendingRestoreRef = ref(360) + const restoreWriteRef = ref(null) + + startGitHubListScrollRestore({ + target: 360, + scrollElementRef: ref(list.element), + pendingRestoreRef, + restoreWriteRef, + onScrollTopApplied: () => {} + }) + + list.setMaxScrollTop(900) + list.element.append(list.rows) + await Promise.resolve() + + expect(list.element.scrollTop).toBe(360) + expect(pendingRestoreRef.current).toBeNull() + }) + it('keeps the remembered offset armed when the list never becomes tall enough', () => { const list = createScrollList(0) const pendingRestoreRef = ref(360) diff --git a/src/renderer/src/components/task-page/github/github-list-scroll-restore.ts b/src/renderer/src/components/task-page/github/github-list-scroll-restore.ts index 6596674c588..0cc4db99732 100644 --- a/src/renderer/src/components/task-page/github/github-list-scroll-restore.ts +++ b/src/renderer/src/components/task-page/github/github-list-scroll-restore.ts @@ -40,6 +40,7 @@ export function startGitHubListScrollRestore({ restoreWriteRef.current = null let frame: number | null = null let observer: ResizeObserver | null = null + let mutationObserver: MutationObserver | null = null const stop = (): void => { if (frame !== null) { window.cancelAnimationFrame(frame) @@ -47,6 +48,8 @@ export function startGitHubListScrollRestore({ } observer?.disconnect() observer = null + mutationObserver?.disconnect() + mutationObserver = null } const restore = (): void => { const element = scrollElementRef.current @@ -72,6 +75,10 @@ export function startGitHubListScrollRestore({ for (const child of scrollElement.children) { observer.observe(child) } + // Rows can mount after this layout effect (for example when a cached page settles), + // so observe child-list changes to cover elements that were not present initially. + mutationObserver = new MutationObserver(restore) + mutationObserver.observe(scrollElement, { childList: true, subtree: true }) restore() if (pendingRestoreRef.current === target) { frame = window.requestAnimationFrame(restore)