From cc71ab8cd96ae652f5a227b22b147f75f503101f Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 3 Aug 2026 16:46:43 +0200 Subject: [PATCH] fix: contain board canvas overflow --- .../[locale]/boards/(content)/_client.module.css | 2 +- .../src/app/[locale]/boards/(content)/_client.tsx | 13 +++++++------ .../board/layout/scaled-board-canvas.module.css | 1 + apps/nextjs/src/components/layout/header.tsx | 2 +- e2e/board-grid.spec.ts | 5 ++++- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css b/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css index 3dbf87f39..158ad16ae 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.module.css @@ -9,7 +9,7 @@ } .gutter { - position: sticky; + position: relative; top: 0; align-self: start; padding-top: 24px; diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx index c710ea24c..5edc63d6f 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useEffect, useState } from "react"; import { Box } from "@mantine/core"; import { useCurrentLayout, useInitialViewportWidth, useRequiredBoard } from "@homarr/boards/context"; @@ -24,17 +24,16 @@ import classes from "./_client.module.css"; const APP_SHELL_INLINE_PADDING = 32; const useFixedBoardGutters = () => { - const columnsRef = useRef(null); + const [columns, setColumns] = useState(null); useEffect(() => { - const columns = columnsRef.current; if (!columns) return; let frame = 0; const update = () => { frame = 0; const columnsRect = columns.getBoundingClientRect(); - const headerBottom = document.querySelector("header")?.getBoundingClientRect().bottom ?? 0; + const headerBottom = document.querySelector("[data-app-shell-header]")?.getBoundingClientRect().bottom ?? 0; const stickyTop = Math.max(0, headerBottom + 16); for (const gutter of columns.querySelectorAll("[data-board-gutter]")) { @@ -62,6 +61,7 @@ const useFixedBoardGutters = () => { const resizeObserver = new ResizeObserver(scheduleUpdate); resizeObserver.observe(columns); document.addEventListener("scroll", scheduleUpdate, { capture: true, passive: true }); + window.addEventListener("scroll", scheduleUpdate, { passive: true }); window.addEventListener("resize", scheduleUpdate, { passive: true }); scheduleUpdate(); @@ -69,11 +69,12 @@ const useFixedBoardGutters = () => { window.cancelAnimationFrame(frame); resizeObserver.disconnect(); document.removeEventListener("scroll", scheduleUpdate, { capture: true }); + window.removeEventListener("scroll", scheduleUpdate); window.removeEventListener("resize", scheduleUpdate); }; - }, []); + }, [columns]); - return columnsRef; + return setColumns; }; export const ClientBoard = () => { diff --git a/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css b/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css index 6ba183ba6..6c98e40a2 100644 --- a/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css +++ b/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css @@ -3,6 +3,7 @@ max-width: 100%; min-width: 0; overflow: visible; + overflow-x: auto; overscroll-behavior-inline: contain; } diff --git a/apps/nextjs/src/components/layout/header.tsx b/apps/nextjs/src/components/layout/header.tsx index e9d93f7fc..4d206978d 100644 --- a/apps/nextjs/src/components/layout/header.tsx +++ b/apps/nextjs/src/components/layout/header.tsx @@ -18,7 +18,7 @@ interface Props { export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => { return ( - + {hasNavigation && } diff --git a/e2e/board-grid.spec.ts b/e2e/board-grid.spec.ts index f8c99e44b..0195c02cc 100644 --- a/e2e/board-grid.spec.ts +++ b/e2e/board-grid.spec.ts @@ -177,6 +177,7 @@ describe("Board grid", () => { const zoomedTileBox = await expectBoundingBoxAsync(logicalTile); expect(zoomedTileBox.width).toBeGreaterThan(normalZoomTileBox.width * 1.25); await expectHorizontalOverflowAsync(canvas); + await expectDocumentNotHorizontallyScrollableAsync(page); const overflowRailY = (await expectBoundingBoxAsync(rail)).y; await page.evaluate(() => window.scrollTo({ top: 300, behavior: "auto" })); await expect.poll(async () => (await expectBoundingBoxAsync(rail)).y).toBeGreaterThanOrEqual(0); @@ -348,6 +349,8 @@ describe("Board grid", () => { await page.setViewportSize({ width: 600, height: 1200 }); await expect(canvas).toHaveAttribute("data-canvas-scale", String(minimumCanvasScale)); + await expectHorizontalOverflowAsync(canvas); + await expectDocumentNotHorizontallyScrollableAsync(page); for (const direction of ["n", "ne", "e", "se", "s", "sw", "w", "nw"] as const) { await expectTargetSizeAsync(firstItem.locator(`:scope > [data-grid-resize-handle="${direction}"]`)); } @@ -823,8 +826,8 @@ describe("Board grid", () => { const stickyRailY = (await expectBoundingBoxAsync(rail)).y; await page.evaluate(() => window.scrollTo({ top: 500, behavior: "auto" })); + await expect.poll(async () => (await expectBoundingBoxAsync(rail)).y).toBeGreaterThanOrEqual(0); const scrolledRailY = (await expectBoundingBoxAsync(rail)).y; - expect(scrolledRailY).toBeGreaterThanOrEqual(0); expect(scrolledRailY).toBeLessThanOrEqual(stickyRailY + 0.5); await page.evaluate(() => window.scrollTo({ top: 0, behavior: "auto" }));