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

Implement StorybookJS #8497

Merged
75 changes: 75 additions & 0 deletions .storybook/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const { propNames } = require("@chakra-ui/react")

module.exports = {
stories: [
{
directory: "../src/components",
titlePrefix: "Components",
files: "**/*.stories.tsx",
},
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
// https://storybook.js.org/addons/@storybook/addon-a11y/
"@storybook/addon-a11y",
"@chakra-ui/storybook-addon",
],
framework: "@storybook/react",
refs: {
"@chakra-ui/react": {
disable: true,
},
},
core: {
builder: "webpack5",
},
features: {
storyStoreV7: true,
emotionAlias: false,
},
webpackFinal: async (config) => {
// Transpile Gatsby module because Gatsby includes un-transpiled ES6 code.
config.module.rules[0].exclude = [
/node_modules\/(?!(gatsby|gatsby-script)\/)/,
]
// Remove core-js to prevent issues with Storybook
config.module.rules[0].exclude = [/core-js/]
// Use babel-plugin-remove-graphql-queries to remove static queries from components when rendering in storybook
config.module.rules[0].use[0].options.plugins.push(
require.resolve("babel-plugin-remove-graphql-queries")
)
config.resolve.mainFields = ["browser", "module", "main"]
return config
},
typescript: {
check: false,
checkOptions: {},
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {
compilerOptions: {
allowSyntheticDefaultImports: false,
esModuleInterop: false,
},
shouldExtractLiteralValuesFromEnum: true,
/**
* For handling bloated controls table of Chakra Props
*
* https://github.com/chakra-ui/chakra-ui/issues/2009#issuecomment-852793946
*/
propFilter: (prop) => {
const excludedPropNames = propNames.concat([
"as",
"apply",
"sx",
"__css",
])
const isStyledSystemProp = excludedPropNames.includes(prop.name)
const isHTMLElementProp =
prop.parent?.fileName.includes("node_modules") ?? false
return !(isStyledSystemProp || isHTMLElementProp)
},
},
},
}
57 changes: 57 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { action } from "@storybook/addon-actions"
import theme from "../src/@chakra-ui/gatsby-plugin/theme"
import { theme as DefaultChakraTheme } from "@chakra-ui/react"

const chakraBreakpointArray = Object.entries(DefaultChakraTheme.breakpoints)

// Gatsby's Link overrides:
// Gatsby Link calls the `enqueue` & `hovering` methods on the global variable ___loader.
// This global object isn't set in storybook context, requiring you to override it to empty functions (no-op),
// so Gatsby Link doesn't throw errors.
global.___loader = {
enqueue: () => {},
hovering: () => {},
}
// This global variable prevents the "__BASE_PATH__ is not defined" error inside Storybook.
global.__BASE_PATH__ = "/"

// Navigating through a gatsby app using gatsby-link or any other gatsby component will use the `___navigate` method.
// In Storybook, it makes more sense to log an action than doing an actual navigate. Check out the actions addon docs for more info: https://storybook.js.org/docs/react/essentials/actions

// @ts-ignore
window.___navigate = (pathname) => {
action("NavigateTo:")(pathname)
}

export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
chakra: {
theme,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loved that we can test the color mode and different breakpoints from the chakra theme.

},
// Modify viewport selection to match Chakra breakpoints (or custom breakpoints)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet 👍

viewport: {
viewports: chakraBreakpointArray.reduce((prevVal, currVal) => {
const [token, key] = currVal

// Unnecessary breakpoint
if (token === "base") return { ...prevVal }

return {
...prevVal,
[token]: {
name: token,
styles: {
width: key,
height: "600px",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering why do we need this height 🤔 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without a specified height set, an error would occur from the viewport tool:
Uncaught TypeError: styles.height is undefined

By the way, the viewport addon (part of the initial setup) comes with a collection of viewports that can also be applied to view stories with different common device specs.

},
},
}
}, {}),
},
}
94 changes: 94 additions & 0 deletions docs/applying-storybook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Applying Storybook to Components and Pages

## Overview

StorybookJS is a UI tool for isolating UI components to visually test their styles and states.

This is great for checking the various iterations of a component in a sandbox versus scowering all the pages in a large scale project it is used to verify that the component is rendering properly.

You can also render pages if you need that level of visual testing.

Storybook also gives you a library of addons provided by the team and the community to enhance the testing, including UX testing, A11y compliance, etc.

