Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions packages/website-builder-react/src/editorComponents/Grid.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import type { ComponentProps, ComponentPropsWithChildren } from "~/types.js";
import { createGridClass, createGridStackingCss } from "./gridStyles.js";

export const GridColumnComponent = ({
inputs
Expand All @@ -23,7 +24,7 @@ type GridProps = ComponentProps<{
reverseWhenStacked?: boolean;
}>;

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

const stackColumns = breakpoint === stackAtBreakpoint;
const gridClass = createGridClass(element.id);

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

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

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

return (
<div
className="wb-grid-col"
style={{
flex: `0 0 ${width}`,
maxWidth: width,
Expand Down
58 changes: 58 additions & 0 deletions packages/website-builder-react/src/editorComponents/gridStyles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { viewportManager } from "@webiny/website-builder-sdk";

/**
* Builds a stable, SSR-consistent class scoped to a single grid instance
* (e.g. `wb-grid-<id>`), sanitized to a valid CSS class name.
*/
export const createGridClass = (elementId: string): string => {
return `wb-grid-${String(elementId).replace(/[^a-zA-Z0-9_-]/g, "-")}`;
};

interface CreateGridStackingCssParams {
/** Scoped class applied to the grid container (from `createGridClass`). */
gridClass: string;
/** Breakpoint name at (and below) which columns stack, e.g. "mobile". */
stackAtBreakpoint?: string;
/** Reverse the visual order of columns when stacked. */
reverseWhenStacked?: boolean;
}

/**
* Builds the media query that stacks a grid's columns at (and below) the given
* breakpoint's width.
*
* Stacking is expressed as CSS rather than JS so the layout is correct straight
* from the SSR HTML — no hydration, no viewport JS, no flash — the browser
* applies it by the real viewport width. The width comes from the theme's
* breakpoint definition (populated on both server and client via ContentSdk.init).
*
* Returns an empty string when no stacking breakpoint is configured (or the
* named breakpoint is unknown), in which case the grid never stacks.
*/
export const createGridStackingCss = ({
gridClass,
stackAtBreakpoint,
reverseWhenStacked
}: CreateGridStackingCssParams): string => {
if (!stackAtBreakpoint) {
return "";
}

const breakpoint = viewportManager
.getViewport()
.breakpoints.find(bp => bp.name === stackAtBreakpoint);

if (!breakpoint) {
return "";
}

const direction = reverseWhenStacked ? "column-reverse" : "column";

// `!important` overrides the inline base (row) styles set on the elements.
return [
`@media (max-width: ${breakpoint.maxWidth}px) {`,
` .${gridClass} { flex-direction: ${direction} !important; }`,
` .${gridClass} > .wb-grid-col { flex: 0 0 100% !important; max-width: 100% !important; }`,
`}`
].join("\n");
};
8 changes: 5 additions & 3 deletions packages/website-builder-sdk/src/ContentSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ export class ContentSdk implements IContentSdk, IRedirects {

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

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

let editingSdk;
if (environment.isEditing()) {
Expand Down