From b765d173d838b90842f2bb78565640a9774c1d33 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Fri, 19 Jun 2026 19:24:06 -0700 Subject: [PATCH] fix(ui): prevent mobile viewport horizontal scroll The mobile layout root had no horizontal-overflow guard, so any stray descendant wider than the screen (a long token, an over-wide element) made the entire viewport scroll sideways. Add overflow-x: clip to the mobile root. Using clip rather than hidden leaves overflow-y computed as visible, preserving native body scrolling and the sticky breadcrumb. Add a Layout regression test asserting the mobile root carries overflow-x-clip and the desktop root carries overflow-clip. Co-Authored-By: Paperclip --- ui/src/components/Layout.test.tsx | 47 +++++++++++++++++++++++++++++++ ui/src/components/Layout.tsx | 6 +++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/ui/src/components/Layout.test.tsx b/ui/src/components/Layout.test.tsx index 0a550ca9869..5a4967b3a02 100644 --- a/ui/src/components/Layout.test.tsx +++ b/ui/src/components/Layout.test.tsx @@ -723,4 +723,51 @@ describe("Layout", () => { root.unmount(); }); }); + + async function renderLayoutRoot(): Promise<{ root: ReturnType; rootEl: HTMLElement }> { + const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + await act(async () => { + root.render( + + + , + ); + }); + await flushReact(); + const rootEl = container.firstElementChild as HTMLElement; + return { root, rootEl }; + } + + it("clips horizontal overflow on the mobile layout root so the viewport can't scroll sideways", async () => { + mockSidebarState.isMobile = true; + mockSidebarState.sidebarOpen = false; + const { root, rootEl } = await renderLayoutRoot(); + + expect(rootEl.tagName).toBe("DIV"); + expect(rootEl.className).toContain("bg-background"); + // The mobile root must clip horizontal overflow to prevent a stray wide + // descendant from making the whole viewport scroll sideways. clip (not + // hidden) keeps overflow-y visible so body scroll keeps working. + expect(rootEl.classList.contains("overflow-x-clip")).toBe(true); + expect(rootEl.classList.contains("overflow-hidden")).toBe(false); + + await act(async () => { + root.unmount(); + }); + }); + + it("clips overflow on the desktop layout root", async () => { + mockSidebarState.isMobile = false; + const { root, rootEl } = await renderLayoutRoot(); + + expect(rootEl.className).toContain("bg-background"); + expect(rootEl.classList.contains("overflow-clip")).toBe(true); + + await act(async () => { + root.unmount(); + }); + }); }); diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 390b9f441c3..44af0a81d8f 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -469,7 +469,11 @@ export function Layout() {