Check out [Intro to Storybook](https://storybook.js.org/tutorials/intro-to-storybook/) to get an in-depth look on the workflow.

## Spinning up the Storybook server

It's as easy as running `yarn storybook` to boot up a dedicated localhost to see all the components that have stories.

## Setting up a component's stories

A Storybook "story" is an instance of a component in a certain state or with certain paramaters applied to show an alternative version of the component.

Each component will only need one file containing all the stories, and should follow the naming convention of the component.

So for the component `ExpandableCard.tsx`, the stories file will be named `ExpandableCard.stories.tsx`.

The stories file will reside with each component. So the base folder structure in `src` will look like this:

```
src/
└── components/
└── ComponentA/
├── index.tsx
├── ComponentA.stories.tsx
└── // Any other files as applicable (utils, child components, useHook, etc.)
```

The initial structure of each story file will look something like this (in typescript):

```tsx
import ComponentA from "."

export default {
title: "ComponentA", // Generates the nav structure in the Storybook server
} as ComponentMeta<typeof ComponentA>

export const Basic = () => <ComponentA />
```

Should the component accept props on all or some renders, a template can be created.

Let's say for a `Button` component with different style variants...

```tsx
import Button from "."

export default {
title: "Button",
} as ComponentMeta<typeof Button>

const Template: ComponentStory<typeof Button> = (args) => (
<ComponentA {...args} />
)

export const Solid = Template.bind({})
Solid.args = {
variant: "solid",
children: "A Button", // Assuming the `children` prop takes text content only
}

export const Outline = Template.bind({})
Outline.args = {
variant: "outline",
children: "A Button", // Assuming the `children` prop takes text content only
}

/**
* For practical purposes, if you are displaying different "variants",
* they should be shown under one story, so they can be seen side-by-side in the GUI
* for reviewers to easily compare.
* This can be done for various sizes or other like alterations
*/

// Assuming `solid` is the default variant in the Chakra theme config
export const Variants = () => (
<VStack>
<Button>A Solid Button</Button>
<Button variant="outline">An Outline Button</Button>
<Button variant="unstyled">An Unstyled Button</Button>
</VStack>
)
```

As you go and make adjustments to the component itself or it's variant styles, Storybook will hot reload and those changes will appear in the stories that emphazise them.
16 changes: 15 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,18 @@
"unist-util-visit-parents": "^2.1.2"
},
"devDependencies": {
"@babel/core": "^7.19.6",
"@chakra-ui/storybook-addon": "^4.0.12",
"@netlify/functions": "^1.2.0",
"@storybook/addon-a11y": "^6.5.13",
"@storybook/addon-actions": "^6.5.13",
"@storybook/addon-essentials": "^6.5.13",
"@storybook/addon-interactions": "^6.5.13",
"@storybook/addon-links": "^6.5.13",
"@storybook/builder-webpack5": "^6.5.13",
"@storybook/manager-webpack5": "^6.5.13",
"@storybook/react": "^6.5.13",
"@storybook/testing-library": "^0.0.13",
"@types/browser-lang": "^0.1.0",
"@types/github-slugger": "^1.3.0",
"@types/luxon": "^2.3.2",
Expand All @@ -85,6 +96,7 @@
"@types/react-dom": "^18.0.6",
"@types/react-instantsearch-core": "^6.26.2",
"@types/styled-system": "^5.1.15",
"babel-loader": "^8.3.0",
"babel-preset-gatsby": "^2.23.0",
"github-slugger": "^1.3.0",
"gray-matter": "^4.0.3",
Expand All @@ -110,7 +122,9 @@
"start:lambda": "netlify-lambda serve src/lambda",
"start:static": "gatsby build && gatsby serve",
"serve": "gatsby serve",
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the idea of this build command to have a new endpoint where we can access all the stories all the time for each new build?

Thinking about our ci/cd pipeline, where do you see this applied?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think he mostly addressed this here: #8497 (comment)

},
"husky": {
"hooks": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ComponentMeta, ComponentStory } from "@storybook/react"
import React from "react"
import BannerNotification from "."

export default {
component: BannerNotification,
} as ComponentMeta<typeof BannerNotification>

/**
* Story taken from PostMergeBanner component
* and content from `../../content/developers/tutorials/hello-world-smart-contract-fullstack/index.md`
*/
export const PostMergeBanner: ComponentStory<typeof BannerNotification> = (
args
) => <BannerNotification {...args} />

PostMergeBanner.args = {
shouldShow: true,
justify: "center",
textAlign: "center",
sx: {
"& p": {
maxWidth: "100ch",
m: 0,
p: 0,
},
"& a": {
textDecor: "underline",
},
},
children: (
<p>
This tutorial is out of date after the merge and may not work. Please
raise a PR if you would like to contribute.
</p>
),
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react"
import { Flex, FlexProps, useMediaQuery } from "@chakra-ui/react"
import { lightTheme as oldTheme } from "../theme"
import { lightTheme as oldTheme } from "../../theme"

export interface IProps extends FlexProps {
shouldShow?: boolean
Expand Down
32 changes: 32 additions & 0 deletions src/components/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ComponentMeta, ComponentStory } from "@storybook/react"
import React from "react"
import Button from "."

export default {
component: Button,
} as ComponentMeta<typeof Button>

const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />

export const Solid = Template.bind({})
Solid.args = {
children: "What is Ethereum?",
}

export const Outline = Template.bind({})
Outline.args = {
children: "More on digital money",
variant: "outline",
}

export const OutlineColor = Template.bind({})
OutlineColor.args = {
children: "More on digital money",
variant: "outline-color",
}

export const Disabled = Template.bind({})
Disabled.args = {
children: "I am disabled",
disabled: true,
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react"
import { Button as ChakraButton, ButtonProps } from "@chakra-ui/react"

import { scrollIntoView } from "../utils/scrollIntoView"
import { scrollIntoView } from "../../utils/scrollIntoView"

export interface IProps extends ButtonProps {
toId?: string
Expand Down
30 changes: 30 additions & 0 deletions src/components/Card/Card.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Box } from "@chakra-ui/react"
import { ComponentMeta, ComponentStory } from "@storybook/react"
import React from "react"
import Card, { IProps } from "."
import Button from "../Button"

const Component = Card

export default {
component: Card,
decorators: [
(Story) => (
<Box maxW="342px" margin="0 auto">
<Story />
</Box>
),
],
} as ComponentMeta<typeof Component>

const defaultProps: IProps = {
emoji: ":woman_student:",
title: "Learn Ethereum development",
description: "Read up on core concepts and the Ethereum stack with our docs",
}

export const Default: ComponentStory<typeof Component> = (args) => (
<Component {...defaultProps} {...args}>
<Button>Read the docs</Button>
</Component>
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { ReactNode } from "react"
import { ChakraProps, Flex, Heading, Text } from "@chakra-ui/react"
import Emoji from "./Emoji"
import Emoji from "../Emoji"

export interface IProps extends ChakraProps {
children?: React.ReactNode
Expand Down