diff --git a/ui/src/components/Layout.test.tsx b/ui/src/components/Layout.test.tsx index 0a550ca9..5a4967b3 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 390b9f44..44af0a81 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -469,7 +469,11 @@ export function Layout() {