Skip to content

Commit 563d202

Browse files
adrians5jclaude
andauthored
fix: stack Grid columns via CSS media query (#5628)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5674cf6 commit 563d202

3 files changed

Lines changed: 81 additions & 18 deletions

File tree

packages/website-builder-react/src/editorComponents/Grid.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react";
22
import type { ComponentProps, ComponentPropsWithChildren } from "~/types.js";
3+
import { createGridClass, createGridStackingCss } from "./gridStyles.js";
34

45
export const GridColumnComponent = ({
56
inputs
@@ -23,7 +24,7 @@ type GridProps = ComponentProps<{
2324
reverseWhenStacked?: boolean;
2425
}>;
2526

26-
export const GridComponent = ({ inputs, styles, breakpoint }: GridProps) => {
27+
export const GridComponent = ({ inputs, styles, element }: GridProps) => {
2728
const { gridLayout = "12", columns, columnGap, stackAtBreakpoint, reverseWhenStacked } = inputs;
2829
const rowConfig = gridLayout.split("-").map(size => parseInt(size));
2930
const rows: Column[][] = [];
@@ -36,22 +37,23 @@ export const GridComponent = ({ inputs, styles, breakpoint }: GridProps) => {
3637
// Number of pixels we need to subtract from each cell to ensure they fit in the grid with column gap
3738
const cellWidthReduction = columnGap ? columnGap - columnGap / rowConfig.length : 0;
3839

39-
const stackColumns = breakpoint === stackAtBreakpoint;
40+
const gridClass = createGridClass(element.id);
4041

41-
if (stackColumns) {
42-
styles.flexDirection = reverseWhenStacked ? "column-reverse" : "column";
43-
}
42+
// Columns stack via a CSS media query (see createGridStackingCss).
43+
const stackCss = createGridStackingCss({ gridClass, stackAtBreakpoint, reverseWhenStacked });
4444

4545
return (
46-
<div style={styles}>
46+
<div className={gridClass} style={styles}>
47+
{/*
48+
A media query can't be expressed in an inline `style` attribute (that only
49+
holds declarations, not at-rules), so the stacking CSS is emitted as a
50+
scoped <style> tag. A <style> is `display:none` and valid inside <body>,
51+
so it doesn't affect the grid's flex layout.
52+
*/}
53+
{stackCss ? <style dangerouslySetInnerHTML={{ __html: stackCss }} /> : null}
4754
{rows.map(columns => {
4855
return columns.map((column, i) => (
49-
<Span
50-
key={i}
51-
stackColumns={stackColumns}
52-
size={rowConfig[i]}
53-
reductionInPx={cellWidthReduction}
54-
>
56+
<Span key={i} size={rowConfig[i]} reductionInPx={cellWidthReduction}>
5557
<GridColumnComponent key={i} inputs={{ children: column.children }} />
5658
</Span>
5759
));
@@ -63,15 +65,16 @@ export const GridComponent = ({ inputs, styles, breakpoint }: GridProps) => {
6365
interface SpanProps {
6466
size: number;
6567
reductionInPx: number;
66-
stackColumns: boolean;
6768
children: React.ReactNode;
6869
}
6970

70-
const Span = ({ size, children, reductionInPx, stackColumns }: SpanProps) => {
71-
const width = stackColumns ? "100%" : `calc(${(size / 12) * 100}% - ${reductionInPx}px)`;
71+
const Span = ({ size, children, reductionInPx }: SpanProps) => {
72+
// Base (row) width. The Grid's media query overrides this to 100% when stacked.
73+
const width = `calc(${(size / 12) * 100}% - ${reductionInPx}px)`;
7274

7375
return (
7476
<div
77+
className="wb-grid-col"
7578
style={{
7679
flex: `0 0 ${width}`,
7780
maxWidth: width,
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { viewportManager } from "@webiny/website-builder-sdk";
2+
3+
/**
4+
* Builds a stable, SSR-consistent class scoped to a single grid instance
5+
* (e.g. `wb-grid-<id>`), sanitized to a valid CSS class name.
6+
*/
7+
export const createGridClass = (elementId: string): string => {
8+
return `wb-grid-${String(elementId).replace(/[^a-zA-Z0-9_-]/g, "-")}`;
9+
};
10+
11+
interface CreateGridStackingCssParams {
12+
/** Scoped class applied to the grid container (from `createGridClass`). */
13+
gridClass: string;
14+
/** Breakpoint name at (and below) which columns stack, e.g. "mobile". */
15+
stackAtBreakpoint?: string;
16+
/** Reverse the visual order of columns when stacked. */
17+
reverseWhenStacked?: boolean;
18+
}
19+
20+
/**
21+
* Builds the media query that stacks a grid's columns at (and below) the given
22+
* breakpoint's width.
23+
*
24+
* Stacking is expressed as CSS rather than JS so the layout is correct straight
25+
* from the SSR HTML — no hydration, no viewport JS, no flash — the browser
26+
* applies it by the real viewport width. The width comes from the theme's
27+
* breakpoint definition (populated on both server and client via ContentSdk.init).
28+
*
29+
* Returns an empty string when no stacking breakpoint is configured (or the
30+
* named breakpoint is unknown), in which case the grid never stacks.
31+
*/
32+
export const createGridStackingCss = ({
33+
gridClass,
34+
stackAtBreakpoint,
35+
reverseWhenStacked
36+
}: CreateGridStackingCssParams): string => {
37+
if (!stackAtBreakpoint) {
38+
return "";
39+
}
40+
41+
const breakpoint = viewportManager
42+
.getViewport()
43+
.breakpoints.find(bp => bp.name === stackAtBreakpoint);
44+
45+
if (!breakpoint) {
46+
return "";
47+
}
48+
49+
const direction = reverseWhenStacked ? "column-reverse" : "column";
50+
51+
// `!important` overrides the inline base (row) styles set on the elements.
52+
return [
53+
`@media (max-width: ${breakpoint.maxWidth}px) {`,
54+
` .${gridClass} { flex-direction: ${direction} !important; }`,
55+
` .${gridClass} > .wb-grid-col { flex: 0 0 100% !important; max-width: 100% !important; }`,
56+
`}`
57+
].join("\n");
58+
};

packages/website-builder-sdk/src/ContentSdk.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,11 @@ export class ContentSdk implements IContentSdk, IRedirects {
9595

9696
const theme = Theme.from(config.theme ?? {});
9797

98-
if (environment.isClient()) {
99-
viewportManager.setBreakpoints(theme.breakpoints);
100-
}
98+
// Populate breakpoints on the server too, so SSR can generate
99+
// breakpoint-aware CSS (e.g. Grid stacking media queries) from the
100+
// real theme widths. The resize listener stays client-only (it's
101+
// guarded in the ViewportManager constructor).
102+
viewportManager.setBreakpoints(theme.breakpoints);
101103

102104
let editingSdk;
103105
if (environment.isEditing()) {

0 commit comments

Comments
 (0)