Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Page Builder Improvements (@container, Emotion Context, Theme-aware mq Utility Function) #3141

Merged
merged 38 commits into from Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4f463a1
feat: upgrade to Emotion 11
adrians5j Mar 25, 2023
d0b25f1
feat: upgrade to Emotion 11
adrians5j Mar 25, 2023
b35112e
fix: remove custom "assign styles" callback
adrians5j Mar 27, 2023
522c94a
fix: use `@container` instead of `@media`
adrians5j Mar 27, 2023
46ab611
fix: wrap Emotion-related code into `EmotionProvider`
adrians5j Mar 27, 2023
353bf97
feat: use containers to enable responsive design in both website and …
adrians5j Mar 27, 2023
31042d0
feat: enumerate default theme breakpoints
adrians5j Mar 27, 2023
00995fd
chore: remove old comment
adrians5j Mar 27, 2023
2c9ca5f
feat: wrap provider with Emotion's theme provider
adrians5j Mar 27, 2023
ded6fc2
feat: upgrade to Emotion 11
adrians5j Mar 27, 2023
87f8ae0
Merge branch '535-emo11' into 535-container
adrians5j Mar 27, 2023
64f7ced
fix: apply container to page preview
adrians5j Mar 27, 2023
7af220a
fix: revert `@container` usage
adrians5j Mar 27, 2023
fdaa7cc
fix: "containerize" breakpoints
adrians5j Mar 27, 2023
d97f380
fix: update comment
adrians5j Mar 27, 2023
b1e9392
fix: update comment
adrians5j Mar 27, 2023
41e7bed
fix: update comment
adrians5j Mar 27, 2023
1d16a45
fix: remove old `ssr-cache` type
adrians5j Mar 28, 2023
2cf0253
feat: add Emotion's `Theme` type
adrians5j Mar 28, 2023
7d22b8c
fix: remove invalid import
adrians5j Mar 28, 2023
4ae348e
feat: add `mq` hook
adrians5j Mar 28, 2023
03bf329
chore: run eslint
adrians5j Mar 28, 2023
5db7da9
chore: run prettier
adrians5j Mar 28, 2023
d5a59ad
chore: update deps
adrians5j Mar 28, 2023
4b339de
feat: create `mediaToContainer` function
adrians5j Mar 28, 2023
7d8bddb
fix: use newly introduced `mediaToContainer` to convert media queries
adrians5j Mar 28, 2023
cb70e94
fix: undo emotion-related changes
adrians5j Mar 28, 2023
c0f0c68
fix: rename container name from `body` to `page-editor-canvas`
adrians5j Mar 28, 2023
be80f82
chore: run prettier
adrians5j Mar 28, 2023
804ca2d
fix: undo line removal
adrians5j Mar 28, 2023
faea828
fix: rename `DefaultThemeBreakpoints` to `DefaultThemeBreakpoint`
adrians5j Mar 30, 2023
9d6c67f
fix: import using relative path
adrians5j Mar 30, 2023
8b7ec23
fix: do not use `css` prop
adrians5j Mar 30, 2023
25f9d9a
fix: undo `compilerOptions` change
adrians5j Mar 30, 2023
462aad7
Merge branch '535-emo11' into 535-container
adrians5j Mar 30, 2023
79bb7b9
fix: instead of importing, c/p types into the file
adrians5j Mar 30, 2023
526dae1
Merge remote-tracking branch 'origin/next' into 535-container
adrians5j Mar 31, 2023
883806f
chore: update deps
adrians5j Mar 31, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 0 additions & 3 deletions apps/theme/index.ts
@@ -1,7 +1,5 @@
import StaticLayout from "./layouts/pages/Static";
import theme from "./theme";

