From d3be46e2c56bd59a7bb6d622f504e0c09bcbdbb3 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Wed, 14 Jun 2023 23:52:02 -0400 Subject: [PATCH 1/2] Setup Storybook with light and dark theme and multiple viewports --- .storybook/main.ts | 1 + .storybook/preview.ts | 39 ----- .storybook/preview.tsx | 154 ++++++++++++++++++ package.json | 1 + src/screens/Onboarding/Onboarding.stories.tsx | 2 +- src/screens/SelectProject.stories.tsx | 2 +- src/storyWrapper.tsx | 23 --- src/utils/graphQLClient.tsx | 8 +- 8 files changed, 165 insertions(+), 65 deletions(-) delete mode 100644 .storybook/preview.ts create mode 100644 .storybook/preview.tsx delete mode 100644 src/storyWrapper.tsx diff --git a/.storybook/main.ts b/.storybook/main.ts index ccfa83d1..35704de9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -5,6 +5,7 @@ const config: StorybookConfig = { "@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", + "storybook-addon-designs", "./local-preset.js", ], docs: { diff --git a/.storybook/preview.ts b/.storybook/preview.ts deleted file mode 100644 index 7a048c18..00000000 --- a/.storybook/preview.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Preview } from "@storybook/react"; -import { initialize, mswDecorator } from "msw-storybook-addon"; - -// Initialize MSW -initialize({ - onUnhandledRequest(req) { - if (req.url.origin !== document.location.origin) { - console.error( - `[MSW] %s %s %s (UNHANDLED)`, - new Date().toTimeString().slice(0, 8), - req.method.toUpperCase(), - req.url.href - ); - } - }, -}); - -// Provide the MSW addon decorator globally -export const decorators = [mswDecorator]; - -const preview: Preview = { - parameters: { - actions: { - argTypesRegex: "^on[A-Z].*", - }, - backgrounds: { - default: "light", - }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, - layout: "fullscreen", - }, -}; - -export default preview; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 00000000..a8f16026 --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,154 @@ +import { + Global, + ThemeProvider, + themes, + createReset, + convert, + styled, + useTheme, +} from "@storybook/theming"; +import type { Preview } from "@storybook/react"; +import { initialize, mswDecorator } from "msw-storybook-addon"; +import React from "react"; + +// Initialize MSW +initialize({ + onUnhandledRequest(req) { + if (req.url.origin !== document.location.origin) { + console.error( + `[MSW] %s %s %s (UNHANDLED)`, + new Date().toTimeString().slice(0, 8), + req.method.toUpperCase(), + req.url.href + ); + } + }, +}); + +const Panels = styled.div<{ orientation: "right" | "bottom" }>( + ({ orientation }) => ({ + flexDirection: orientation === "right" ? "row" : "column", + }), + { + width: "100vw", + height: "100vh", + display: "flex", + justifyContent: "center", + alignItems: "center", + gap: 20, + } +); + +const Panel = styled.div<{ orientation: "right" | "bottom" }>( + ({ orientation }) => ({ + width: orientation === "right" ? 420 : 900, + height: orientation === "right" ? 640 : 300, + overflow: "auto", + }), + ({ theme }) => ({ + position: "relative", + outline: `1px solid ${theme.color.border}`, + background: theme.background.content, + color: theme.color.defaultText, + fontSize: theme.typography.size.s2 - 1, + }) +); + +const ThemedSetRoot = () => { + const theme = useTheme(); + React.useEffect(() => { + document.body.style.background = theme.background.content; + document.body.style.color = theme.color.defaultText; + document.body.style.fontSize = `${theme.typography.size.s2 - 1}px`; + }); + return null; +}; + +export const decorators = [ + // Provide the MSW addon decorator globally + mswDecorator, + + // Render two panels side-by-side or stacked, depending on selected orientation + // Note this assumes any play functions are equipped to deal with multiple canvases + (StoryFn, { globals, parameters }) => { + const theme = globals.theme || parameters.theme || "right"; + return theme === "light" || theme === "dark" ? ( + + + + + + + ) : ( + <> + + + + + + + + + + + + + + + + + + ); + }, +]; + +export const parameters: Preview["parameters"] = { + actions: { + argTypesRegex: "^on[A-Z].*", + }, + backgrounds: { + disable: true, + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + layout: "fullscreen", + viewport: { + viewports: { + right: { + name: "Right panel", + styles: { + width: "420px", + height: "640px", + }, + }, + bottom: { + name: "Bottom panel", + styles: { + width: "900px", + height: "300px", + }, + }, + }, + }, +}; + +export const globalTypes = { + theme: { + name: "Theme", + description: "Panel theme", + toolbar: { + icon: "sidebaralt", + title: "Theme", + items: [ + { value: "light", icon: "circlehollow", title: "Light" }, + { value: "dark", icon: "circle", title: "Dark" }, + { value: "right", icon: "sidebaralt", title: "Right 2-up" }, + { value: "bottom", icon: "bottombar", title: "Bottom 2-up" }, + ], + }, + }, +}; diff --git a/package.json b/package.json index 23a7f0e5..85e41eb3 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "react-dom": "^18.0.0", "rimraf": "^3.0.2", "storybook": "^7.0.0", + "storybook-addon-designs": "^7.0.0-beta.2", "tsup": "^6.6.3", "typescript": "^4.9.5", "vite": "^4.1.4", diff --git a/src/screens/Onboarding/Onboarding.stories.tsx b/src/screens/Onboarding/Onboarding.stories.tsx index e79b3e5f..0ccea412 100644 --- a/src/screens/Onboarding/Onboarding.stories.tsx +++ b/src/screens/Onboarding/Onboarding.stories.tsx @@ -3,7 +3,7 @@ import { Meta, StoryObj } from "@storybook/react"; import { findByRole, userEvent } from "@storybook/testing-library"; import { rest } from "msw"; -import { storyWrapper } from "../../storyWrapper"; +import { storyWrapper } from "../../utils/graphQLClient"; import { Onboarding } from "./Onboarding"; const meta = { diff --git a/src/screens/SelectProject.stories.tsx b/src/screens/SelectProject.stories.tsx index 1633c074..af2ecebd 100644 --- a/src/screens/SelectProject.stories.tsx +++ b/src/screens/SelectProject.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryObj } from "@storybook/react"; import { graphql } from "msw"; -import { storyWrapper } from "../storyWrapper"; +import { storyWrapper } from "../utils/graphQLClient"; import { SelectProject } from "./SelectProject"; const meta = { diff --git a/src/storyWrapper.tsx b/src/storyWrapper.tsx deleted file mode 100644 index 4ea7cfc8..00000000 --- a/src/storyWrapper.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { styled, typography } from "@storybook/theming"; -import React from "react"; - -import { Provider, client } from "./utils/graphQLClient"; - -const Wrapper = styled.div({ - width: "100vw", - height: "100vh", - - "*": { - boxSizing: "border-box", - fontFamily: typography.fonts.base, - fontSize: typography.size.s2 - 1, - }, -}); - -export const storyWrapper = (Story: any) => ( - - - - - -); diff --git a/src/utils/graphQLClient.tsx b/src/utils/graphQLClient.tsx index 8fcc58cf..bcade3b4 100644 --- a/src/utils/graphQLClient.tsx +++ b/src/utils/graphQLClient.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import React, { useState } from "react"; import { Client, Provider, cacheExchange, fetchExchange } from "urql"; import { CHROMATIC_BASE_URL, STORAGE_KEY } from "../constants"; @@ -29,3 +29,9 @@ export const client = new Client({ }, }), }); + +export const storyWrapper = (Story: any) => ( + + + +); From 328c9e799491bdb38b3104d77a4894b2bf82c4f7 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 20 Jun 2023 11:48:08 +0200 Subject: [PATCH 2/2] Fix play functions and base theme --- src/components/Container.tsx | 8 ++++---- src/screens/Onboarding/Onboarding.stories.tsx | 17 +++++++---------- src/utils/playAll.ts | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 src/utils/playAll.ts diff --git a/src/components/Container.tsx b/src/components/Container.tsx index 58af1eed..15ef152d 100644 --- a/src/components/Container.tsx +++ b/src/components/Container.tsx @@ -1,10 +1,10 @@ -import { background, styled } from "@storybook/theming"; +import { styled } from "@storybook/theming"; -export const Container = styled.div({ - background: background.app, +export const Container = styled.div(({ theme }) => ({ + background: theme.background.app, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", height: "100%", -}); +})); diff --git a/src/screens/Onboarding/Onboarding.stories.tsx b/src/screens/Onboarding/Onboarding.stories.tsx index 0ccea412..ee83d04f 100644 --- a/src/screens/Onboarding/Onboarding.stories.tsx +++ b/src/screens/Onboarding/Onboarding.stories.tsx @@ -5,6 +5,7 @@ import { rest } from "msw"; import { storyWrapper } from "../../utils/graphQLClient"; import { Onboarding } from "./Onboarding"; +import { playAll } from "../../utils/playAll"; const meta = { component: Onboarding, @@ -47,32 +48,28 @@ type Story = StoryObj; export const Welcome: Story = {}; export const SignIn: Story = { - play: async ({ canvasElement }) => { + play: playAll(async ({ canvasElement }) => { const button = await findByRole(canvasElement, "button", { name: "Enable", }); await userEvent.click(button); - }, + }), }; export const SSO: Story = { - play: async (context) => { - await SignIn.play(context); - + play: playAll(SignIn, async (context) => { const button = await findByRole(context.canvasElement, "button", { name: "Sign into Chromatic with SSO", }); await userEvent.click(button); - }, + }), }; export const Verify: Story = { - play: async (context) => { - await SignIn.play(context); - + play: playAll(SignIn, async (context) => { const button = await findByRole(context.canvasElement, "button", { name: "Sign in with Chromatic", }); await userEvent.click(button); - }, + }), }; diff --git a/src/utils/playAll.ts b/src/utils/playAll.ts new file mode 100644 index 00000000..af6cd46b --- /dev/null +++ b/src/utils/playAll.ts @@ -0,0 +1,18 @@ +import { StoryObj } from "@storybook/react"; + +export function playAll( + ...sequence: (Story | Story["play"])[] +): Story["play"] { + return async (context) => { + const canvasNodes = context.canvasElement.querySelectorAll("[data-canvas]"); + const canvasElements = canvasNodes.length ? Array.from(canvasNodes) : [context.canvasElement]; + + await Promise.all( + sequence.flatMap((storyOrPlay) => + typeof storyOrPlay === "function" + ? canvasElements.map((canvasElement) => storyOrPlay({ ...context, canvasElement })) + : storyOrPlay?.play?.(context) + ) + ); + }; +}