From 5f572f1b92c74a2430fbfe42f9fc2604a44e2a6f Mon Sep 17 00:00:00 2001 From: Bergasha Date: Fri, 28 Aug 2026 10:59:48 +1000 Subject: [PATCH] fix(board): reserve and scale space for collapsible container header (#6727) --- .../sections/grid/section-grid.module.css | 35 +++++++++++++------ .../board/sections/grid/section-grid.tsx | 23 +++++++++++- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/apps/nextjs/src/components/board/sections/grid/section-grid.module.css b/apps/nextjs/src/components/board/sections/grid/section-grid.module.css index d24f5d776..df8a2c3ea 100644 --- a/apps/nextjs/src/components/board/sections/grid/section-grid.module.css +++ b/apps/nextjs/src/components/board/sections/grid/section-grid.module.css @@ -3,6 +3,22 @@ overflow: visible; } +/* --board-grid-drag-height inherits down the DOM tree like any custom property. A capped + section's own GridPreviewLayer effect correctly avoids setting it on itself (see + grid-preview-layer.tsx), but that does nothing to stop it inheriting the value an + *uncapped* ancestor section (e.g. the board's root section) legitimately sets on itself + while growing during a drag happening anywhere else on the board - any container (not just + a scrollable one - a non-scrollable, e.g. collapsible, container is just as capped) sits + inside that section's DOM tree, so the stray value cascades straight through and silently + defeats the `var(--board-grid-drag-height, ...px)` fallback in section-grid.tsx (including + its collapsibleHeaderInset adjustment), which only kicks in when the property is entirely + unset, not merely inherited. Reset it here so every container can never inherit a stray + value from anywhere above it, no matter what any ancestor's own drag state is doing. + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables#custom_properties_and_inheritance */ +.viewport[data-section-kind="container"] { + --board-grid-drag-height: initial; +} + /* Chained with .viewport (rather than a standalone .scrollableViewport rule) so this always wins on specificity, not source order. GridEditor loads as a separate async chunk in edit mode, and a board mutation can trigger its stylesheet to re-register - at equal specificity, @@ -11,17 +27,14 @@ .viewport.scrollableViewport { overflow-x: hidden; overflow-y: auto; - /* --board-grid-drag-height inherits down the DOM tree like any custom property. A capped - section's own GridPreviewLayer effect correctly avoids setting it on itself (see - grid-preview-layer.tsx), but that does nothing to stop it inheriting the value an - *uncapped* ancestor section (e.g. the board's root section) legitimately sets on itself - while growing during a drag happening anywhere else on the board - a nested scrollable - container sits inside that section's DOM tree, so the stray value cascades straight - through and silently defeats the `var(--board-grid-drag-height, ...px)` fallback in - section-grid.tsx, which only kicks in when the property is entirely unset, not merely - inherited. Reset it here so this element can never inherit a stray value from anywhere - above it, no matter what any ancestor's own drag state is doing. - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables#custom_properties_and_inheritance */ + /* The grid content inside has a fixed pixel width matching this viewport's full + outer width (needed for the drag/resize math), so there's no room to reserve + for a scrollbar without it eating into - or overlapping - the content on + whichever side it renders. Hiding the native scrollbar track entirely (wheel/ + touch/drag scrolling still works) avoids that instead of trying to reserve + space for it. */ + scrollbar-width: none; + -ms-overflow-style: none; --board-grid-drag-height: initial; } diff --git a/apps/nextjs/src/components/board/sections/grid/section-grid.tsx b/apps/nextjs/src/components/board/sections/grid/section-grid.tsx index a7401f0e9..23a584d6b 100644 --- a/apps/nextjs/src/components/board/sections/grid/section-grid.tsx +++ b/apps/nextjs/src/components/board/sections/grid/section-grid.tsx @@ -148,8 +148,25 @@ export const SectionGrid = ({ // card and visually spills past its right/bottom edges. 10 must match that CSS rule's inset. const effectiveCanvasScale = Number.isFinite(canvasScale) && canvasScale > 0 ? canvasScale : 1; const outerCardInset = section.kind === "container" ? (2 * 10) / effectiveCanvasScale : 0; + // A collapsible container's toggle bar (see the `containerToggle` Button in + // container-section.tsx) is an absolutely positioned overlay sitting on top of this grid. + // Its height comes from a Mantine size prop, which - like spacing/font-size - is compensated + // by --mantine-scale to render at a constant physical size regardless of board zoom (see + // scaled-board-canvas.module.css). That makes it behave like the outerCardInset gap above, + // not like the grid's own un-compensated logical-pixel math, so it needs the same + // effectiveCanvasScale conversion - a plain, unconverted subtraction would under-reserve on + // a zoomed-out board and leave the header overlapping the content. Without reserving it at + // all, the header bar covers the first row of the container's content instead of sitting + // above it. Only the vertical axis is affected - the toggle spans the full width already. + const collapsibleHeaderInset = + section.kind === "container" && section.options.collapsible + ? CONTAINER_HEADER_HEIGHT / effectiveCanvasScale + : 0; const logicalWidth = getLogicalGridSize(columnCount) - outerCardInset; - const viewportHeight = getLogicalGridSize(viewportRowCount) - outerCardInset; + const viewportHeight = Math.max( + 1, + getLogicalGridSize(viewportRowCount) - outerCardInset - collapsibleHeaderInset, + ); // Items are positioned in a coordinate space sized to the *un-inset* column/row count // (fullGridWidth/Height below - see getLogicalItemStyle), but logicalWidth/Height above are // deliberately smaller by outerCardInset to match the container's actual visible card size. @@ -283,6 +300,7 @@ export const SectionGrid = ({ { width: logicalWidth, height: `var(--board-grid-drag-height, ${viewportHeight}px)`, + marginTop: collapsibleHeaderInset || undefined, "--board-item-radius": `var(--mantine-radius-${board.itemRadius})`, } as CSSProperties } @@ -317,6 +335,9 @@ export const SectionGrid = ({ ); }; +// Matches the collapsible container toggle's h={24} in container-section.tsx. +const CONTAINER_HEADER_HEIGHT = 24; + const INTERACTIVE_GRID_SELECTOR = 'a,button,input,textarea,select,option,[contenteditable="true"],[role="button"],[data-grid-no-drag]'; const EDIT_ACTIVATION_KEYS = new Set(["Enter", " ", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]);