diff --git a/.github/workflows/build-chromatic.yml b/.github/workflows/build-chromatic.yml index 82405ee8dc..385cb5a0b0 100644 --- a/.github/workflows/build-chromatic.yml +++ b/.github/workflows/build-chromatic.yml @@ -1,4 +1,4 @@ -name: 'Build Storybook Preview and Publish to Chromatic' +name: 'Build Storybook - UI Tests with Chromatic' on: push: @@ -41,6 +41,8 @@ jobs: - name: Publish to Chromatic id: chromatic-publish uses: chromaui/action@v1 + env: + CHROMATIC: 1 with: token: ${{ secrets.GITHUB_TOKEN }} projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} diff --git a/.github/workflows/build-storybook.yml b/.github/workflows/build-storybook.yml new file mode 100644 index 0000000000..c4aa2e6cfc --- /dev/null +++ b/.github/workflows/build-storybook.yml @@ -0,0 +1,49 @@ +name: 'Build Storybook - Docs with Chromatic' + +on: + push: + branches: + - release + paths: + - 'app/client/packages/design-system/**' + pull_request: + paths: + - 'app/client/packages/design-system/**' + +jobs: + chromatic-deployment: + runs-on: ubuntu-latest + + steps: + - name: Checkout PR if pull_request event + if: github.event_name == 'pull_request' + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: refs/pull/${{ github.event.pull_request.number }}/merge + + - name: Checkout PR if push event + if: github.event_name == 'push' + uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: release + + - name: Use Node.js 16.14.0 + uses: actions/setup-node@v3 + with: + node-version: "16.14.0" + + - name: Install Dependencies + working-directory: ./app/client/packages/storybook + run: yarn install --immutable + + - name: Publish to Chromatic + id: chromatic-publish + uses: chromaui/action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + projectToken: ${{ secrets.STORYBOOK_PROJECT_TOKEN }} + workingDir: ./app/client/packages/storybook + exitOnceUploaded: true + buildScriptName: "build" diff --git a/app/client/packages/design-system/widgets/src/components/Switch/index.styled.tsx b/app/client/packages/design-system/widgets/src/components/Switch/index.styled.tsx index b0623239e0..4fa5da4b97 100644 --- a/app/client/packages/design-system/widgets/src/components/Switch/index.styled.tsx +++ b/app/client/packages/design-system/widgets/src/components/Switch/index.styled.tsx @@ -14,7 +14,7 @@ export const StyledSwitch = styled(HeadlessSwitch)` position: relative; width: var(--sizing-8); height: var(--sizing-4); - background-color: var(--color-bg-neutral); + background-color: var(--color-bd-neutral); border-radius: var(--knob-size); color: var(--color-bg); display: inline-flex; @@ -36,7 +36,7 @@ export const StyledSwitch = styled(HeadlessSwitch)` } &[data-hovered]:not([data-disabled]) [data-icon] { - --checkbox-border-color: var(--color-bd-neutral-hover); + background-color: var(--color-bd-neutral-hover); } /** diff --git a/app/client/packages/design-system/widgets/src/index.ts b/app/client/packages/design-system/widgets/src/index.ts index 84d36bdd31..f3ea2263e1 100644 --- a/app/client/packages/design-system/widgets/src/index.ts +++ b/app/client/packages/design-system/widgets/src/index.ts @@ -10,4 +10,6 @@ export * from "./components/Tooltip"; export * from "./components/Flex"; export * from "./components/Radio"; export * from "./components/RadioGroup"; +export * from "./components/Switch"; +export * from "./components/SwitchGroup"; export * from "./utils"; diff --git a/app/client/packages/storybook/.storybook/decorators/theming.tsx b/app/client/packages/storybook/.storybook/decorators/theming.tsx index d90169b004..0ab03736dd 100644 --- a/app/client/packages/storybook/.storybook/decorators/theming.tsx +++ b/app/client/packages/storybook/.storybook/decorators/theming.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import styled from "styled-components"; +import isChromatic from "chromatic/isChromatic"; import { ThemeProvider, useTheme } from "@design-system/theming"; const StyledThemeProvider = styled(ThemeProvider)` @@ -16,14 +17,17 @@ const StyledThemeProvider = styled(ThemeProvider)` export const theming = (Story, args) => { const { theme } = useTheme({ seedColor: args.globals.accentColor, - colorMode: args.globals.colorMode, + colorMode: args.parameters.colorMode || args.globals.colorMode, borderRadius: args.globals.borderRadius, fontFamily: args.globals.fontFamily, rootUnitRatio: args.globals.rootUnitRatio, }); return ( - + ); diff --git a/app/client/packages/storybook/.storybook/main.ts b/app/client/packages/storybook/.storybook/main.ts index 1e5c2fc5ba..37edec3401 100644 --- a/app/client/packages/storybook/.storybook/main.ts +++ b/app/client/packages/storybook/.storybook/main.ts @@ -1,5 +1,6 @@ import { dirname, join } from "path"; -const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); +import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; + async function webpackConfig(config) { config.module.rules.push({ test: /\.(js|jsx|ts|tsx)$/, @@ -17,11 +18,20 @@ async function webpackConfig(config) { config.resolve.plugins.push(new TsconfigPathsPlugin()); return config; } -module.exports = { - stories: [ + +function getStories() { + if (process.env.CHROMATIC) { + return ["../chromatic/**/*.chromatic.stories.@(js|jsx|ts|tsx)"]; + } + + return [ "../stories/**/*.stories.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)", - ], + ]; +} + +module.exports = { + stories: getStories(), addons: [ getAbsolutePath("@storybook/addon-viewport"), getAbsolutePath("@storybook/addon-docs"), diff --git a/app/client/packages/storybook/.storybook/styles.css b/app/client/packages/storybook/.storybook/styles.css index fbe7c6ae71..b08d2f98fe 100644 --- a/app/client/packages/storybook/.storybook/styles.css +++ b/app/client/packages/storybook/.storybook/styles.css @@ -5,6 +5,11 @@ body, width: 100%; } +/** we are stopping animation for all elements for chromatic so that our snapshopts are consistent */ +.is-chromatic * { + animation: none !important; +} + /* fonts */ @import url("https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap"); diff --git a/app/client/packages/storybook/chromatic/design-system/widgets/Button.chromatic.stories.tsx b/app/client/packages/storybook/chromatic/design-system/widgets/Button.chromatic.stories.tsx new file mode 100644 index 0000000000..a311075970 --- /dev/null +++ b/app/client/packages/storybook/chromatic/design-system/widgets/Button.chromatic.stories.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import { + Button, + BUTTON_VARIANTS, + BUTTON_COLORS, + Icon, +} from "@design-system/widgets"; +import { importRemixIcon } from "design-system-old"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { StoryGrid } from "../../../helpers/StoryGrid"; +import { DataAttrWrapper } from "../../../helpers/DataAttrWrapper"; + +const StarIcon = importRemixIcon(() => import("remixicon-react/StarFillIcon")); + +const variants = Object.values(BUTTON_VARIANTS); +const colors = Object.values(BUTTON_COLORS); + +const meta: Meta = { + component: Button, + title: "Design System/Widgets/Button", +}; + +export default meta; + +type Story = StoryObj; + +const states = [ + "", + "data-hovered", + "data-active", + "data-focused", + "aria-disabled", +]; + +const ICON = ( + + + +); + +export const LightMode: Story = { + render: () => ( + + {variants.map((variant) => + colors.map((color) => + states.map((state) => ( + + + + )), + ), + )} + + + + + ), +}; + +export const DarkMode: Story = Object.assign({}, LightMode); + +DarkMode.parameters = { + colorMode: "dark", +}; diff --git a/app/client/packages/storybook/chromatic/design-system/widgets/Checkbox.chromatic.stories.tsx b/app/client/packages/storybook/chromatic/design-system/widgets/Checkbox.chromatic.stories.tsx new file mode 100644 index 0000000000..1a0bb89e2c --- /dev/null +++ b/app/client/packages/storybook/chromatic/design-system/widgets/Checkbox.chromatic.stories.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { Checkbox } from "@design-system/widgets"; +import { StoryGrid } from "../../../helpers/StoryGrid"; +import { DataAttrWrapper } from "../../../helpers/DataAttrWrapper"; + +const meta: Meta = { + component: Checkbox, + title: "Design System/Widgets/Checkbox", +}; + +export default meta; + +type Story = StoryObj; + +const states = ["", "data-hovered", "data-focused", "data-disabled"]; + +export const LightMode: Story = { + render: () => ( + + Indeterminate + {states.map((state) => ( + <> + + unchecked {state} + + + checked {state} + + + ))} + + Readonly + + + ), +}; + +export const DarkMode: Story = Object.assign({}, LightMode); + +DarkMode.parameters = { + colorMode: "dark", +}; diff --git a/app/client/packages/storybook/chromatic/design-system/widgets/Group.chromatic.stories.tsx b/app/client/packages/storybook/chromatic/design-system/widgets/Group.chromatic.stories.tsx new file mode 100644 index 0000000000..017417780a --- /dev/null +++ b/app/client/packages/storybook/chromatic/design-system/widgets/Group.chromatic.stories.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { + CheckboxGroup, + RadioGroup, + Switch, + Checkbox, + SwitchGroup, + Radio, +} from "@design-system/widgets"; +import { StoryGrid } from "../../../helpers/StoryGrid"; + +const meta: Meta = { + component: Switch, + title: "Design System/Widgets/Group", +}; + +export default meta; + +type Story = StoryObj; + +export const LightMode: Story = { + render: () => ( + + + Option 1 + Option 2 + Opion 3 + + + Option 1 + Option 2 + Option 3 + + + Option 1 + Option 2 + Option 3 + + + ), +}; + +export const DarkMode: Story = Object.assign({}, LightMode); + +DarkMode.parameters = { + colorMode: "dark", +}; diff --git a/app/client/packages/storybook/chromatic/design-system/widgets/Radio.chromatic.stories.tsx b/app/client/packages/storybook/chromatic/design-system/widgets/Radio.chromatic.stories.tsx new file mode 100644 index 0000000000..333623f3ed --- /dev/null +++ b/app/client/packages/storybook/chromatic/design-system/widgets/Radio.chromatic.stories.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { Radio, RadioGroup } from "@design-system/widgets"; +import { StoryGrid } from "../../../helpers/StoryGrid"; +import { DataAttrWrapper } from "../../../helpers/DataAttrWrapper"; + +const meta: Meta = { + component: Radio, + title: "Design System/Widgets/Radio", +}; + +export default meta; + +type Story = StoryObj; + +const states = ["", "data-hovered", "data-focused", "data-disabled"]; + +export const LightMode: Story = { + render: () => ( + + {states.map((state) => ( + <> + + + unchecked {state} + + + + + checked {state} + + + + ))} + + ), +}; + +export const DarkMode: Story = Object.assign({}, LightMode); + +DarkMode.parameters = { + colorMode: "dark", +}; diff --git a/app/client/packages/storybook/chromatic/design-system/widgets/Switch.chromatic.stories.tsx b/app/client/packages/storybook/chromatic/design-system/widgets/Switch.chromatic.stories.tsx new file mode 100644 index 0000000000..8dcc48773c --- /dev/null +++ b/app/client/packages/storybook/chromatic/design-system/widgets/Switch.chromatic.stories.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { Switch } from "@design-system/widgets"; +import { StoryGrid } from "../../../helpers/StoryGrid"; +import { DataAttrWrapper } from "../../../helpers/DataAttrWrapper"; + +const meta: Meta = { + component: Switch, + title: "Design System/Widgets/Switch", +}; + +export default meta; + +type Story = StoryObj; + +const states = ["", "data-hovered", "data-focused", "data-disabled"]; + +export const LightMode: Story = { + render: () => ( + + {states.map((state) => ( + <> + + unchecked {state} + + + checked {state} + + + ))} + + ), +}; + +export const DarkMode: Story = Object.assign({}, LightMode); + +DarkMode.parameters = { + colorMode: "dark", +}; diff --git a/app/client/packages/storybook/chromatic/design-system/widgets/Text.chromatic.stories.tsx b/app/client/packages/storybook/chromatic/design-system/widgets/Text.chromatic.stories.tsx new file mode 100644 index 0000000000..6da1408f37 --- /dev/null +++ b/app/client/packages/storybook/chromatic/design-system/widgets/Text.chromatic.stories.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { Text } from "@design-system/widgets"; +import { StoryGrid } from "../../../helpers/StoryGrid"; +import { TypographyColor, TypographyVariant } from "@design-system/theming"; + +const meta: Meta = { + component: Text, + title: "Design System/Widgets/Text", +}; + +export default meta; + +type Story = StoryObj; + +export const LightMode: Story = { + storyName: "Text", + render: () => ( + + + Default - Lorem ipsum dolor sit, amet consectetur adipisicing elit. + Reiciendis, obcaecati velit voluptatibus ratione officia consectetur + similique ab rem adipisci atque eum dolores culpa cum reprehenderit + quidem cupiditate impedit modi in. + + + LineClamp - 2 - Lorem ipsum dolor sit amet consectetur adipisicing elit. + Enim eaque consequatur vel cupiditate nihil! Natus itaque voluptatibus, + possimus nisi expedita, inventore nobis obcaecati aspernatur + necessitatibus, molestias deleniti corrupti aliquam repudiandae. + + + LineClamp - 1 - Lorem ipsum dolor sit amet consectetur adipisicing elit. + Enim eaque consequatur vel cupiditate nihil! Natus itaque voluptatibus, + possimus nisi expedita, inventore nobis obcaecati aspernatur + necessitatibus, molestias deleniti corrupti aliquam repudiandae. + + Text Align Center + Text Align Right + Italic + Bold + {Object.values(TypographyVariant).map((variant) => ( + + {variant} + + ))} + {Object.values(TypographyColor).map((color) => ( + + {color} + + ))} + + ), +}; + +export const DarkMode: Story = Object.assign({}, LightMode); + +DarkMode.parameters = { + colorMode: "dark", +}; diff --git a/app/client/packages/storybook/chromatic/design-system/widgets/Tooltip.chromatic.stories.tsx b/app/client/packages/storybook/chromatic/design-system/widgets/Tooltip.chromatic.stories.tsx new file mode 100644 index 0000000000..af7765f949 --- /dev/null +++ b/app/client/packages/storybook/chromatic/design-system/widgets/Tooltip.chromatic.stories.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import { + Button, + TooltipTrigger, + TooltipContent, + TooltipRoot as Tooltip, +} from "@design-system/widgets"; +import { StoryGrid } from "../../../helpers/StoryGrid"; + +const meta: Meta = { + component: Tooltip, + title: "Design System/Widgets/Tooltip", +}; + +export default meta; + +type Story = StoryObj; + +export const LightMode: Story = { + render: () => ( + + + + + + This is a tooltip + + + + + + This is a tooltip + + + + + + This is a tooltip + + + + + + This is a tooltip + + + ), +}; + +export const DarkMode: Story = Object.assign({}, LightMode); + +DarkMode.parameters = { + colorMode: "dark", +}; diff --git a/app/client/packages/storybook/helpers/DataAttrWrapper.tsx b/app/client/packages/storybook/helpers/DataAttrWrapper.tsx new file mode 100644 index 0000000000..121a2292e5 --- /dev/null +++ b/app/client/packages/storybook/helpers/DataAttrWrapper.tsx @@ -0,0 +1,37 @@ +import React, { useEffect, useRef } from "react"; + +type DataAttrWrapperProps = { + children: React.ReactNode; + attr: string; +}; + +export const DataAttrWrapper = (props: DataAttrWrapperProps) => { + const { attr, children } = props; + + // Adding any type here because WDS components has different types for ref + // some are HTMLElement and some are objects only ( For e.g - CheckboxRef ) + const ref = useRef(null); + + useEffect(() => { + if (attr && ref?.current) { + if ( + ref.current.setAttribute && + typeof ref.current.setAttribute === "function" + ) { + ref.current.setAttribute(attr, ""); + + return; + } + + if (typeof ref.current.UNSAFE_getDOMNode === "function") { + const domNode = ref.current.UNSAFE_getDOMNode(); + + if (domNode) domNode.setAttribute(attr, ""); + + return; + } + } + }, [attr, ref.current]); + + return React.cloneElement(children as React.ReactElement, { ref }); +}; diff --git a/app/client/packages/storybook/helpers/StoryGrid.tsx b/app/client/packages/storybook/helpers/StoryGrid.tsx new file mode 100644 index 0000000000..5a6b055b15 --- /dev/null +++ b/app/client/packages/storybook/helpers/StoryGrid.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +type StoryGrid = { + cols?: number | string; + children: React.ReactNode; + gap?: string; +}; + +export function StoryGrid(props: StoryGrid) { + const { cols = 5, gap = "10px" } = props; + + return ( +
+ {props.children} +
+ ); +} diff --git a/app/client/packages/storybook/package.json b/app/client/packages/storybook/package.json index 5454fc8de4..f8a9bc9570 100644 --- a/app/client/packages/storybook/package.json +++ b/app/client/packages/storybook/package.json @@ -7,7 +7,7 @@ "scripts": { "storybook": "storybook dev -p 6006", "build": "storybook build", - "chromatic": "chromatic --project-token CHROMATIC_PROJECT_TOKEN" + "chromatic": "CHROMATIC=1 storybook dev -p 6006" }, "devDependencies": { "@babel/core": "^7.21.0", diff --git a/app/client/packages/design-system/widgets/src/components/Switch/Switch.stories.mdx b/app/client/packages/storybook/stories/design-system/widgets/Switch.stories.mdx similarity index 97% rename from app/client/packages/design-system/widgets/src/components/Switch/Switch.stories.mdx rename to app/client/packages/storybook/stories/design-system/widgets/Switch.stories.mdx index c8c76328c5..58e9fdc2e6 100644 --- a/app/client/packages/design-system/widgets/src/components/Switch/Switch.stories.mdx +++ b/app/client/packages/storybook/stories/design-system/widgets/Switch.stories.mdx @@ -1,7 +1,7 @@ import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs"; import EmotionHappyLineIcon from "remixicon-react/EmotionHappyLineIcon"; -import { Switch } from "./"; +import { Switch } from "@design-system/widgets";