diff --git a/src/client/sidebar.module.css b/src/client/sidebar.module.css index f65ec26..4220a91 100644 --- a/src/client/sidebar.module.css +++ b/src/client/sidebar.module.css @@ -37,6 +37,15 @@ inset: 0; z-index: 25; pointer-events: none; + /* Clip at the viewport edge: collapsed panels slide off-screen with + `transform: translate(102%)`, and a transformed element still + contributes to its ancestors' scrollable overflow — without this the + hidden panels extended the document's scrollable area, making the whole + page draggable left-right and up-down while any panel was collapsed. + The host is exactly viewport-sized (inset: 0), so the clip is invisible + until a panel slides in from the edge; internal scroll containers and + the app's fixed overlay stack are unaffected. */ + overflow: hidden; } :global([data-dsh-panel-host][data-dsh-panel-host-degraded]) { diff --git a/tests/e2e/mount.e2e.ts b/tests/e2e/mount.e2e.ts index 8bf81e8..52a075b 100644 --- a/tests/e2e/mount.e2e.ts +++ b/tests/e2e/mount.e2e.ts @@ -191,6 +191,26 @@ test('plugin mounts into the DSH shell and survives a built-in tab sweep', async const tabBar = sidebar.locator('[title]') await expect(tabBar.first()).toBeAttached({ timeout: 90_000 }) + // Regression: with the panels still COLLAPSED, the document must not grow + // beyond the viewport. Collapsed panels are slid off-screen with + // `transform: translate(102%)`, and a transformed element still + // contributes to its ancestors' scrollable overflow — unclipped, the + // hidden panels extended the document's scroll area and the whole page + // became draggable left-right and up-down. The panel host clips at the + // viewport edge (overflow: hidden), so scrollWidth/scrollHeight must stay + // within the viewport here. (Assert <= not == : a classic scrollbar + // narrows clientWidth, so scrollWidth may sit slightly under innerWidth.) + await expect + .poll( + () => + page.evaluate(() => ({ + overflowX: document.documentElement.scrollWidth <= window.innerWidth, + overflowY: document.documentElement.scrollHeight <= window.innerHeight, + })), + { timeout: 30_000 }, + ) + .toEqual({ overflowX: true, overflowY: true }) + // openByDefault defaults OFF: a fresh session's panel starts collapsed. // Expand it through the toggle cluster before the layout push can apply. const expandButton = sidebar.getByRole('button', { name: 'Expand sidebar' })