From 33e5f0bcd139f66a179aefacba0b0debd8ab86e0 Mon Sep 17 00:00:00 2001 From: Yaowei Zheng Date: Fri, 24 Jul 2026 00:00:52 +0800 Subject: [PATCH] fix(web): sidebar list expansion made the whole page scroll (#53) Co-authored-by: Claude Fable 5 --- packages/web/e2e/paging.spec.mjs | 17 +++++++++++++++++ packages/web/e2e/run.sh | 4 +++- packages/web/src/components/layout/sidebar.tsx | 9 +++++++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/web/e2e/paging.spec.mjs b/packages/web/e2e/paging.spec.mjs index 022e524..b0a1918 100644 --- a/packages/web/e2e/paging.spec.mjs +++ b/packages/web/e2e/paging.spec.mjs @@ -5,6 +5,12 @@ * page, after which all 21 rows are visible and the "更多" row disappears (no more hidden * rows, no more server pages). * + * A list taller than the viewport must scroll INSIDE the sidebar: the document itself + * stays unscrollable before and after "更多". Each row's sr-only Agent name is + * position:absolute, and without a positioned scroller those boxes anchored to the + * initial containing block, stretched the document, and let the whole page scroll (the + * composer could be pushed up, leaving blank space below). + * * Standalone spec: shares one server with the other specs, so it registers its own user * (auto-provisions a default Project) and seeds sessions via the API. */ @@ -59,8 +65,19 @@ test("sidebar shows 20 sessions plus a More row; More loads the 21st and then di const more = sidebar.getByRole("button", { name: "更多" }); await expect(more).toBeVisible(); + // The 20-row list already exceeds the 720px viewport: it must scroll inside the + // sidebar, never stretch the document (the sr-only regression described up top). + const docScrollable = () => + page.evaluate( + () => document.documentElement.scrollHeight > document.documentElement.clientHeight + 1, + ); + expect(await docScrollable(), "document scrollable before 更多").toBe(false); + // More: raises the display cap and fetches the next server page → all 21 rows, no More left. await more.click(); await expect(rows).toHaveCount(TOTAL); await expect(more).toHaveCount(0); + + // Still only the sidebar scrolls after the list grew past one page. + expect(await docScrollable(), "document scrollable after 更多").toBe(false); }); diff --git a/packages/web/e2e/run.sh b/packages/web/e2e/run.sh index aead72b..eeafa69 100644 --- a/packages/web/e2e/run.sh +++ b/packages/web/e2e/run.sh @@ -7,7 +7,9 @@ ROOT="$(cd "$HERE/../../.." && pwd)" DATA="$(mktemp -d)" MOCK_PORT="${MOCK_PORT:-8931}" SRV_PORT="${SRV_PORT:-8930}" -export BASE_URL="http://127.0.0.1:$SRV_PORT" +# localhost, not 127.0.0.1: since the Workspace-preview split the server canonicalizes the +# App onto localhost and reserves 127.0.0.1 as the preview host, where /api answers 401. +export BASE_URL="http://localhost:$SRV_PORT" export MOCK_URL="http://127.0.0.1:$MOCK_PORT" cleanup() { diff --git a/packages/web/src/components/layout/sidebar.tsx b/packages/web/src/components/layout/sidebar.tsx index 086d7ce..c1808f0 100644 --- a/packages/web/src/components/layout/sidebar.tsx +++ b/packages/web/src/components/layout/sidebar.tsx @@ -704,8 +704,13 @@ export function Sidebar({ ))} - {/* Session area (scrollable): grouped by Workspace (default) or by Agent */} -
+ {/* Session area (scrollable): grouped by Workspace (default) or by Agent. + relative: the scroller acts as its own containing block, so absolute descendants + (each row's sr-only Agent name) anchor and scroll inside it — anchored to the + initial containing block instead, rows past the fold would bypass this + overflow-y-auto and stretch the **document**, so expanding "More" / a source + folder made the whole page scroll (composer pushed up, blank space below). */} +
{/* Section header: list label + grouping-mode toggle (the choice persists in localStorage) */}