// TODO CLEAN!
import { PbPageLayoutPlugin } from "@webiny/app-page-builder";
import { FbFormLayoutPlugin } from "@webiny/app-form-builder";
import { ThemePlugin } from "@webiny/app-website";
Expand All @@ -17,7 +15,6 @@ export default () => [
new FbFormLayoutPlugin({
name: "default",
title: "Default layout",

component: DefaultFormLayout
})
];
7 changes: 3 additions & 4 deletions packages/app-page-builder-elements/package.json
Expand Up @@ -18,7 +18,8 @@
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@webiny/lexical-editor": "0.0.0",
"@webiny/theme": "0.0.0"
"@webiny/theme": "0.0.0",
"facepaint": "^1.2.1"
},
"peerDependencies": {
"@editorjs/editorjs": "^2.20.1",
Expand Down Expand Up @@ -48,15 +49,13 @@
"devDependencies": {
"@babel/cli": "^7.19.3",
"@babel/core": "^7.19.3",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/preset-env": "^7.19.4",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@types/facepaint": "^1.2.2",
"@types/react": "17.0.39",
"@types/resize-observer-browser": "^0.1.4",
"@webiny/cli": "0.0.0",
"@webiny/project-utils": "0.0.0",
"babel-plugin-lodash": "^3.3.4",
"execa": "^5.0.0",
"rimraf": "^3.0.2",
"ttypescript": "^1.5.12",
Expand Down
@@ -1,4 +1,5 @@
import React, { createContext, useCallback, useEffect, useState } from "react";
import { ThemeProvider } from "@emotion/react";
import {
PageElementsContextValue,
PageElementsProviderProps,
Expand Down Expand Up @@ -122,5 +123,9 @@ export const PageElementsProvider: React.FC<PageElementsProviderProps> = ({
afterRenderer
};

return <PageElementsContext.Provider value={value}>{children}</PageElementsContext.Provider>;
return (
<ThemeProvider theme={theme}>
<PageElementsContext.Provider value={value}>{children}</PageElementsContext.Provider>
</ThemeProvider>
);
};
14 changes: 14 additions & 0 deletions packages/app-page-builder-elements/src/hooks/useFacepaint.ts
@@ -0,0 +1,14 @@
import { useMemo } from "react";
import facepaint, { type DynamicStyleFunction } from "facepaint";
import { usePageElements } from "~/hooks/usePageElements";

export const useFacepaint: typeof facepaint = (...args) => {
return useMemo(() => facepaint(...args), [JSON.stringify(args)]);
};

export const mq = (...args: Parameters<DynamicStyleFunction>) => {
const { theme } = usePageElements();
const facepaint = useFacepaint(Object.values(theme.breakpoints));

return facepaint(...args);
};
1 change: 1 addition & 0 deletions packages/app-page-builder-elements/src/index.ts
Expand Up @@ -5,6 +5,7 @@ export * from "./createRenderer";
export * from "./hooks/usePage";
export * from "./hooks/usePageElements";
export * from "./hooks/useRenderer";
export * from "./hooks/useFacepaint";

export * from "./contexts/PageElements";
export * from "./contexts/Page";
Expand Down
5 changes: 5 additions & 0 deletions packages/app-page-builder/jest.config.js
@@ -0,0 +1,5 @@
const base = require("../../jest.config.base");

module.exports = {
...base({ path: __dirname })
};
22 changes: 15 additions & 7 deletions packages/app-page-builder/src/editor/components/Editor/Content.tsx
Expand Up @@ -70,11 +70,19 @@ const contentContainerWrapper = css({
zIndex: 1
});

const BaseContainer = styled("div")({
width: "100%",
left: 52,
margin: "0 auto"
});
const LegacyBaseContainer = styled.div`
width: 100%;
left: 52px;
margin: 0 auto;
`;

const BaseContainer = styled(LegacyBaseContainer)`
/* The usage of containers (the "@container" CSS at-rule) enables us to have responsive */
/* design not only on the actual website, but also within the Page Builder's page editor. */
/* Note that on the actual website, regular media queries are being used. */
container-type: inline-size;
container-name: page-editor-canvas;
`;

const Content: React.FC = () => {
const rootElementId = useRecoilValue(rootElementAtom);
Expand Down Expand Up @@ -122,12 +130,12 @@ const Content: React.FC = () => {
)} webiny-pb-media-query--${kebabCase(displayMode)}`}
>
<EditorContent />
<BaseContainer
<LegacyBaseContainer
ref={pagePreviewRef}
className={"webiny-pb-editor-content-preview"}
>
<Element id={rootElement.id} />
</BaseContainer>
</LegacyBaseContainer>
</LegacyContentContainer>
</Elevation>
);
Expand Down
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from "react";
import React, { useCallback, useMemo } from "react";
import { css } from "emotion";
import classNames from "classnames";
import { plugins } from "@webiny/plugins";
Expand All @@ -7,12 +7,8 @@ import { Tooltip } from "@webiny/ui/Tooltip";
import { Typography } from "@webiny/ui/Typography";
import { PbEditorResponsiveModePlugin } from "~/types";
import { usePageBuilder } from "~/hooks/usePageBuilder";
import { usePageElements } from "@webiny/app-page-builder-elements/hooks/usePageElements";
import { isPerBreakpointStylesObject } from "@webiny/app-page-builder-elements/utils";
import { useUI } from "~/editor/hooks/useUI";
import { setDisplayModeMutation } from "~/editor/recoil/modules";
import { isLegacyRenderingEngine } from "~/utils";
import { CSSObject } from "@emotion/react";

const classes = {
wrapper: css({
Expand Down Expand Up @@ -87,20 +83,6 @@ const classes = {
})
};

// This function ensures properties that have `undefined` as its
// value are not assigned to the target object.
function assignDefined(target: Record<string, any>, ...sources: Array<Record<string, any>>) {
for (const source of sources) {
for (const key of Object.keys(source)) {
const val = source[key];
if (val !== undefined) {
target[key] = val;
}
}
}
return target;
}

export const ResponsiveModeSelector: React.FC = () => {
const [{ displayMode, pagePreviewDimension }, setUiValue] = useUI();
const {
Expand All @@ -124,41 +106,6 @@ export const ResponsiveModeSelector: React.FC = () => {
[]
);

const pageElements = usePageElements();
if (!isLegacyRenderingEngine) {
// By default, we want to only assign styles for the first breakpoint in line, which is "desktop".
// We only care about tablet, mobile-landscape, and mobile-portrait if user selects one of those.
useEffect(() => {
const whitelistedBreakpoints: string[] = [];
for (let i = 0; i < editorModes.length; i++) {
const current = editorModes[i];
whitelistedBreakpoints.push(current.config.displayMode);
if (current.config.displayMode === displayMode) {
break;
}
}

pageElements.setAssignStylesCallback(params => {
const { breakpoints, styles = {}, assignTo = {} } = params;
if (isPerBreakpointStylesObject({ breakpoints, styles })) {
for (const breakpointName in breakpoints) {
if (
styles[breakpointName] &&
whitelistedBreakpoints.includes(breakpointName)
) {
// Filter out properties that have `undefined` set as its value.
assignDefined(assignTo, styles[breakpointName] as CSSObject);
}
}
} else {
Object.assign(assignTo, styles);
}

return assignTo;
});
}, [displayMode]);
}

const responsiveBarContent = useMemo(() => {
return editorModes.map(({ config: { displayMode: mode, icon, toolTip } }) => {
return (
Expand Down
@@ -1,4 +1,4 @@
import React from "react";
import React, { useMemo } from "react";
import { PageElementsProvider as PbPageElementsProvider } from "@webiny/app-page-builder-elements/contexts/PageElements";

// Attributes modifiers.
Expand Down Expand Up @@ -30,8 +30,8 @@ import { usePageBuilder } from "~/hooks/usePageBuilder";
import { Theme } from "@webiny/app-theme/types";
import { plugins } from "@webiny/plugins";
import { PbEditorPageElementPlugin } from "~/types";

import { ElementControls } from "./EditorPageElementsProvider/ElementControls";
import { mediaToContainer } from "./EditorPageElementsProvider/mediaToContainer";

export const EditorPageElementsProvider: React.FC = ({ children }) => {
const pageBuilder = usePageBuilder();
Expand Down Expand Up @@ -66,10 +66,26 @@ export const EditorPageElementsProvider: React.FC = ({ children }) => {
}
};

// We override all `@media` usages in breakpoints with `@container page-editor-canvas`. This is what
// enables us responsive design inside the Page Builder's page editor.
const containerizedTheme = useMemo(() => {
const theme = pageBuilder.theme as Theme;

return {
...pageBuilder.theme,
breakpoints: Object.keys(theme.breakpoints).reduce((result, breakpointName) => {
const breakpoint = theme.breakpoints[breakpointName];
return {
...result,
[breakpointName]: mediaToContainer(breakpoint)
};
}, {})
} as Theme;
}, [pageBuilder.theme]);

return (
<PbPageElementsProvider
// We can assign `Theme` here because we know at this point we're using the new elements rendering engine.
theme={pageBuilder.theme as Theme}
theme={containerizedTheme}
renderers={renderers}
modifiers={modifiers}
beforeRenderer={ElementControls}
Expand Down
@@ -0,0 +1,27 @@
import { mediaToContainer } from "../mediaToContainer";

describe("mediaToContainer function should correctly transform @media into @container queries", () => {
it("should correctly transform max-width", async () => {
expect(mediaToContainer("@media (max-width: 4000px)")).toBe(
"@container page-editor-canvas (max-width: 4000px)"
);
});

it("should correctly transform min-width", async () => {
expect(mediaToContainer("@media (min-width: 4000px)")).toBe(
"@container page-editor-canvas (min-width: 4000px)"
);
});

it("should correctly transform when both max-width and min-width are used", async () => {
expect(mediaToContainer("@media screen and (min-width: 10px) and (max-width: 15px)")).toBe(
"@container page-editor-canvas (min-width: 10px) and (max-width: 15px)"
);
});

it("should correctly transform when different units are used", async () => {
expect(mediaToContainer("@media screen and (min-width: 10em) and (max-width: 15vh)")).toBe(
"@container page-editor-canvas (min-width: 10em) and (max-width: 15vh)"
);
});
});
@@ -0,0 +1,21 @@
const minWidthRegExp = new RegExp("min-width:[ ]*([0-9a-z]*)");
const maxWidthRegExp = new RegExp("max-width:[ ]*([0-9a-z]*)");

/**
* At the moment, we only support basic transformations (check tests).
* If this won't be good enoough, we can potentially check this
* library: https://www.npmjs.com/package/media-query-parser
*/
export const mediaToContainer = (mediaQuery: string): string => {
const [, minWidth] = mediaQuery.match(minWidthRegExp) || [];
const [, maxWidth] = mediaQuery.match(maxWidthRegExp) || [];

const widthRules = [
minWidth && `(min-width: ${minWidth})`,
maxWidth && `(max-width: ${maxWidth})`
]
.filter(Boolean)
.join(" and ");

return `@container page-editor-canvas ${widthRules}`;
};
@@ -0,0 +1,7 @@
import { Theme as WTheme } from "@webiny/theme/types";

declare module "@emotion/react" {
// Ignoring "@typescript-eslint/no-empty-interface" rule here.
// eslint-disable-next-line
export interface Theme extends WTheme {}
}
16 changes: 15 additions & 1 deletion packages/theme/src/types.ts
Expand Up @@ -9,7 +9,21 @@ export interface StylesObject {
[key: string]: CSSObject | string | number | undefined;
}

export type ThemeBreakpoints = Record<string, string>;
export enum DefaultThemeBreakpoint {
DESKTOP = "desktop",
TABLET = "tablet",
MOBILE_LANDSCAPE = "mobile-landscape",
MOBILE_PORTRAIT = "mobile-portrait"
}

export type ThemeBreakpoints = {
[DefaultThemeBreakpoint.DESKTOP]: string;
[DefaultThemeBreakpoint.TABLET]: string;
[DefaultThemeBreakpoint.MOBILE_LANDSCAPE]: string;
[DefaultThemeBreakpoint.MOBILE_PORTRAIT]: string;

[key: string]: string;
};

export interface ThemeStyles {
colors: Record<string, any>;
Expand Down
47 changes: 47 additions & 0 deletions typings/emotion/index.d.ts
@@ -0,0 +1,47 @@
/**
* We are copy/pasting the `@webiny/theme/types` file here because of an issue
* with our build system. This is not the case in users' projects. There we
* simply import the types from `@webiny/theme` packages. See:
* packages/cwp-template-aws/template/common/types/emotion/index.d.ts
*/

import { type CSSObject } from "@emotion/react";

export type Content = Element;

/**
* Should be a `CSSObject` object or an object with breakpoint names as keys and `CSSObject` objects as values.
*/
export interface StylesObject {
[key: string]: CSSObject | string | number | undefined;
}

export enum DefaultThemeBreakpoint {
DESKTOP = "desktop",
TABLET = "tablet",
MOBILE_LANDSCAPE = "mobile-landscape",
MOBILE_PORTRAIT = "mobile-portrait"
}

export type ThemeBreakpoints = {
[DefaultThemeBreakpoint.DESKTOP]: string;
[DefaultThemeBreakpoint.TABLET]: string;
[DefaultThemeBreakpoint.MOBILE_LANDSCAPE]: string;
[DefaultThemeBreakpoint.MOBILE_PORTRAIT]: string;

[key: string]: string;
};

export interface ThemeStyles {
colors: Record<string, any>;
borderRadius?: number;
typography: Record<string, StylesObject>;
elements: Record<string, Record<string, any> | StylesObject>;

[key: string]: any;
}

export interface Theme {
breakpoints: ThemeBreakpoints;
styles: ThemeStyles;
}