From efd5f0a80f3c7a4eeb54cfcdb5767cc78ea0ef21 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:47:37 +1000 Subject: [PATCH] fix(app): preserve timeline selection autoscroll (#35383) --- .../scroll-interaction.spec.ts | 34 +++++++++++++++++++ .../session/timeline/message-timeline.tsx | 9 +++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts b/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts index 4c2e99ffaa..e67545104d 100644 --- a/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts @@ -48,6 +48,40 @@ test("does not reverse visible rows when the user wheels during shell remeasurem await reportVisualStability(testInfo, "wheel-during-resize", trace, rowPairPlan(regions, 1)) }) +test("keeps moving upward while drag-selecting above the timeline", async ({ page }) => { + await setupTimeline(page, { + messages: history(80), + viewport: { width: 1400, height: 700 }, + reducedMotion: true, + }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + const text = page.getByText("History 79.", { exact: false }) + await expect(text).toBeVisible() + await scroller.evaluate((element) => { + element.dataset.selectionLength = "0" + document.addEventListener("selectionchange", () => { + element.dataset.selectionLength = String( + Math.max(Number(element.dataset.selectionLength), window.getSelection()?.toString().length ?? 0), + ) + }) + }) + const textBox = await text.boundingBox() + const scrollBox = await scroller.boundingBox() + expect(textBox).not.toBeNull() + expect(scrollBox).not.toBeNull() + if (!textBox || !scrollBox) return + + await page.mouse.move(textBox.x + textBox.width - 10, textBox.y + textBox.height / 2) + await page.mouse.down() + await page.mouse.move(textBox.x + 20, scrollBox.y - 120, { steps: 30 }) + + await expect.poll(() => scroller.evaluate((element) => Number(element.dataset.selectionLength))).toBeGreaterThan(0) + await expect + .poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeGreaterThan(500) + await page.mouse.up() +}) + test("does not pull a keyboard-scrolled user during shell remeasurement", async ({ page }, testInfo) => { const shellID = "prt_keyboard_01_shell" const followingID = "prt_keyboard_02_following" diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 9d8d82bcc9..2e2c65af3b 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -598,8 +598,12 @@ export function MessageTimeline(props: { const handleListPointerDown = (event: PointerEvent & { currentTarget: HTMLDivElement }) => { if (!prependLoading) clearPrependAnchor() - if (event.target !== event.currentTarget) return - props.onMarkScrollGesture(event.currentTarget) + props.onMarkScrollGesture(event.target) + } + + const handleListPointerMove = (event: PointerEvent) => { + if (event.buttons !== 1) return + props.onMarkScrollGesture(event.target) } const handleListKeyDown = (event: KeyboardEvent & { currentTarget: HTMLDivElement }) => { @@ -1356,6 +1360,7 @@ export function MessageTimeline(props: { onTouchEnd={handleListTouchEnd} onTouchCancel={handleListTouchEnd} onPointerDown={handleListPointerDown} + onPointerMove={handleListPointerMove} onKeyDown={handleListKeyDown} onScroll={handleListScroll} onClick={props.onAutoScrollInteraction}