diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 963961a744..d5a1f9dbe9 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -192,6 +192,11 @@ scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) transparent; } + /* App shell: lock the document to the viewport; each route owns its scroll container. */ + html, + body { + @apply h-full overflow-hidden overscroll-none; + } body { @apply bg-background text-foreground; } diff --git a/apps/web/components/settings/settings-layout-client.tsx b/apps/web/components/settings/settings-layout-client.tsx index e57c20a6d0..4e8ef689ec 100644 --- a/apps/web/components/settings/settings-layout-client.tsx +++ b/apps/web/components/settings/settings-layout-client.tsx @@ -116,7 +116,10 @@ function SettingsShell({ /> {/* Scroll the content, not the topbar: min-h-0 lets this flex child shrink below its content height so overflow-y-auto can take effect. */} -
+
{children}
diff --git a/apps/web/e2e/tests/automations-settings.spec.ts b/apps/web/e2e/tests/automations-settings.spec.ts index eefd30d9c0..e2b71d8554 100644 --- a/apps/web/e2e/tests/automations-settings.spec.ts +++ b/apps/web/e2e/tests/automations-settings.spec.ts @@ -227,6 +227,32 @@ test.describe("Automations settings page", () => { }); }); + test("create page keeps scrolling inside the settings pane", async ({ testPage, seedData }) => { + const automations = new AutomationsPage(testPage, seedData.workspaceId); + await automations.gotoNew(); + + const rootScrollStyles = await testPage.evaluate(() => ({ + htmlOverflowY: getComputedStyle(document.documentElement).overflowY, + htmlOverscrollY: getComputedStyle(document.documentElement).overscrollBehaviorY, + bodyOverflowY: getComputedStyle(document.body).overflowY, + bodyOverscrollY: getComputedStyle(document.body).overscrollBehaviorY, + })); + expect(rootScrollStyles).toEqual({ + htmlOverflowY: "hidden", + htmlOverscrollY: "none", + bodyOverflowY: "hidden", + bodyOverscrollY: "none", + }); + + const settingsScroller = testPage.getByTestId("settings-scroll-container"); + await expect(settingsScroller).toBeVisible(); + await expect(settingsScroller).toHaveCSS("overflow-y", "auto"); + await expect(settingsScroller).toHaveCSS("overscroll-behavior-y", "contain"); + + await testPage.mouse.wheel(0, 3000); + await expect.poll(() => testPage.evaluate(() => window.scrollY), { timeout: 5_000 }).toBe(0); + }); + test("enable/disable toggle on list page", async ({ testPage, seedData }) => { const automations = new AutomationsPage(testPage, seedData.workspaceId); diff --git a/apps/web/e2e/tests/mobile-automations-scroll.spec.ts b/apps/web/e2e/tests/mobile-automations-scroll.spec.ts new file mode 100644 index 0000000000..e877d50847 --- /dev/null +++ b/apps/web/e2e/tests/mobile-automations-scroll.spec.ts @@ -0,0 +1,51 @@ +import type { Locator, Page } from "@playwright/test"; +import { test, expect } from "../fixtures/test-base"; +import { AutomationsPage } from "../pages/automations-page"; + +/** Swipe up on a scroll container — the touch equivalent of wheel-down at scroll bottom. */ +async function swipeUpOnElement(page: Page, element: Locator): Promise { + const box = await element.boundingBox(); + if (!box) throw new Error("scroll container has no bounding box"); + + const cdp = await page.context().newCDPSession(page); + const centerX = box.x + box.width / 2; + const startY = box.y + box.height - 20; + const endY = box.y + 20; + + await cdp.send("Input.dispatchTouchEvent", { + type: "touchStart", + touchPoints: [{ x: centerX, y: startY }], + }); + for (let i = 1; i <= 8; i++) { + const y = startY + ((endY - startY) * i) / 8; + await cdp.send("Input.dispatchTouchEvent", { + type: "touchMove", + touchPoints: [{ x: centerX, y }], + }); + } + await cdp.send("Input.dispatchTouchEvent", { + type: "touchEnd", + touchPoints: [], + }); +} + +test.describe("Automations settings on mobile", () => { + test("create page does not hand off bottom overscroll to the document", async ({ + testPage, + seedData, + }) => { + const automations = new AutomationsPage(testPage, seedData.workspaceId); + await automations.gotoNew(); + + const settingsScroller = testPage.getByTestId("settings-scroll-container"); + await expect(settingsScroller).toBeVisible(); + await expect(settingsScroller).toHaveCSS("overscroll-behavior-y", "contain"); + + await settingsScroller.evaluate((el) => { + el.scrollTop = el.scrollHeight; + }); + await swipeUpOnElement(testPage, settingsScroller); + + await expect.poll(() => testPage.evaluate(() => window.scrollY), { timeout: 5_000 }).toBe(0); + }); +});