Merge branch 'next' into web-component-shadow-part-props-table

This commit is contained in:
Michael Shilman 2020-09-24 14:27:12 +08:00 committed by GitHub
commit 02400c272b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1782 changed files with 32184 additions and 44921 deletions

View File

@ -1,14 +1,21 @@
version: 2.1
aliases:
- &defaults
executors:
sb_node:
parameters:
class:
description: The Resource class
type: enum
enum: ["small", "medium", "large", "xlarge"]
default: "medium"
working_directory: /tmp/storybook
docker:
- image: circleci/node:10-browsers
resource_class: <<parameters.class>>
jobs:
install:
<<: *defaults
executor: sb_node
steps:
- checkout
- restore_cache:
@ -37,7 +44,7 @@ jobs:
- app
- lib
build:
<<: *defaults
executor: sb_node
steps:
- checkout
- attach_workspace:
@ -54,7 +61,7 @@ jobs:
- app
- lib
chromatic:
<<: *defaults
executor: sb_node
parallelism: 11
steps:
- checkout
@ -65,7 +72,7 @@ jobs:
command: |
yarn run-chromatics
packtracker:
<<: *defaults
executor: sb_node
steps:
- checkout
- attach_workspace:
@ -76,7 +83,7 @@ jobs:
cd examples/official-storybook
yarn packtracker
examples:
<<: *defaults
executor: sb_node
parallelism: 11
steps:
- checkout
@ -91,7 +98,7 @@ jobs:
paths:
- built-storybooks
publish:
<<: *defaults
executor: sb_node
steps:
- checkout
- attach_workspace:
@ -159,7 +166,7 @@ jobs:
command: yarn info @storybook/core
- run:
name: run e2e tests
command: yarn test:e2e-framework yarn2Cra
command: yarn test:e2e-framework --use-yarn-2 sfcVue cra
- store_artifacts:
path: /tmp/storybook/cypress
destination: cypress
@ -183,9 +190,12 @@ jobs:
- run:
name: cypress run
command: yarn test:e2e
- store_artifacts:
path: /tmp/storybook/cypress
destination: cypress
smoke-tests:
<<: *defaults
executor: sb_node
steps:
- checkout
- attach_workspace:
@ -251,7 +261,7 @@ jobs:
cd examples/cra-react15
yarn storybook --smoke-test --quiet
frontpage:
<<: *defaults
executor: sb_node
steps:
- checkout
- restore_cache:
@ -264,22 +274,10 @@ jobs:
- run:
name: Trigger build
command: ./scripts/build-frontpage.js
docs:
<<: *defaults
steps:
- checkout
- run:
name: Install dependencies
command: |
cd docs
yarn install
- run:
name: Build docs
command: |
cd docs
yarn build
lint:
<<: *defaults
executor:
class: small
name: sb_node
steps:
- checkout
- attach_workspace:
@ -288,7 +286,7 @@ jobs:
name: Lint
command: yarn lint
test:
<<: *defaults
executor: sb_node
steps:
- checkout
- attach_workspace:
@ -301,7 +299,9 @@ jobs:
paths:
- coverage
coverage:
<<: *defaults
executor:
class: small
name: sb_node
steps:
- checkout
- attach_workspace:
@ -352,5 +352,4 @@ workflows:
- publish
deploy:
jobs:
- docs
- frontpage

View File

@ -49,5 +49,11 @@ module.exports = {
'spaced-comment': 'off',
},
},
{
files: ['**/mithril/**/*'],
rules: {
'react/no-unknown-property': 'off', // Need to deactivate otherwise eslint replaces some unknown properties with React ones
},
},
],
};

View File

@ -1,7 +1,6 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
@ -9,6 +8,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@ -24,7 +24,7 @@ If applicable, add screenshots to help explain your problem.
If applicable, add code samples to help explain your problem.
**System:**
Please paste the results of `npx -p @storybook/cli@next sb info` here.
Please paste the results of `npx sb@next info` here.
**Additional context**
Add any other context about the problem here.

View File

@ -1,5 +1,5 @@
'app: angular': ['kroeder', 'igor-dv', 'MaximSagan']
'app: ember': ['gabrielcsapo']
'app: angular': ['kroeder', 'igor-dv', 'MaximSagan', 'Marklb']
'app: ember': ['gabrielcsapo', 'gossi', 'meirish']
'app: html': ['Hypnosphi']
'app: marko': ['nm123github']
'app: preact': ['BartWaardenburg']
@ -16,8 +16,9 @@
typescript: ['kroeder', 'gaetanmaisse', 'ndelangen', 'emilio-martinez']
theming: ['ndelangen', 'domyen']
cra: ['mrmckeb']
cli: ['Keraito']
cli: ['yannbf', 'tooppaaa']
dependencies: ['ndelangen']
'babel / webpack': ['ndelangen', 'igor-dv', 'shilman']
'yarn / npm': ['ndelangen', 'tmeasday']
'source-loader': ['libetl']
documentation: ['jonniebigodes']

View File

@ -17,6 +17,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: increase filewatcher limit
run: |
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
- uses: actions/checkout@v2
- name: Cache node modules
uses: actions/cache@v1
@ -32,40 +35,3 @@ jobs:
- name: cli
run: |
yarn test --cli
cli-yarn-2:
name: CLI Fixtures with Yarn 2
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v2
- name: Cache node modules
uses: actions/cache@v1
with:
path: node_modules
key: build-v2-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
build-v2-${{ env.cache-name }}-
build-v2-
- name: install, bootstrap
run: |
yarn bootstrap --core
- name: cli with Yarn 2
run: |
cd lib/cli
yarn test-yarn-2
latest-cra:
name: Latest CRA
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v2
- name: install, bootstrap
run: |
yarn bootstrap --core
- name: latest-cra
run: |
yarn test-latest-cra

View File

@ -1,24 +1,24 @@
## Addon / Framework Support Table
| | [React](app/react) | [React Native](app/react-native) | [Vue](app/vue) | [Angular](app/angular) | [Mithril](app/mithril) | [HTML](app/html) | [Marko](app/marko) | [Svelte](app/svelte) | [Riot](app/riot) | [Ember](app/ember) | [Preact](app/preact) | [Rax](app/rax) |
| ------------------------------------------- | :----------------: | :------------------------------: | :------------: | :--------------------: | :--------------------: | :--------------: | :----------------: | :------------------: | :--------------: | :----------------: | :------------------: | -------------- |
| [a11y](addons/a11y) | + | | + | + | + | + | + | + | + | + | + | + |
| [actions](addons/actions) | + | +\* | + | + | + | + | + | + | + | + | + | + |
| [backgrounds](addons/backgrounds) | + | \* | + | + | + | + | + | + | + | + | + | + |
| [cssresources](addons/cssresources) | + | | + | + | + | + | + | + | + | + | + | + |
| [design assets](addons/design-assets) | + | | + | + | + | + | + | + | + | + | + | + |
| [docs](addons/docs) | + | | + | + | + | + | + | + | + | + | + | + |
| [events](addons/events) | + | | + | + | + | + | + | | | + | + | + |
| [google-analytics](addons/google-analytics) | + | + | + | + | + | + | + | + | + | + | + | + |
| [graphql](addons/graphql) | + | | | | | | | | | | | |
| [jest](addons/jest) | + | + | + | + | + | + | + | + | + | + | + | + |
| [knobs](addons/knobs) | + | +\* | + | + | + | + | + | + | + | + | + | + |
| [links](addons/links) | + | + | + | + | + | + | | + | + | + | + | + |
| [options](addons/options) | + | + | + | + | + | + | | + | + | + | + | + |
| [query params](addons/queryparams) | + | | + | + | + | + | + | + | + | + | + | + |
| [storyshots](addons/storyshots) | + | + | + | + | | + | | + | + | | + | + |
| [storysource](addons/storysource) | + | | + | + | + | + | + | + | + | + | + | + |
| [viewport](addons/viewport) | + | | + | + | + | + | + | + | + | + | + | + |
| | [React](app/react) | [React Native](app/react-native) | [Vue](app/vue) | [Angular](app/angular) | [Mithril](app/mithril) | [HTML](app/html) | [Web Components](app/html) | [Marko](app/marko) | [Svelte](app/svelte) | [Riot](app/riot) | [Ember](app/ember) | [Preact](app/preact) | [Rax](app/rax) |
| ------------------------------------------- | :----------------: | :------------------------------: | :------------: | :--------------------: | :--------------------: | :--------------: | :------------------------: | :----------------: | :------------------: | :--------------: | :----------------: | :------------------: | -------------- |
| [a11y](addons/a11y) | + | | + | + | + | + | + | + | + | + | + | + | + |
| [actions](addons/actions) | + | +\* | + | + | + | + | + | + | + | + | + | + | + |
| [backgrounds](addons/backgrounds) | + | \* | + | + | + | + | + | + | + | + | + | + | + |
| [cssresources](addons/cssresources) | + | | + | + | + | + | + | + | + | + | + | + | + |
| [design assets](addons/design-assets) | + | | + | + | + | + | + | + | + | + | + | + | + |
| [docs](addons/docs) | + | | + | + | + | + | + | + | + | + | + | + | + |
| [events](addons/events) | + | | + | + | + | + | + | + | | | + | + | + |
| [google-analytics](addons/google-analytics) | + | + | + | + | + | + | + | + | + | + | + | + | + |
| [graphql](addons/graphql) | + | | | | | | | | | | | | |
| [jest](addons/jest) | + | + | + | + | + | + | + | + | + | + | + | + | + |
| [knobs](addons/knobs) | + | +\* | + | + | + | + | + | + | + | + | + | + | + |
| [links](addons/links) | + | + | + | + | + | + | + | | + | + | + | + | + |
| [options](addons/options) | + | + | + | + | + | + | + | | + | + | + | + | + |
| [query params](addons/queryparams) | + | | + | + | + | + | + | + | + | + | + | + | + |
| [storyshots](addons/storyshots) | + | + | + | + | | + | + | | + | + | | + | + |
| [storysource](addons/storysource) | + | | + | + | + | + | + | + | + | + | + | + | + |
| [viewport](addons/viewport) | + | | + | + | + | + | + | + | + | + | + | + | + |
`*` - React Native on device addon (addons/onDevice-\<name>)
@ -26,7 +26,7 @@
| | [React](app/react) | [React Native](app/react-native) | [Vue](app/vue) | [Angular](app/angular) | [Mithril](app/mithril) | [HTML](app/html) | [Marko](app/marko) | [Svelte](app/svelte) | [Riot](app/riot) | [Ember](app/ember) | [Preact](app/preact) | [Rax](app/rax) |
| ------------------------------------------- | :----------------: | :------------------------------: | :------------: | :--------------------: | :--------------------: | :--------------: | :----------------: | :------------------: | :--------------: | :----------------: | :------------------: | -------------- |
| [info](https://github.com/storybookjs/storybook/tree/master/addons/info) | + | | | | | | | | | | | |
| [notes](https://github.com/storybookjs/storybook/tree/master/addons/notes) | + | +\* | + | + | + | + | | + | + | + | + | + |
| [info](https://github.com/storybookjs/deprecated-addons/tree/master/addons/info) | + | | | | | | | | | | | |
| [notes](https://github.com/storybookjs/deprecated-addons/tree/master/addons/notes) | + | +\* | + | + | + | + | | + | + | + | + | + |
`*` - React Native on device addon (addons/onDevice-\<name>)

File diff suppressed because it is too large Load Diff

View File

@ -362,6 +362,12 @@ Save and go to `http://localhost:9011` (or wherever storybook is running)
If you don't see the changes rerun `yarn storybook` again in your sandbox app
### Documentation
The documentation for Storybook is served by the [frontpage](https://github.com/storybookjs/frontpage), but the docs files are in this repository.
To see changes in a development version of the docs, use the "linking" method documented [here](https://github.com/storybookjs/frontpage#docs-content).
## Release Guide
This section is for Storybook maintainers who will be creating releases. It assumes:

View File

@ -1,21 +1,32 @@
<h1>Migration</h1>
- [From version 6.0.x to 6.1.0](#from-version-60x-to-610)
- [6.1 deprecations](#61-deprecations)
- [Deprecated onBeforeRender](#deprecated-onbeforerender)
- [From version 5.3.x to 6.0.x](#from-version-53x-to-60x)
- [Hoisted CSF annotations](#hoisted-csf-annotations)
- [Zero config typescript](#zero-config-typescript)
- [Correct globs in main.js](#correct-globs-in-mainjs)
- [Backgrounds addon has a new api](#backgrounds-addon-has-a-new-api)
- [CRA preset removed](#cra-preset-removed)
- [Core-JS dependency errors](#core-js-dependency-errors)
- [Args passed as first argument to story](#args-passed-as-first-argument-to-story)
- [6.0 Docs breaking changes](#60-docs-breaking-changes)
- [Remove framework-specific docs presets](#remove-framework-specific-docs-presets)
- [Preview/Props renamed](#previewprops-renamed)
- [Docs theme separated](#docs-theme-separated)
- [DocsPage slots removed](#docspage-slots-removed)
- [React prop tables with Typescript](#react-prop-tables-with-typescript)
- [ConfigureJSX true by default in React](#configurejsx-true-by-default-in-react)
- [User babelrc disabled by default in MDX](#user-babelrc-disabled-by-default-in-mdx)
- [Docs description parameter](#docs-description-parameter)
- [6.0 Inline stories](#60-inline-stories)
- [New addon presets](#new-addon-presets)
- [Removed babel-preset-vue from Vue preset](#removed-babel-preset-vue-from-vue-preset)
- [Removed Deprecated APIs](#removed-deprecated-apis)
- [New setStories event](#new-setstories-event)
- [Removed renderCurrentStory event](#removed-rendercurrentstory-event)
- [Removed hierarchy separators](#removed-hierarchy-separators)
- [No longer pass denormalized parameters to storySort](#no-longer-pass-denormalized-parameters-to-storysort)
- [Client API changes](#client-api-changes)
- [Removed Legacy Story APIs](#removed-legacy-story-apis)
- [Can no longer add decorators/parameters after stories](#can-no-longer-add-decoratorsparameters-after-stories)
@ -24,15 +35,24 @@
- [Story Store immutable outside of configuration](#story-store-immutable-outside-of-configuration)
- [Improved story source handling](#improved-story-source-handling)
- [6.0 Addon API changes](#60-addon-api-changes)
- [Consistent local addon paths in main.js](#consistent-local-addon-paths-in-mainjs)
- [Deprecated setAddon](#deprecated-setaddon)
- [Deprecated disabled parameter](#deprecated-disabled-parameter)
- [Actions addon uses parameters](#actions-addon-uses-parameters)
- [Removed action decorator APIs](#removed-action-decorator-apis)
- [Removed withA11y decorator](#removed-witha11y-decorator)
- [Essentials addon disables differently](#essentials-addon-disables-differently)
- [Backgrounds addon has a new api](#backgrounds-addon-has-a-new-api)
- [6.0 Deprecations](#60-deprecations)
- [Deprecated addon-info, addon-notes](#deprecated-addon-info-addon-notes)
- [Deprecated addon-contexts](#deprecated-addon-contexts)
- [Removed addon-centered](#removed-addon-centered)
- [Deprecated polymer](#deprecated-polymer)
- [Deprecated immutable options parameters](#deprecated-immutable-options-parameters)
- [Deprecated addParameters and addDecorator](#deprecated-addparameters-and-adddecorator)
- [Deprecated clearDecorators](#deprecated-cleardecorators)
- [Deprecated configure](#deprecated-configure)
- [Deprecated support for duplicate kinds](#deprecated-support-for-duplicate-kinds)
- [From version 5.2.x to 5.3.x](#from-version-52x-to-53x)
- [To main.js configuration](#to-mainjs-configuration)
- [Using main.js](#using-mainjs)
@ -112,6 +132,16 @@
- [Packages renaming](#packages-renaming)
- [Deprecated embedded addons](#deprecated-embedded-addons)
## From version 6.0.x to 6.1.0
### 6.1 deprecations
#### Deprecated onBeforeRender
The `@storybook/addon-docs` previously accepted a `jsx` option called `onBeforeRender`, which was unfortunately named as it was called after the render.
We've renamed it `transformSource` and also allowed it to receive the `StoryContext` in case source rendering requires additional information.
## From version 5.3.x to 6.0.x
### Hoisted CSF annotations
@ -142,7 +172,7 @@ Basic.decorators = [ ... ];
2. Similar to React's `displayName`, `propTypes`, `defaultProps` annotations
3. We're introducing a new feature, [Storybook Args](https://docs.google.com/document/d/1Mhp1UFRCKCsN8pjlfPdz8ZdisgjNXeMXpXvGoALjxYM/edit?usp=sharing), where the new syntax will be significantly more ergonomic
To help you upgrade your stories, we've crated a codemod:
To help you upgrade your stories, we've created a codemod:
```
npx @storybook/cli@next migrate csf-hoist-story-annotations --glob="**/*.stories.js"
@ -156,7 +186,7 @@ Storybook has built-in Typescript support in 6.0. That means you should remove y
To migrate from an old setup, we recommend deleting any typescript-specific webpack/babel configurations in your project. You should also remove `@storybook/preset-typescript`, which is superceded by the built-in configuration.
If you want to override the defaults, see the [typescript configuration docs](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/configurations/typescript-config/index.md).
If you want to override the defaults, see the [typescript configuration docs](https://storybook.js.org/docs/react/configure/typescript).
### Correct globs in main.js
@ -178,52 +208,31 @@ Example of a **valid** glob:
stories: ['./**/*.stories.@(ts|js)']
```
### Backgrounds addon has a new api
Starting in 6.0, the backgrounds addon now receives an object instead of an array as parameter, with a property to define the default background.
Consider the following example of its usage in `Button.stories.js`:
```jsx
// Button.stories.js
export default {
title: 'Button',
parameters: {
backgrounds: [
{ name: 'twitter', value: '#00aced', default: true },
{ name: 'facebook', value: '#3b5998' },
],
},
};
```
Here's an updated version of the example, using the new api:
```jsx
// Button.stories.js
export default {
title: 'Button',
parameters: {
backgrounds: {
default: 'twitter',
values: [
{ name: 'twitter', value: '#00aced' },
{ name: 'facebook', value: '#3b5998' },
],
},
},
};
```
### CRA preset removed
The built-in create-react-app preset, which was [previously deprecated](#create-react-app-preset), has been fully removed.
If you're using CRA and migrating from an earlier Storybook version, please install [`@storybook/preset-create-react-app`](https://github.com/storybookjs/presets/tree/master/packages/preset-create-react-app) if you haven't already.
### Core-JS dependency errors
Some users have experienced `core-js` dependency errors when upgrading to 6.0, such as:
```
Module not found: Error: Can't resolve 'core-js/modules/web.dom-collections.iterator'
```
We think this comes from having multiple versions of `core-js` installed, but haven't isolated a good solution (see [#11255](https://github.com/storybookjs/storybook/issues/11255) for discussion).
For now, the workaround is to install `core-js` directly in your project as a dev dependency:
```sh
npm install core-js@^3.0.1 --save-dev
```
### Args passed as first argument to story
Starting in 6.0, the first argument to a story function is an [Args object](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/formats/component-story-format/index.md#args-story-inputs). In 5.3 and earlier, the first argument was a [StoryContext](https://github.com/storybookjs/storybook/blob/next/lib/addons/src/types.ts#L49-L61), and that context is now passed as the second argument by default.
Starting in 6.0, the first argument to a story function is an [Args object](https://storybook.js.org/docs/react/api/csf#args-story-inputs). In 5.3 and earlier, the first argument was a [StoryContext](https://github.com/storybookjs/storybook/blob/next/lib/addons/src/types.ts#L49-L61), and that context is now passed as the second argument by default.
This breaking change only affects you if your stories actually use the context, which is not common. If you have any stories that use the context, you can either (1) update your stories, or (2) set a flag to opt-out of new behavior.
@ -253,6 +262,10 @@ export const parameters = {
In SB 5.2, each framework had its own preset, e.g. `@storybook/addon-docs/react/preset`. In 5.3 we [unified this into a single preset](#unified-docs-preset): `@storybook/addon-docs/preset`. In 6.0 we've removed the deprecated preset.
#### Preview/Props renamed
In 6.0 we renamed `Preview` to `Canvas`, `Props` to `ArgsTable`. The change should be otherwise backwards-compatible.
#### Docs theme separated
In 6.0, you should theme Storybook Docs with the `docs.theme` parameter.
@ -265,7 +278,7 @@ In SB5.2, we introduced the concept of [DocsPage slots](https://github.com/story
In 5.3, we introduced `docs.x` story parameters like `docs.prepareForInline` which get filled in by frameworks and can also be overwritten by users, which is a more natural/convenient way to make global customizations.
We also introduced introduced [Custom DocsPage](https://github.com/storybookjs/storybook/blob/next/addons/docs/docs/docspage.md#replacing-docspage), which makes it possible to add/remove/update DocBlocks on the page.
We also introduced [Custom DocsPage](https://github.com/storybookjs/storybook/blob/next/addons/docs/docs/docspage.md#replacing-docspage), which makes it possible to add/remove/update DocBlocks on the page.
These mechanisms are superior to slots, so we've removed slots in 6.0. For each slot, we provide a migration path here:
@ -286,13 +299,73 @@ Starting in 6.0, we have [zero-config typescript support](#zero-config-typescrip
There are also two typescript handling options that can be set in `.storybook/main.js`. `react-docgen-typescript` (default) and `react-docgen`. This is [discussed in detail in the docs](https://github.com/storybookjs/storybook/blob/next/addons/docs/react/README.md#typescript-props-with-react-docgen).
#### ConfigureJSX true by default in React
In SB 6.0, the Storybook Docs preset option `configureJSX` is now set to `true` for all React projects. It was previously `false` by default for React only in 5.x). This `configureJSX` option adds `@babel/plugin-transform-react-jsx`, to process the output of the MDX compiler, which should be a safe change for all projects.
If you need to restore the old JSX handling behavior, you can configure `.storybook/main.js`:
```js
module.exports = {
addons: [
{
name: '@storybook/addon-docs',
options: { configureJSX: false },
},
],
};
```
#### User babelrc disabled by default in MDX
In SB 6.0, the Storybook Docs no longer applies the user's babelrc by default when processing MDX files. It caused lots of hard-to-diagnose bugs.
To restore the old behavior, or pass any MDX-specific babel options, you can configure `.storybook/main.js`:
```js
module.exports = {
addons: [
{
name: '@storybook/addon-docs',
options: { mdxBabelOptions: { babelrc: true, configFile: true } },
},
],
};
```
#### Docs description parameter
In 6.0, you can customize a component description using the `docs.description.component` parameter, and a story description using `docs.description.story` parameter.
Example:
```js
import { Button } from './Button';
export default {
title: 'Button'
parameters: { docs: { description: { component: 'some component **markdown**' }}}
}
export const Basic = () => <Button />
Basic.parameters = { docs: { description: { story: 'some story **markdown**' }}}
```
In 5.3 you customized a story description with the `docs.storyDescription` parameter. This has been deprecated, and support will be removed in 7.0.
#### 6.0 Inline stories
The following frameworks now render stories inline on the Docs tab by default, rather than in an iframe: `react`, `vue`, `web-components`, `html`.
To disable inline rendering, set the `docs.inlineStories` parameter to `false`.
### New addon presets
In Storybook 5.3 we introduced a declarative [main.js configuration](#to-mainjs-configuration), which is now the recommended way to configure Storybook. Part of the change is a simplified syntax for registering addons, which in 6.0 automatically registers many addons _using a preset_, which is a slightly different behavior than in earlier versions.
This breaking change currently applies to: `addon-a11y`, `addon-actions`, `addon-knobs`, `addon-links`, `addon-queryparams`.
Consider the following `main.js` config for the accessibility addon, `addon-knobs`:
Consider the following `main.js` config for `addon-knobs`:
```js
module.exports = {
@ -385,6 +458,36 @@ const parameters = combineParameters(
);
```
### Removed renderCurrentStory event
The story store no longer emits `renderCurrentStory`/`RENDER_CURRENT_STORY` to tell the renderer to render the story. Instead it emits a new declarative `CURRENT_STORY_WAS_SET` (in response to the existing `SET_CURRENT_STORY`) which is used to decide to render.
### Removed hierarchy separators
We've removed the ability to specify the hierarchy separators (how you control the grouping of story kinds in the sidebar). From Storybook 6.0 we have a single separator `/`, which cannot be configured.
If you are currently using custom separators, we encourage you to migrate to using `/` as the sole separator. If you are using `|` or `.` as a separator currently, we provide a codemod, [`upgrade-hierarchy-separators`](https://github.com/storybookjs/storybook/blob/next/lib/codemod/README.md#upgrade-hierarchy-separators), that can be used to rename your components. **Note: the codemod will not work for `.mdx` components, you will need to make the changes by hand.**
```
npx sb@next migrate upgrade-hierarchy-separators --glob="*/**/*.stories.@(tsx|jsx|ts|js)"
```
We also now default to showing "roots", which are non-expandable groupings in the sidebar for the top-level groups. If you'd like to disable this, set the `showRoots` option in `.storybook/manager.js`:
```js
import addons from '@storybook/addons';
addons.setConfig({
showRoots: false,
});
```
### No longer pass denormalized parameters to storySort
The `storySort` function (set via the `parameters.options.storySort` parameter) previously compared two entries `[storyId, storeItem]`, where `storeItem` included the full "denormalized" set of parameters of the story (i.e. the global, kind and story parameters that applied to that story).
For performance reasons, we now store the parameters uncombined, and so pass the format: `[storyId, storeItem, kindParameters, globalParameters]`.
### Client API changes
#### Removed Legacy Story APIs
@ -414,6 +517,8 @@ export StoryOne = ...;
StoryOne.story = { parameters: { ...commonParameters, other: 'things' } };
```
> NOTE: also the use of `addParameters` and `addDecorator` at arbitrary points is also deprecated, see [the deprecation warning](#deprecated-addparameters-and-adddecorator).
#### Changed Parameter Handling
There have been a few rationalizations of parameter handling in 6.0 to make things more predictable and fit better with the intention of parameters:
@ -484,12 +589,52 @@ The MDX analog:
### 6.0 Addon API changes
#### Consistent local addon paths in main.js
If you use `.storybook/main.js` config and have locally-defined addons in your project, you need to update your file paths.
In 5.3, `addons` paths were relative to the project root, which was inconsistent with `stories` paths, which were relative to the `.storybook` folder. In 6.0, addon paths are now relative to the config folder.
So, for example, if you had:
```js
module.exports = { addons: ['./.storybook/my-local-addon/register'] };
```
You'd need to update this to:
```js
module.exports = { addons: ['./my-local-addon/register'] };
```
#### Deprecated setAddon
We've deprecated the `setAddon` method of the `storiesOf` API and plan to remove it in 7.0.
Since early versions, Storybook shipped with a `setAddon` API, which allows you to extend `storiesOf` with arbitrary code. We've removed this from all core addons long ago and recommend writing stories in [Component Story Format](https://medium.com/storybookjs/component-story-format-66f4c32366df) rather than using the internal Storybook API.
#### Deprecated disabled parameter
Starting in 6.0.17, we've renamed the `disabled` parameter to `disable` to resolve an inconsistency where `disabled` had been used to hide the addon panel, whereas `disable` had been used to disable an addon's execution. Since `disable` was much more widespread in the code, we standardized on that.
So, for example:
```
Story.parameters = { actions: { disabled: true } }
```
Should be rewritten as:
```
Story.parameters = { actions: { disable: true } }
```
#### Actions addon uses parameters
Leveraging the new preset `@storybook/addon-actions` uses parameters to pass action options. If you previously had:
```js
import { withactions } from `@storybook/addon-actions`;
import { withActions } from `@storybook/addon-actions`;
export StoryOne = ...;
StoryOne.story = {
@ -515,17 +660,16 @@ In 6.0 we removed the actions addon decorate API. Actions handles can be configu
In 6.0 we removed the `withA11y` decorator. The code that runs accessibility checks is now directly injected in the preview.
Remove the addon-a11y decorator.
To configure a11y now, you have to specify configuration using `addParameters`.
To configure a11y now, you have to specify configuration using story parameters, e.g. in `.storybook/preview.js`:
```js
addParameters({
export const parameters = {
a11y: {
element: "#root",
element: '#root',
config: {},
options: {},
manual: true,
}
},
};
```
@ -533,6 +677,48 @@ addParameters({
In 6.0, `addon-essentials` doesn't configure addons if the user has already configured them in `main.js`. In 5.3 it previously checked to see whether the package had been installed in `package.json` to disable configuration. The new setup is preferably because now users' can install essential packages and import from them without disabling their configuration.
#### Backgrounds addon has a new api
Starting in 6.0, the backgrounds addon now receives an object instead of an array as parameter, with a property to define the default background.
Consider the following example of its usage in `Button.stories.js`:
```jsx
// Button.stories.js
export default {
title: 'Button',
parameters: {
backgrounds: [
{ name: 'twitter', value: '#00aced', default: true },
{ name: 'facebook', value: '#3b5998' },
],
},
};
```
Here's an updated version of the example, using the new api:
```jsx
// Button.stories.js
export default {
title: 'Button',
parameters: {
backgrounds: {
default: 'twitter',
values: [
{ name: 'twitter', value: '#00aced' },
{ name: 'facebook', value: '#3b5998' },
],
},
},
};
```
In addition, backgrounds now ships with the following defaults:
- no selected background (transparent)
- light/dark options
### 6.0 Deprecations
We've deprecated the following in 6.0: `addon-info`, `addon-notes`, `addon-contexts`, `addon-centered`, `polymer`.
@ -551,7 +737,7 @@ The addon's source code is still available in the [deprecated-addons repo](https
#### Removed addon-centered
In 6.0 we removed the centered addon. Centering is now core feature of storybook, so w no longer need an addon.
In 6.0 we removed the centered addon. Centering is now core feature of storybook, so we no longer need an addon.
Remove the addon-centered decorator and instead add a `layout` parameter:
@ -568,6 +754,80 @@ Other possible values are: `padded` (default) and `fullscreen`.
We've deprecated `@storybook/polymer` and are focusing on `@storybook/web-components`. If you use Polymer and are interested in maintaining it, please get in touch on [our Discord](https://discordapp.com/invite/UUt2PJb).
#### Deprecated immutable options parameters
The UI options `sidebarAnimations`, `enableShortcuts`, `theme`, `showRoots` should not be changed on a per-story basis, and as such there is no reason to set them via parameters.
You should use `addon.setConfig` to set them:
```js
// in .storybook/manager.js
import addons from '@storybook/addons';
addons.setConfig({
showRoots: false,
});
```
#### Deprecated addParameters and addDecorator
The `addParameters` and `addDecorator` APIs to add global decorators and parameters, exported by the various frameworks (e.g. `@storybook/react`) and `@storybook/client` are now deprecated.
Instead, use `export const parameters = {};` and `export const decorators = [];` in your `.storybook/preview.js`. Addon authors similarly should use such an export in a preview entry file (see [Preview entries](https://github.com/storybookjs/storybook/blob/next/docs/api/writing-presets.md#preview-entries)).
#### Deprecated clearDecorators
Similarly, `clearDecorators`, exported by the various frameworks (e.g. `@storybook/react`) is deprecated.
#### Deprecated configure
The `configure` API to load stories from `preview.js`, exported by the various frameworks (e.g. `@storybook/react`) is now deprecated.
To load stories, use the `stories` field in `main.js`. You can pass a glob or array of globs to load stories like so:
```js
// in .storybook/main.js
module.exports = {
stories: ['../src/**/*.stories.js'],
};
```
You can also pass an array of single file names if you want to be careful about loading files:
```js
// in .storybook/main.js
module.exports = {
stories: [
'../src/components/Button.stories.js',
'../src/components/Table.stories.js',
'../src/components/Page.stories.js',
],
};
```
#### Deprecated support for duplicate kinds
In 6.0 we deprecated the ability to split a kind's (component's) stories into multiple files because it was causing issues in hot module reloading (HMR). It will likely be removed completely in 7.0.
If you had N stories that contained `export default { title: 'foo/bar' }` (or the MDX equivalent `<Meta title="foo/bar">`), Storybook will now raise the warning `Duplicate title '${kindName}' used in multiple files`.
To split a component's stories into multiple files, e.g. for the `foo/bar` example above:
- Create a single file with the `export default { title: 'foo/bar' }` export, which is the primary file
- Comment out or delete the default export from the other files
- Re-export the stories from the other files in the primary file
So the primary example might look like:
```js
export default { title: 'foo/bar' };
export * from './Bar1.stories'
export * from './Bar2.stories'
export * from './Bar3.stories'
export const SomeStory = () => ...;
```
## From version 5.2.x to 5.3.x
### To main.js configuration
@ -601,11 +861,11 @@ If you had a `presets.js` file before you can add the array of presets to the ma
module.exports = {
stories: ['../**/*.stories.js'],
addons: [
'@storybook/preset-create-react-app'
'@storybook/preset-create-react-app',
{
name: '@storybook/addon-docs',
options: { configureJSX: true }
}
options: { configureJSX: true },
},
],
};
```
@ -882,7 +1142,7 @@ module.exports = ({ config }) => ({
});
```
Please refer to the [current custom webpack documentation](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/configurations/custom-webpack-config/index.md) for more information on custom webpack config and to [Issue #6081](https://github.com/storybookjs/storybook/issues/6081) for more information about the change.
Please refer to the [current custom webpack documentation](https://storybook.js.org/docs/react/configure/webpack) for more information on custom webpack config and to [Issue #6081](https://github.com/storybookjs/storybook/issues/6081) for more information about the change.
## From version 4.1.x to 5.0.x
@ -923,11 +1183,11 @@ module.exports = ({ config, mode }) => { config.module.rules.push(...); return c
In contrast, the 4.x configuration function accepted either two or three arguments (`(baseConfig, mode)`, or `(baseConfig, mode, defaultConfig)`). The `config` object in the 5.x signature is equivalent to 4.x's `defaultConfig`.
Please see the [current custom webpack documentation](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/configurations/custom-webpack-config/index.md) for more information on custom webpack config.
Please see the [current custom webpack documentation](https://storybook.js.org/docs/react/configure/webpack) for more information on custom webpack config.
### Theming overhaul
Theming has been rewritten in v5. If you used theming in v4, please consult the [theming docs](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/configurations/theming/index.md) to learn about the new API.
Theming has been rewritten in v5. If you used theming in v4, please consult the [theming docs](https://storybook.js.org/docs/react/configure/theming) to learn about the new API.
### Story hierarchy defaults
@ -959,7 +1219,7 @@ addParameters({
### Options addon deprecated
In 4.x we added story parameters. In 5.x we've deprecated the options addon in favor of [global parameters](./docs/src/pages/configurations/options-parameter/index.md), and we've also renamed some of the options in the process (though we're maintaining backwards compatibility until 6.0).
In 4.x we added story parameters. In 5.x we've deprecated the options addon in favor of [global parameters](https://storybook.js.org/docs/react/configure/features-and-behavior), and we've also renamed some of the options in the process (though we're maintaining backwards compatibility until 6.0).
Here's an old configuration:
@ -1320,7 +1580,7 @@ The `@storybook/react-native` had built-in addons (`addon-actions` and `addon-li
### Webpack 4
Storybook now uses webpack 4. If you have a [custom webpack config](https://storybook.js.org/configurations/custom-webpack-config/), make sure that all the loaders and plugins you use support webpack 4.
Storybook now uses webpack 4. If you have a [custom webpack config](https://storybook.js.org/docs/react/configure/webpack), make sure that all the loaders and plugins you use support webpack 4.
### Babel 7
@ -1430,7 +1690,7 @@ This was done to support different major versions of babel.
### Base webpack config now contains vital plugins ([#1775](https://github.com/storybookjs/storybook/pull/1775))
This affects you if you use custom webpack config in [Full Control Mode](https://storybook.js.org/configurations/custom-webpack-config/#full-control-mode) while not preserving the plugins from `storybookBaseConfig`. Before `3.3`, preserving them was a recommendation, but now it [became](https://github.com/storybookjs/storybook/pull/2578) a requirement.
This affects you if you use custom webpack config in [Full Control Mode](https://storybook.js.org/docs/react/configure/webpack#full-control-mode) while not preserving the plugins from `storybookBaseConfig`. Before `3.3`, preserving them was a recommendation, but now it [became](https://github.com/storybookjs/storybook/pull/2578) a requirement.
### Refactored Knobs
@ -1500,7 +1760,7 @@ Now we use:
- `preview-head.html` for including extra content into the preview pane.
- `manager-head.html` for including extra content into the manager window.
[Read our docs](https://storybook.js.org/configurations/add-custom-head-tags/) for more details.
[Read our docs](https://storybook.js.org/docs/react/configure/story-rendering#adding-to-head) for more details.
## From version 2.x.x to 3.x.x

102
README.md
View File

@ -28,8 +28,8 @@
<a href="https://discord.gg/sMFvFsG">
<img src="https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=flat" />
</a>
<a href="https://now-examples-slackin-rrirkqohko.now.sh/">
<img src="https://now-examples-slackin-rrirkqohko.now.sh/badge.svg?logo=slack" alt="Storybook Slack" />
<a href="https://storybook.js.org/community/">
<img src="https://img.shields.io/badge/community-join-4BC424.svg" alt="Storybook Community" />
</a>
<a href="#backers">
<img src="https://opencollective.com/storybook/backers/badge.svg" alt="Backers on Open Collective" />
@ -59,24 +59,24 @@ It allows you to browse a component library, view the different states of each c
Storybook runs outside of your app. This allows you to develop UI components in isolation, which can improve component reuse, testability, and development speed. You can build quickly without having to worry about application-specific dependencies.
Here are some featured examples that you can reference to see how Storybook works: <https://storybook.js.org/examples/>
Here are some featured examples that you can reference to see how Storybook works: <https://storybook.js.org/docs/examples/>
Storybook comes with a lot of [addons](https://storybook.js.org/addons/introduction/) for component design, documentation, testing, interactivity, and so on. Storybook's API makes it possible to configure and extend in various ways. It has even been extended to support React Native development for mobile.
Storybook comes with a lot of [addons](https://storybook.js.org/docs/react/configure/storybook-addons) for component design, documentation, testing, interactivity, and so on. Storybook's API makes it possible to configure and extend in various ways. It has even been extended to support React Native development for mobile.
## Table of contents
- 🚀[Getting Started](#getting-started)
- 📒[Projects](#projects)
- 🛠[Supported Frameworks & Examples](#supported-frameworks)
- 🚇[Sub Projects](#sub-projects)
- 🚀 [Getting Started](#getting-started)
- 📒 [Projects](#projects)
- 🛠 [Supported Frameworks & Examples](#supported-frameworks)
- 🚇[ Sub Projects](#sub-projects)
- 🔗[Addons](#addons)
- 🏅[Badges & Presentation materials](#badges--presentation-materials)
- 👥[Community](#community)
- 👏[Contributing](#contributing)
- 👨‍💻[Development scripts](#development-scripts)
- 💵[Backers](#backers)
- 💸[Sponsors](#sponsors)
- :memo:[License](#license)
- 🏅 [Badges & Presentation materials](#badges--presentation-materials)
- 👥 [Community](#community)
- 👏 [Contributing](#contributing)
- 👨‍💻 [Development scripts](#development-scripts)
- 💵 [Backers](#backers)
- 💸 [Sponsors](#sponsors)
- :memo: [License](#license)
## Getting Started
@ -84,46 +84,36 @@ First install storybook:
```sh
cd my-react-app
npx -p @storybook/cli sb init
npx sb init
```
If you'd rather set up your project manually, take a look at our [Slow Start Guide](https://storybook.js.org/basics/slow-start-guide/).
If you'd rather set up your project manually, take a look at our [Slow Start Guide](https://storybook.js.org/docs/react/configure/overview).
Once it's installed, you can `npm run storybook` and it will run the development server on your local machine, and give you a URL to browse some sample stories.
**Storybook v2.x migration note**:
If you're using Storybook v2.x and want to shift to 4.x version the easiest way is:
```sh
cd my-storybook-v2-app
npx -p @storybook/cli sb init
```
It runs a codemod to update all package names. Read all migration details in our [Migration Guide](MIGRATION.md)
For full documentation on using Storybook visit: [storybook.js.org](https://storybook.js.org)
For additional help, join us [in our Discord](https://discord.gg/sMFvFsG) or [Slack](https://now-examples-slackin-rrirkqohko.now.sh/)
For additional help, join us [in our Discord](https://discord.gg/sMFvFsG) or [Slack (legacy)](https://now-examples-slackin-rrirkqohko.now.sh/)
## Projects
### Supported Frameworks
| Framework | Demo | |
| -------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [React](app/react) | [v5.3.0](https://storybookjs.netlify.com/official-storybook/?path=/story/*) | [![React](https://img.shields.io/npm/dm/@storybook/react.svg)](app/react) |
| [React Native](app/react-native) | - | [![React Native](https://img.shields.io/npm/dm/@storybook/react-native.svg)](app/react-native) |
| [Vue](app/vue) | [v5.3.0](https://storybookjs.netlify.com/vue-kitchen-sink/) | [![Vue](https://img.shields.io/npm/dm/@storybook/vue.svg)](app/vue) |
| [Angular](app/angular) | [v5.3.0](https://storybookjs.netlify.com/angular-cli/) | [![Angular](https://img.shields.io/npm/dm/@storybook/angular.svg)](app/angular) |
| [Marionette.js](app/marionette) | - | [![Marionette.js](https://img.shields.io/npm/dm/@storybook/marionette.svg)](app/marionette) |
| [Mithril](app/mithril) | [v5.3.0](https://storybookjs.netlify.com/mithril-kitchen-sink/) | [![Mithril](https://img.shields.io/npm/dm/@storybook/mithril.svg)](app/mithril) |
| [Marko](app/marko) | [v5.3.0](https://storybookjs.netlify.com/marko-cli/) | [![Marko](https://img.shields.io/npm/dm/@storybook/marko.svg)](app/marko) |
| [HTML](app/html) | [v5.3.0](https://storybookjs.netlify.com/html-kitchen-sink/) | [![HTML](https://img.shields.io/npm/dm/@storybook/html.svg)](app/html) |
| [Svelte](app/svelte) | [v5.3.0](https://storybookjs.netlify.com/svelte-kitchen-sink/) | [![Svelte](https://img.shields.io/npm/dm/@storybook/svelte.svg)](app/svelte) |
| [Riot](app/riot) | [v5.3.0](https://storybookjs.netlify.com/riot-kitchen-sink/) | [![Riot](https://img.shields.io/npm/dm/@storybook/riot.svg)](app/riot) |
| [Ember](app/ember) | [v5.3.0](https://storybookjs.netlify.com/ember-cli/) | [![Ember](https://img.shields.io/npm/dm/@storybook/ember.svg)](app/ember) |
| [Preact](app/preact) | [v5.3.0](https://storybookjs.netlify.com/preact-kitchen-sink/) | [![Preact](https://img.shields.io/npm/dm/@storybook/preact.svg)](app/preact) |
| [Rax](app/rax) | [v5.3.0](https://storybookjs.netlify.com/rax-kitchen-sink/) | [![Rax](https://img.shields.io/npm/dm/@storybook/rax.svg)](app/rax) |
| Framework | Demo | |
| ----------------------------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [React](app/react) | [v6.0.x](https://storybookjs.netlify.com/official-storybook/?path=/story/*) | [![React](https://img.shields.io/npm/dm/@storybook/react.svg)](app/react) |
| [React Native](https://github.com/storybookjs/react-native) | - | [![React Native](https://img.shields.io/npm/dm/@storybook/react-native.svg)](app/react-native) |
| [Vue](app/vue) | [v6.0.x](https://storybookjs.netlify.com/vue-kitchen-sink/) | [![Vue](https://img.shields.io/npm/dm/@storybook/vue.svg)](app/vue) |
| [Angular](app/angular) | [v6.0.x](https://storybookjs.netlify.com/angular-cli/) | [![Angular](https://img.shields.io/npm/dm/@storybook/angular.svg)](app/angular) |
| [Marionette.js](app/marionette) | - | [![Marionette.js](https://img.shields.io/npm/dm/@storybook/marionette.svg)](app/marionette) |
| [Mithril](app/mithril) | [v6.0.x](https://storybookjs.netlify.com/mithril-kitchen-sink/) | [![Mithril](https://img.shields.io/npm/dm/@storybook/mithril.svg)](app/mithril) |
| [Marko](app/marko) | [v6.0.x](https://storybookjs.netlify.com/marko-cli/) | [![Marko](https://img.shields.io/npm/dm/@storybook/marko.svg)](app/marko) |
| [HTML](app/html) | [v6.0.x](https://storybookjs.netlify.com/html-kitchen-sink/) | [![HTML](https://img.shields.io/npm/dm/@storybook/html.svg)](app/html) |
| [Svelte](app/svelte) | [v6.0.x](https://storybookjs.netlify.com/svelte-kitchen-sink/) | [![Svelte](https://img.shields.io/npm/dm/@storybook/svelte.svg)](app/svelte) |
| [Riot](app/riot) | [v6.0.x](https://storybookjs.netlify.com/riot-kitchen-sink/) | [![Riot](https://img.shields.io/npm/dm/@storybook/riot.svg)](app/riot) |
| [Ember](app/ember) | [v6.0.x](https://storybookjs.netlify.com/ember-cli/) | [![Ember](https://img.shields.io/npm/dm/@storybook/ember.svg)](app/ember) |
| [Preact](app/preact) | [v6.0.x](https://storybookjs.netlify.com/preact-kitchen-sink/) | [![Preact](https://img.shields.io/npm/dm/@storybook/preact.svg)](app/preact) |
| [Rax](app/rax) | [v6.0.x](https://storybookjs.netlify.com/rax-kitchen-sink/) | [![Rax](https://img.shields.io/npm/dm/@storybook/rax.svg)](app/rax) |
### Sub Projects
@ -146,7 +136,6 @@ For additional help, join us [in our Discord](https://discord.gg/sMFvFsG) or [Sl
| [jest](addons/jest/) | View the results of components' unit tests in Storybook |
| [knobs](addons/knobs/) | Interactively edit component prop data in the Storybook UI |
| [links](addons/links/) | Create links between stories |
| [options](addons/options/) | Customize the Storybook UI in code |
| [query params](addons/queryparams/) | Mock query params |
| [storyshots](addons/storyshots/) | Snapshot testing for components in Storybook |
| [storysource](addons/storysource/) | View the code of your stories within the Storybook UI |
@ -156,11 +145,12 @@ See [Addon / Framework Support Table](ADDONS_SUPPORT.md)
### Deprecated Addons
| Addons | |
| ------------------------------------------- | -------------------------------------------------------------------------- |
| [info](https://github.com/storybookjs/storybook/tree/master/addons/info) | Annotate stories with extra component usage information |
| [notes](https://github.com/storybookjs/storybook/tree/master/addons/notes) | Annotate Storybook stories with notes |
| [contexts](https://github.com/storybookjs/storybook/tree/master/addons/contexts) | Addon for driving your components under dynamic contexts |
| Addons | |
| ---------------------------------------------------------------------------------- | -------------------------------------------------------- |
| [info](https://github.com/storybookjs/deprecated-addons/tree/master/addons/info) | Annotate stories with extra component usage information |
| [notes](https://github.com/storybookjs/deprecated-addons/tree/master/addons/notes) | Annotate Storybook stories with notes |
| [contexts](https://github.com/storybookjs/storybook/tree/master/addons/contexts) | Addon for driving your components under dynamic contexts |
| [options](https://github.com/storybookjs/storybook/tree/master/addons/options/) | Customize the Storybook UI in code |
In order to continue improving your experience, we have to eventually deprecate certain addons in favor of new, better tools.
@ -184,8 +174,8 @@ If you're looking for material to use in your presentation about storybook, like
- Tweeting via [@storybookjs](https://twitter.com/storybookjs)
- Blogging at [Medium](https://medium.com/storybookjs)
- Chatting on [Slack](https://now-examples-slackin-rrirkqohko.now.sh/)
- Discussions on [Discord](https://discord.gg/sMFvFsG)
- Chatting on [Discord](https://discord.gg/sMFvFsG)
- Chatting (legacy) on [Slack](https://now-examples-slackin-rrirkqohko.now.sh/)
- Streaming saved at [Youtube](https://www.youtube.com/channel/UCr7Quur3eIyA_oe8FNYexfg)
## Contributing
@ -193,8 +183,8 @@ If you're looking for material to use in your presentation about storybook, like
We welcome contributions to Storybook!
- 📥 Pull requests and 🌟 Stars are always welcome.
- Read our [contributing guide](CONTRIBUTING.md) to get started.
or find us on [Discord](https://discord.gg/sMFvFsG), we're will take the time to guide you
- Read our [contributing guide](CONTRIBUTING.md) to get started,
or find us on [Discord](https://discord.gg/sMFvFsG), we will take the time to guide you
Looking for a first issue to tackle?
@ -209,18 +199,12 @@ Storybook is organized as a monorepo using [Lerna](https://lerna.js.org/). Usefu
> Installs package dependencies and links packages together - using lerna
#### `yarn run publish`
> Push a release to git and npm
> will ask for version in interactive mode - using lerna.
#### `yarn lint`
> boolean check if code conforms to linting rules - uses remark & eslint
- `yarn lint:js` - will check js
- `yarn lint:md` - will check markdown + code samples
- `yarn lint:js --fix` - will automatically fix js
#### `yarn test`

18
SECURITY.md Normal file
View File

@ -0,0 +1,18 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 6.0.x | :white_check_mark: |
| 5.3.x | :white_check_mark: |
## Reporting a Vulnerability
We rely on NPM's security advisory process for reporting vulnerabilities.
You can submit a vulnerability in a Storybook package at: https://www.npmjs.com/advisories/report
You can also reach out to the maintainers directly on Twitter: https://twitter.com/storybookjs
When we fix a security issue, we will post a security advisory on Github/NPM, describe the change in the [release notes](https://github.com/storybookjs/storybook/releases), and also announce notify the community on [our Discord](https://discord.com/invite/UUt2PJb).

View File

@ -0,0 +1,37 @@
import React from "react";
import { action } from "@storybook/addon-actions";
import { Button } from "@storybook/react/demo";
export default {
title: "Button",
excludeStories: ["text"],
includeStories: /emoji.*/
};
export const Basic = () => (
<Button onClick={action("clicked")}>Hello Button</Button>
);
export const WithParams = () => <Button>WithParams</Button>;
WithParams.parameters = { foo: 'bar' }
export const WithDocsParams = () => <Button>WithDocsParams</Button>;
WithDocsParams.parameters = { docs: { iframeHeight: 200 } };
export const WithStorySourceParams = () => <Button>WithStorySourceParams</Button>;
WithStorySourceParams.parameters = { storySource: { source: 'foo' } };
const Template = (args: Args) => <Button {...args} />;
export const WithTemplate = Template.bind({});
WithTemplate.args = { foo: 'bar' }
export const WithEmptyTemplate = Template.bind();
WithEmptyTemplate.args = { foo: 'baz' };
export const WithAddFunctionParameters = () => null
WithAddFunctionParameters.parameters = {
foobar: () => {
document.addEventListener('foo', () => console.log('bar'))
},
}

View File

@ -48,7 +48,7 @@ MyNonCheckedStory.parameters = {
## Parameters
For more customizability use parameters to configure [aXe options](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#api-name-axeconfigure).
You can override these options [at story level too](https://storybook.js.org/docs/configurations/options-parameter/#per-story-options).
You can override these options [at story level too](https://storybook.js.org/docs/react/configure/features-and-behavior#per-story-options.
```js
import React from 'react';

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-a11y",
"version": "6.0.0-beta.23",
"version": "6.1.0-alpha.14",
"description": "a11y addon for storybook",
"keywords": [
"a11y",
@ -27,24 +27,26 @@
"README.md",
"*.js",
"*.d.ts",
"ts3.5/**/*"
"ts3.4/**/*"
],
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.0.0-beta.23",
"@storybook/api": "6.0.0-beta.23",
"@storybook/channels": "6.0.0-beta.23",
"@storybook/client-api": "6.0.0-beta.23",
"@storybook/client-logger": "6.0.0-beta.23",
"@storybook/components": "6.0.0-beta.23",
"@storybook/core-events": "6.0.0-beta.23",
"@storybook/theming": "6.0.0-beta.23",
"axe-core": "^3.5.2",
"@storybook/addons": "6.1.0-alpha.14",
"@storybook/api": "6.1.0-alpha.14",
"@storybook/channels": "6.1.0-alpha.14",
"@storybook/client-api": "6.1.0-alpha.14",
"@storybook/client-logger": "6.1.0-alpha.14",
"@storybook/components": "6.1.0-alpha.14",
"@storybook/core-events": "6.1.0-alpha.14",
"@storybook/theming": "6.1.0-alpha.14",
"axe-core": "^4.0.1",
"core-js": "^3.0.1",
"global": "^4.3.2",
"lodash": "^4.17.15",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-sizeme": "^2.5.2",
"regenerator-runtime": "^0.13.3",
"ts-dedent": "^1.1.1",
@ -52,22 +54,16 @@
},
"devDependencies": {
"@testing-library/react": "^10.0.4",
"@types/webpack-env": "^1.15.2",
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
"@types/webpack-env": "^1.15.2"
},
"publishConfig": {
"access": "public"
},
"gitHead": "4b9d901add9452525135caae98ae5f78dd8da9ff",
"gitHead": "4571582a90b646e361cb37df525f62312486307a",
"typesVersions": {
"<=3.5": {
"<3.8": {
"*": [
"ts3.5/*"
"ts3.4/*"
]
}
}

View File

@ -2,21 +2,36 @@ import { document, window } from 'global';
import axe from 'axe-core';
import addons from '@storybook/addons';
import { EVENTS } from './constants';
import { Setup } from './params';
import { A11yParameters } from './params';
if (module && module.hot && module.hot.decline) {
module.hot.decline();
}
const channel = addons.getChannel();
// Holds axe core running state
let active = false;
// Holds latest story we requested a run
let activeStoryId: string | undefined;
const getElement = () => {
const storyRoot = document.getElementById('story-root');
return storyRoot ? storyRoot.children : document.getElementById('root');
};
/**
* Handle A11yContext events.
* Because the event are sent without manual check, we split calls
*/
const handleRequest = (storyId: string) => {
const { manual } = getParams(storyId);
if (!manual) {
run(storyId);
}
};
const run = async (storyId: string) => {
activeStoryId = storyId;
try {
const input = getParams(storyId);
@ -24,14 +39,23 @@ const run = async (storyId: string) => {
active = true;
channel.emit(EVENTS.RUNNING);
const { element = getElement(), config, options } = input;
const { element = getElement(), config, options = {} } = input;
axe.reset();
if (config) {
axe.configure(config);
}
const result = await axe.run(element, options);
channel.emit(EVENTS.RESULT, result);
// It's possible that we requested a new run on a different story.
// Unfortunately, axe doesn't support a cancel method to abort current run.
// We check if the story we run against is still the current one,
// if not, trigger a new run using the current story
if (activeStoryId === storyId) {
channel.emit(EVENTS.RESULT, result);
} else {
active = false;
run(activeStoryId);
}
}
} catch (error) {
channel.emit(EVENTS.ERROR, error);
@ -41,9 +65,8 @@ const run = async (storyId: string) => {
};
/** Returns story parameters or default ones. */
const getParams = (storyId: string): Setup => {
// eslint-disable-next-line no-underscore-dangle
const { parameters } = window.__STORYBOOK_STORY_STORE__._stories[storyId] || {};
const getParams = (storyId: string): A11yParameters => {
const { parameters } = window.__STORYBOOK_STORY_STORE__.fromId(storyId) || {};
return (
parameters.a11y || {
config: {},
@ -54,5 +77,5 @@ const getParams = (storyId: string): Setup => {
);
};
channel.on(EVENTS.REQUEST, run);
channel.on(EVENTS.REQUEST, handleRequest);
channel.on(EVENTS.MANUAL, run);

View File

@ -1,16 +1,16 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { styled } from '@storybook/theming';
import { ActionBar, Icons, ScrollArea } from '@storybook/components';
import { AxeResults } from 'axe-core';
import { useChannel, useParameter, useStorybookState, useAddonState } from '@storybook/api';
import { useChannel, useParameter, useStorybookState } from '@storybook/api';
import { Report } from './Report';
import { Tabs } from './Tabs';
import { useA11yContext } from './A11yContext';
import { EVENTS, ADDON_ID } from '../constants';
import { EVENTS } from '../constants';
import { A11yParameters } from '../params';
export enum RuleType {
@ -51,13 +51,13 @@ const Centered = styled.span<{}>({
type Status = 'initial' | 'manual' | 'running' | 'error' | 'ran' | 'ready';
export const A11YPanel: React.FC = () => {
const [status, setStatus] = useAddonState<Status>(ADDON_ID, 'initial');
const [error, setError] = React.useState<unknown>(undefined);
const { setResults, results } = useA11yContext();
const { storyId } = useStorybookState();
const { manual } = useParameter<Pick<A11yParameters, 'manual'>>('a11y', {
manual: false,
});
const [status, setStatus] = useState<Status>(manual ? 'manual' : 'initial');
const [error, setError] = React.useState<unknown>(undefined);
const { setResults, results } = useA11yContext();
const { storyId } = useStorybookState();
React.useEffect(() => {
setStatus(manual ? 'manual' : 'initial');

View File

@ -64,9 +64,9 @@ export const A11yContextProvider: React.FC<A11yContextProviderProps> = ({ active
: prevHighlighted.filter((t) => !target.includes(t))
);
}, []);
const handleRun = React.useCallback(() => {
emit(EVENTS.REQUEST, storyId);
}, [storyId]);
const handleRun = (renderedStoryId: string) => {
emit(EVENTS.REQUEST, renderedStoryId);
};
const handleClearHighlights = React.useCallback(() => setHighlighted([]), []);
const handleSetTab = React.useCallback((index: number) => {
handleClearHighlights();
@ -90,7 +90,7 @@ export const A11yContextProvider: React.FC<A11yContextProviderProps> = ({ active
React.useEffect(() => {
if (active) {
handleRun();
handleRun(storyId);
} else {
handleClearHighlights();
}

View File

@ -0,0 +1,83 @@
## Advanced/Legacy Actions usage
For basic usage, see the [documentation](https://storybook.js.org/docs/react/essentials/actions).
This document describes the pre-6.0 usage of the addon, and as such is no longer recommended (although it will be supported until at least 7.0).
## Manually-specified actions
Import the `action` function and use it to create actions handlers. When creating action handlers, provide a **name** to make it easier to identify.
> _Note: Make sure NOT to use reserved words as function names. [issues#29](https://github.com/storybookjs/storybook-addon-actions/issues/29#issuecomment-288274794)_
```js
import { action } from '@storybook/addon-actions';
import Button from './button';
export default {
title: 'Button',
component: Button,
};
export const defaultView = () => <Button onClick={action('button-click')}>Hello World!</Button>;
```
## Multiple actions
If your story requires multiple actions, it may be convenient to use `actions` to create many at once:
```js
import { actions } from '@storybook/addon-actions';
import Button from './button';
export default {
title: 'Button',
component: Button,
};
// This will lead to { onClick: action('onClick'), ... }
const eventsFromNames = actions('onClick', 'onMouseOver');
// This will lead to { onClick: action('clicked'), ... }
const eventsFromObject = actions({ onClick: 'clicked', onMouseOver: 'hovered' });
export const first = () => <Button {...eventsFromNames}>Hello World!</Button>;
export const second = () => <Button {...eventsFromObject}>Hello World!</Button>;
```
## Configuration
Arguments which are passed to the action call will have to be serialized while be "transferred" over the channel.
This is not very optimal and can cause lag when large objects are being logged, for this reason it is possible to configure a maximum depth.
The action logger, by default, will log all actions fired during the lifetime of the story. After a while this can make the storybook laggy. As a workaround, you can configure an upper limit to how many actions should be logged.
To apply the configuration globally use the `configureActions` function in your `preview.js` file.
```js
import { configureActions } from '@storybook/addon-actions';
configureActions({
depth: 100,
// Limit the number of items logged into the actions panel
limit: 20,
});
```
To apply the configuration per action use:
```js
action('my-action', {
depth: 5,
});
```
### Available Options
| Name | Type | Description | Default |
| -------------------- | ------- | ----------------------------------------------------------------------------------- | ------- |
| `depth` | Number | Configures the transferred depth of any logged objects. | `10` |
| `clearOnStoryChange` | Boolean | Flag whether to clear the action logger when switching away from the current story. | `true` |
| `limit` | Number | Limits the number of items logged in the action logger | `50` |

View File

@ -2,19 +2,19 @@
Storybook Addon Actions can be used to display data received by event handlers in [Storybook](https://storybook.js.org).
[Framework Support](https://github.com/storybookjs/storybook/blob/master/ADDONS_SUPPORT.md)
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
![Screenshot](https://raw.githubusercontent.com/storybookjs/storybook/HEAD/addons/actions/docs/screenshot.png)
## Getting Started
## Installation
Install:
Actions is part of [essentials](https://storybook.js.org/docs/react/essentials/introduction) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run:
```sh
npm i -D @storybook/addon-actions
```
Then, add following content to `.storybook/main.js`
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/overview#configure-your-storybook-project):
```js
module.exports = {
@ -22,132 +22,6 @@ module.exports = {
};
```
## Actions args
## Usage
Starting in SB6.0, we recommend using story parameters to specify actions which get passed into your story as [Args](https://docs.google.com/document/d/1Mhp1UFRCKCsN8pjlfPdz8ZdisgjNXeMXpXvGoALjxYM/edit?usp=sharing) (passed as the first argument when `passArgsFirst` is set to `true`).
The first option is to specify `argTypes` for your story with an `action` field. Take the following example:
```js
import Button from './button';
export default {
title: 'Button',
argTypes: { onClick: { action: 'clicked' } },
};
export const Basic = ({ onClick }) => <Button onClick={onClick}>Hello World!</Button>;
```
Alternatively, suppose you have a naming convention, like `onX` for event handlers. The following configuration automatically creates actions for each `onX` `argType` (which you can either specify manually or generate automatically using [Storybook Docs](https://www.npmjs.com/package/@storybook/addon-docs).
```js
import Button from './button';
export default {
title: 'Button',
component: Button,
parameters: { actions: { argTypesRegex: '^on.*' } },
};
export const Basic = ({ onClick }) => <Button onClick={onClick}>Hello World!</Button>;
```
> **NOTE:** If you're generating `argTypes` in using another addon (like Docs, which is the common behavior) you'll need to make sure that the actions addon loads **AFTER** the other addon. You can do this by listing it later in the `addons` registration code in `.storybook/main.js`.
## Manually-specified actions
Import the `action` function and use it to create actions handlers. When creating action handlers, provide a **name** to make it easier to identify.
> _Note: Make sure NOT to use reserved words as function names. [issues#29](https://github.com/storybookjs/storybook-addon-actions/issues/29#issuecomment-288274794)_
```js
import { action } from '@storybook/addon-actions';
import Button from './button';
export default {
title: 'Button',
component: Button,
};
export const defaultView = () => <Button onClick={action('button-click')}>Hello World!</Button>;
```
## Multiple actions
If your story requires multiple actions, it may be convenient to use `actions` to create many at once:
```js
import { actions } from '@storybook/addon-actions';
import Button from './button';
export default {
title: 'Button',
component: Button,
};
// This will lead to { onClick: action('onClick'), ... }
const eventsFromNames = actions('onClick', 'onMouseOver');
// This will lead to { onClick: action('clicked'), ... }
const eventsFromObject = actions({ onClick: 'clicked', onMouseOver: 'hovered' });
export const first = () => <Button {...eventsFromNames}>Hello World!</Button>;
export const second = () => <Button {...eventsFromObject}>Hello World!</Button>;
```
## Configuration
Arguments which are passed to the action call will have to be serialized while be "transferred" over the channel.
This is not very optimal and can cause lag when large objects are being logged, for this reason it is possible to configure a maximum depth.
The action logger, by default, will log all actions fired during the lifetime of the story. After a while this can make the storybook laggy. As a workaround, you can configure an upper limit to how many actions should be logged.
To apply the configuration globally use the `configureActions` function in your `preview.js` file.
```js
import { configureActions } from '@storybook/addon-actions';
configureActions({
depth: 100,
// Limit the number of items logged into the actions panel
limit: 20,
});
```
To apply the configuration per action use:
```js
action('my-action', {
depth: 5,
});
```
### Available Options
| Name | Type | Description | Default |
| -------------------- | ------- | ----------------------------------------------------------------------------------- | ------- |
| `depth` | Number | Configures the transferred depth of any logged objects. | `10` |
| `clearOnStoryChange` | Boolean | Flag whether to clear the action logger when switching away from the current story. | `true` |
| `limit` | Number | Limits the number of items logged in the action logger | `50` |
## Declarative Configuration via Parameters
You can define action handles in a declarative way using parameters. They accepts the same arguments as [`actions`](#multiple-actions)
Keys have `'<eventName> <selector>'` format, e.g. `'click .btn'`. Selector is optional. This can be used with any framework but is especially useful for `@storybook/html`.
```js
import Button from './button';
export default {
title: 'Button',
parameters: {
actions: {
handles: ['mouseover', 'click .btn']
}
};
export const first = () => <Button className="btn">Hello World!</Button>;
```
The basic usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/actions). For legacy usage, see the [advanced README](./ADVANCED.md).

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-actions",
"version": "6.0.0-beta.23",
"version": "6.1.0-alpha.14",
"description": "Action Logger addon for storybook",
"keywords": [
"storybook"
@ -22,18 +22,18 @@
"README.md",
"*.js",
"*.d.ts",
"ts3.5/**/*"
"ts3.4/**/*"
],
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.0.0-beta.23",
"@storybook/api": "6.0.0-beta.23",
"@storybook/client-api": "6.0.0-beta.23",
"@storybook/components": "6.0.0-beta.23",
"@storybook/core-events": "6.0.0-beta.23",
"@storybook/theming": "6.0.0-beta.23",
"@storybook/addons": "6.1.0-alpha.14",
"@storybook/api": "6.1.0-alpha.14",
"@storybook/client-api": "6.1.0-alpha.14",
"@storybook/components": "6.1.0-alpha.14",
"@storybook/core-events": "6.1.0-alpha.14",
"@storybook/theming": "6.1.0-alpha.14",
"core-js": "^3.0.1",
"fast-deep-equal": "^3.1.1",
"global": "^4.3.2",
@ -41,6 +41,7 @@
"polished": "^3.4.4",
"prop-types": "^15.7.2",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-inspector": "^5.0.1",
"regenerator-runtime": "^0.13.3",
"ts-dedent": "^1.1.1",
@ -52,17 +53,14 @@
"@types/uuid": "^7.0.3",
"@types/webpack-env": "^1.15.2"
},
"peerDependencies": {
"react-dom": "*"
},
"publishConfig": {
"access": "public"
},
"gitHead": "4b9d901add9452525135caae98ae5f78dd8da9ff",
"gitHead": "4571582a90b646e361cb37df525f62312486307a",
"typesVersions": {
"<=3.5": {
"<3.8": {
"*": [
"ts3.5/*"
"ts3.4/*"
]
}
}

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import deepEqual from 'fast-deep-equal';
import { API } from '@storybook/api';
import { STORY_RENDERED } from '@storybook/core-events';
import { STORY_CHANGED } from '@storybook/core-events';
import { ActionLogger as ActionLoggerComponent } from '../../components/ActionLogger';
import { EVENT_ID } from '../..';
@ -39,14 +39,14 @@ export default class ActionLogger extends Component<ActionLoggerProps, ActionLog
const { api } = this.props;
api.on(EVENT_ID, this.addAction);
api.on(STORY_RENDERED, this.handleStoryChange);
api.on(STORY_CHANGED, this.handleStoryChange);
}
componentWillUnmount() {
this.mounted = false;
const { api } = this.props;
api.off(STORY_RENDERED, this.handleStoryChange);
api.off(STORY_CHANGED, this.handleStoryChange);
api.off(EVENT_ID, this.addAction);
}

View File

@ -1,22 +1,22 @@
# Storybook Addon Backgrounds
Storybook Background Addon can be used to change background colors inside the preview in [Storybook](https://storybook.js.org).
Storybook Addon Backgrounds can be used to change background colors inside the preview in [Storybook](https://storybook.js.org).
[Framework Support](https://github.com/storybookjs/storybook/blob/master/ADDONS_SUPPORT.md)
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
![React Storybook Screenshot](https://raw.githubusercontent.com/storybookjs/storybook/master/docs/static/img/addon-backgrounds.gif)
## Installation
Backgrounds is part of [essentials](https://storybook.js.org/docs/react/essentials/introduction) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run:
```sh
yarn add @storybook/addon-backgrounds --dev
npm i -D @storybook/addon-backgrounds
```
## Configuration
If it doesn't exist yet, create a file called `main.js` in your storybook config.
Add the following content to it:
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/overview#configure-your-storybook-project):
```js
module.exports = {
@ -26,104 +26,4 @@ module.exports = {
## Usage
Backgrounds requires two parameters:
- `default` - matches the **name** of the value which will be selected by default.
- `values` - an array of elements containing name and value (with a valid css color e.g. HEX, RGBA, etc.)
Write your stories like this:
```jsx
import React from 'react';
/*
* Button.stories.js
* Applies backgrounds to the Stories
*/
export default {
title: 'Button',
parameters: {
backgrounds: {
default: 'twitter',
values: [
{ name: 'twitter', value: '#00aced' },
{ name: 'facebook', value: '#3b5998' },
],
},
},
};
export const defaultView = () => <button>Click me</button>;
```
You can add the backgrounds to all stories by using `parameters` in `.storybook/preview.js`:
```js
export const parameters = {
backgrounds: {
default: 'twitter',
values: [
{ name: 'twitter', value: '#00aced' },
{ name: 'facebook', value: '#3b5998' },
],
},
};
```
If you want to override backgrounds for a single story or group of stories, pass the `backgrounds` parameter:
```jsx
import React from 'react';
export default {
title: 'Button',
};
export const defaultView = () => <button>Click me</button>;
defaultView.parameters = {
backgrounds: {
default: 'red',
values: [{ name: 'red', value: 'rgba(255, 0, 0)' }],
},
};
```
Once you have defined backgrounds for your stories (as can be seen in the examples above), you can set a default background per story by passing the `default` property using a name from the available backgrounds:
```jsx
import React from 'react';
/*
* Button.stories.js
* Applies default background to the Stories
*/
export default {
title: 'Button',
parameters: {
backgrounds: { default: 'twitter' },
},
};
export const twitterColorSelected = () => <button>Click me</button>;
```
If you don't want to use backgrounds for a story, you can set the `backgrounds` parameter to `{ disable: true }` to skip the addon:
```jsx
import React from 'react';
/*
* Button.stories.js
* Disables backgrounds for one Story
*/
export default {
title: 'Button',
};
export const disabledBackgrounds = () => <button>Click me</button>;
disabledBackgrounds.parameters = {
backgrounds: { disable: true },
};
```
The usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/backgrounds).

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-backgrounds",
"version": "6.0.0-beta.23",
"version": "6.1.0-alpha.14",
"description": "A storybook addon to show different backgrounds for your preview",
"keywords": [
"addon",
@ -26,37 +26,35 @@
"README.md",
"*.js",
"*.d.ts",
"ts3.5/**/*"
"ts3.4/**/*"
],
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.0.0-beta.23",
"@storybook/api": "6.0.0-beta.23",
"@storybook/client-logger": "6.0.0-beta.23",
"@storybook/components": "6.0.0-beta.23",
"@storybook/core-events": "6.0.0-beta.23",
"@storybook/theming": "6.0.0-beta.23",
"@storybook/addons": "6.1.0-alpha.14",
"@storybook/api": "6.1.0-alpha.14",
"@storybook/client-logger": "6.1.0-alpha.14",
"@storybook/components": "6.1.0-alpha.14",
"@storybook/core-events": "6.1.0-alpha.14",
"@storybook/theming": "6.1.0-alpha.14",
"core-js": "^3.0.1",
"memoizerific": "^1.11.3",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"regenerator-runtime": "^0.13.3"
},
"devDependencies": {
"@types/webpack-env": "^1.15.2"
},
"peerDependencies": {
"react-dom": "*"
},
"publishConfig": {
"access": "public"
},
"gitHead": "4b9d901add9452525135caae98ae5f78dd8da9ff",
"gitHead": "4571582a90b646e361cb37df525f62312486307a",
"typesVersions": {
"<=3.5": {
"<3.8": {
"*": [
"ts3.5/*"
"ts3.4/*"
]
}
}

View File

@ -0,0 +1 @@
module.exports = require('./dist/preset');

View File

@ -178,7 +178,7 @@ export class BackgroundSelector extends Component<Props> {
<Global
styles={(theme: Theme) => ({
[`#${iframeId}`]: {
backgroundColor:
background:
selectedBackgroundColor === 'transparent'
? theme.background.content
: selectedBackgroundColor,

View File

@ -0,0 +1,8 @@
export const parameters = {
backgrounds: {
values: [
{ name: 'light', value: '#F8F8F8' },
{ name: 'dark', value: '#333333' },
],
},
};

View File

@ -0,0 +1,7 @@
export function config(entry: any[] = []) {
return [...entry, require.resolve('./defaultParameters')];
}
export function managerEntries(entry: any[] = [], options: any) {
return [...entry, require.resolve('../register')];
}

View File

@ -1,411 +1,39 @@
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-hero.gif" width="80%" />
</center>
# Storybook Controls Addon
<h1>Storybook Controls</h1>
[Storybook](https://storybook.js.org) Controls gives you a graphical UI to interact with a component's arguments dynamically, without needing to code. It creates an addon panel next to your component examples ("stories"), so you can edit them live.
Storybook Controls gives you UI to interact with a component's inputs dynamically, without needing to code. It creates an addon panel next to your component examples ("stories"), so you can edit them live.
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
It does not require any modification to your components, and stories for controls are:
- **Convenient.** Auto-generate controls based on [React/Vue/Angular/etc.](#framework-support) components.
- **Portable.** Reuse your interactive stories in documentation, tests, and even in designs.
- **Rich.** Customize the controls and interactive data to suit your exact needs.
Controls are built on top of [Storybook Args](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/formats/component-story-format/index.md#args-story-inputs), which is an open, standards-based format that enable stories to be reused in a variety of contexts.
- **Documentation.** 100% compatible with [Storybook Docs](https://github.com/storybookjs/storybook/tree/next/addons/docs).
- **Testing.** Import stories directly into your [Jest](https://jestjs.io/) tests.
- **Ecosystem.** Reuse stories in design/development tools that support it.
Controls replaces [Storybook Knobs](https://github.com/storybookjs/storybook/tree/master/addons/knobs). It incorporates lessons from years of supporting Knobs on tens of thousands of projects and dozens of different frameworks. We couldn't incrementally fix knobs, so we built a better version.
<h2>Contents</h2>
- [Installation](#installation)
- [Writing stories](#writing-stories)
- [Getting started](#getting-started)
- [Auto-generated args](#auto-generated-args)
- [Custom controls args](#custom-controls-args)
- [Fully custom args](#fully-custom-args)
- [Template stories](#template-stories)
- [Configuration](#configuration)
- [Control annotations](#control-annotations)
- [Parameters](#parameters)
- [Expanded: show property documentation](#expanded-show-property-documentation)
- [Framework support](#framework-support)
- [FAQs](#faqs)
- [How will this replace addon-knobs?](#how-will-this-replace-addon-knobs)
- [How do I migrate from addon-knobs?](#how-do-i-migrate-from-addon-knobs)
![Screenshot](https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-hero.gif)
## Installation
Controls requires [Storybook Docs](https://github.com/storybookjs/storybook/tree/next/addons/docs). If you're not using it already, please install that first.
Next, install the package:
Controls is part of [essentials](https://storybook.js.org/docs/react/essentials/introduction) and so is installed in all new Storybooks by default. If you need to add it to your Storybook, you can run:
```sh
npm install @storybook/addon-controls -D # or yarn
npm i -D @storybook/addon-controls
```
And add it to your `.storybook/main.js` config:
Then, add following content to [`.storybook/main.js`](https://storybook.js.org/docs/react/configure/overview#configure-your-storybook-project):
```js
module.exports = {
addons: [
'@storybook/addon-docs'
'@storybook/addon-controls'
],
addons: ['@storybook/addon-controls'],
};
```
## Writing stories
## Usage
Let's see how to write stories that automatically generate controls based on your component properties.
Controls is built on [Storybook Args](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/formats/component-story-format/index.md#args-story-inputs), which is a small, backwards-compatible change to Storybook's [Component Story Format](https://medium.com/storybookjs/component-story-format-66f4c32366df).
This section is a step-by-step walkthrough for how to upgrade your stories. It takes you from a starting point of the traditional "no args" stories, to auto-generated args, to auto-generated args with custom controls, to fully custom args if you need them.
### Getting started
Let's start with the following component/story combination, which should look familiar if you're coming from an older version of Storybook.
```tsx
import React from 'react';
interface ButtonProps {
/** The main label of the button */
label?: string;
}
export const Button = ({ label = 'FIXME' }: ButtonProps) => <button>{label}</button>;
```
And here's a story that shows that Button component:
```jsx
import React from 'react';
import { Button } from './Button';
export default { title: 'Button', component: Button };
export const Basic = () => <Button label="hello" />;
```
After installing the controls addon, you'll see a new tab that shows the component's props, but it doesn't show controls because the story doesn't use args. That's not very useful, but we'll fix that momentarily.
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-install.png" width="80%" />
</center>
### Auto-generated args
To upgrade your story to an Args story, modify it to accept an args object. **NOTE:** you may need to refresh the browser at this point.
```jsx
export const Basic = (args) => {
console.log({ args });
return <Button label="hello" />;
};
```
Now you'll see auto-generated controls in the `Controls` tab, and you can see the `args` data updating as you edit the values in the UI:
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-args-logging.png" width="80%" />
</center>
Since the args directly matches the `Button`'s props, we can pass it into the args directly:
```jsx
export const Basic = (args) => <Button {...args} />;
```
This generates an interactive UI:
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-args-no-annotation.png" width="80%" />
</center>
Unfortunately this uses the default values specified in the component, and not the label `hello`, which is what we wanted. To address this, we add an `args` annotation to the story, which specifies the initial values:
```jsx
export const Basic = (args) => <Button {...args} />;
Basic.args = { label: 'hello' };
```
Now we're back where we started, but we have a fully interactive story!
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-args-annotated.png" width="80%" />
</center>
And this fully interactive story is also available in the `Docs` tab of Storybook:
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-args-docs.png" width="80%" />
</center>
### Custom controls args
There are cases where you'd like to customize the controls that get auto-generated from your component.
Consider the following modification to the `Button` we introduced above:
```tsx
import React from 'react';
interface ButtonProps {
label?: string;
background?: string;
}
export const Button = ({ background, label = 'FIXME' }: ButtonProps) => (
<button style={{ backgroundColor: background }}>{label}</button>
);
```
And the slightly expanded story:
```jsx
export const Basic = (args) => <Button {...args} />;
Basic.args = { label: 'hello', background: '#ff0' };
```
This generates the following `Controls` UI:
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-args-background-string.png" width="80%" />
</center>
This works as long as you type a valid string into the auto-generated text control, but it's certainly is not the best UI for picking a color.
We can specify which controls get used by declaring a custom `ArgType` for the `background` property. `ArgTypes` encode basic metadata for args, such as `name`, `description`, `defaultValue` for an arg. These get automatically filled in by `Storybook Docs`.
`ArgTypes` can also contain arbitrary annotations which can be overridden by the user. Since `background` is a property of the component, let's put that annotation on the default export.
```jsx
import { Button } from './Button';
export default {
title: 'Button',
component: Button,
argTypes: {
background: { control: { type: 'color' } },
},
};
export const Basic = (args) => <Button {...args} />;
Basic.args = { label: 'hello', background: '#ff0' };
```
This generates the following UI, which is what we wanted in the first place:
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-args-background-color.png" width="80%" />
</center>
### Fully custom args
Up until now, we've only been using auto-generated controls based on the component we're writing stories for. What happens when we want a control for something that's not part of the story?
Consider the following story for our `Button` from above:
```jsx
import range from 'lodash/range';
// export default etc.
export const Reflow = ({ count, label, ...args }) => (
<>
{range(count).map((i) => (
<Button label={`${label} ${i}`} {...args} />
))}
</>
);
Reflow.args = { count: 3, label: 'reflow' };
```
This generates the following UI:
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-args-reflow.png" width="80%" />
</center>
Storybook has inferred the control to be a numeric input based on the initial value of the `count` arg. As we did above, we can also specify a custom control [as we did above](#custom-controls). Only this time since it's story specific we can do it at the story level:
```jsx
// export const Reflow = ... (as above)
// Reflow.args = ...
Reflow.argTypes = {
count: { control: { type: 'range', min: 0, max: 20 } },
};
```
This generates the following UI with a custom range slider:
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-args-reflow-slider.png" width="80%" />
</center>
**Note:** If you add an `ArgType` that is not part of the component, Storybook will *only* use your argTypes definitions.
If you want to merge new controls with the existing component properties, you must enable this parameter:
```jsx
docs: { forceExtractedArgTypes: true },
```
#### Angular
To achieve this within an angular-cli build.
```jsx
export const Reflow = ({ count, label, ...args }) => ({
props: {
label: label,
count: [...Array(count).keys()]
},
template: `<Button *ngFor="let i of count">{{label}} {{i}}</Button>`
}
);
Reflow.args = { count: 3, label: 'reflow' };
```
### Template stories
Suppose you've created the `Basic` story from above, but now we want to create a second story with a different state, such as how the button renders with the label is really long.
The simplest thing would be to create a second story:
```jsx
export const VeryLongLabel = (args) => <Button {...args} />;
VeryLongLabel.args = { label: 'this is a very long string', background: '#ff0' };
```
This works, but it repeats code. What we want is to reuse the `Basic` story, but with a different initial state. In Storybook we do this idiomatically for Args stories:
```jsx
export const VeryLongLabel = Basic.bind();
VeryLongLabel.args = { label: 'this is a very long string', background: '#ff0' };
```
We can even reuse initial args from other stories:
```jsx
export const VeryLongLabel = Basic.bind();
VeryLongLabel.args = { ...Basic.args, label: 'this is a very long string' };
```
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-args-template.png" width="80%" />
</center>
## Configuration
The controls addon can be configured in two ways:
- Individual controls can be configured via [control annotations](#control-annotations),
- The addon's appearance can be configured via [parameters](#parameters).
### Control annotations
As shown above in the [custom control args](#custom-controls-args) and [fully custom args](#fully-custom-args) sections, you can configure controls via a "control" annotation in the `argTypes` field of either a component or story.
Here is the full list of available controls:
| data type | control type | description | options |
| ----------- | ------------ | -------------------------------------------------------------- | -------------- |
| **array** | array | serialize array into a comma-separated string inside a textbox | separator |
| **boolean** | boolean | checkbox input | - |
| **number** | number | a numberic text box input | min, max, step |
| | range | a range slider input | min, max, step |
| **object** | object | json editor text input | - |
| **enum** | radio | radio buttons input | options |
| | inline-radio | inline radio buttons input | options |
| | check | multi-select checkbox input | options |
| | inline-check | multi-select inline checkbox input | options |
| | select | select dropdown input | options |
| | multi-select | multi-select dropdown input | options |
| **string** | text | simple text input | - |
| | color | color picker input that assumes strings are color values | - |
| | date | date picker input | - |
Example customizing a control for an `enum` data type (defaults to `select` control type):
```js
export default {
title: 'Widget',
component: Widget,
argTypes: {
loadingState: {
type: 'inline-radio',
options: ['loading', 'error', 'ready'],
},
},
};
```
Example customizing a `number` data type (defaults to `number` control type):
```js
export default {
title: 'Gizmo',
component: Gizmo,
argTypes: {
width: { type: 'range', min: 400, max: 1200, step: 50 };
},
};
```
### Parameters
Controls supports the following configuration parameters, either [globally or on a per-story basis](https://storybook.js.org/docs/basics/writing-stories/#parameters):
- [Expanded: show property documentation](#expanded-show-property-documentation)
- [Hide NoControls warning](#hide-nocontrols-warning)
#### Expanded: show property documentation
Since Controls is built on the same engine as Storybook Docs, it can also show property documentation alongside your controls using the `expanded` parameter (defaults to `false`).
To enable expanded mode globally, add the following to `.storybook/preview.js`:
```jsx
export const parameters = {
controls: { expanded: true },
};
```
And here's what the resulting UI looks like:
<center>
<img src="https://raw.githubusercontent.com/storybookjs/storybook/next/addons/controls/docs/media/addon-controls-expanded.png" width="80%" />
</center>
#### Hide NoControls warning
If you don't plan to handle the control args inside your Story, you can remove the warning with:
```jsx
Basic.parameters = {
controls: { hideNoControlsWarning: true },
};
```
## Framework support
| | Manual | Auto-generated |
| -------------- | :----: | :------------: |
| React | + | + |
| Vue | + | + |
| Angular | + | + |
| Ember | + | # |
| Web components | + | + |
| HTML | + | |
| Svelte | + | |
| Preact | + | |
| Riot | + | |
| Mithril | + | |
| Marko | + | |
**Note:** `#` = WIP support
The usage is documented in the [documentation](https://storybook.js.org/docs/react/essentials/controls).
## FAQs
- [How will this replace addon-knobs?](#how-will-this-replace-addon-knobs)
- [How do I migrate from addon-knobs?](#how-do-i-migrate-from-addon-knobs)
- [My controls aren't being auto-generated. What should I do?](#my-controls-arent-being-auto-generated-what-should-i-do)
- [How can I disable controls for certain fields on a particular story?](#how-can-i-disable-controls-for-certain-fields-on-a-particular-story)
- [How do controls work with MDX?](#how-do-controls-work-with-mdx)
### How will this replace addon-knobs?
Addon-knobs is one of Storybook's most popular addons with over 1M weekly downloads, so we know lots of users will be affected by this change. Knobs is also a mature addon, with various options that are not available in addon-controls.
@ -461,12 +89,106 @@ export const Reflow = () => {
};
```
And again, as above, this can be rewritten using [fully custom args](#fully-custom-args):
And again, as above, this can be rewritten using [fully custom args](https://storybook.js.org/docs/react/essentials/controls#fully-custom-args):
```jsx
export const Reflow = ({ count, label, ...args }) => (
<>{range(count).map((i) => <Button label={`${label} ${i}` {...args}} />)}</>
);
Reflow.args = { count: 3, label: 'reflow' };
Reflow.argTypes = { count: { control: { type: 'range', min: 0, max: 20 } } };
Reflow.argTypes = {
count: { control: { type: 'range', min: 0, max: 20 } }
};
```
### My controls aren't being auto-generated. What should I do?
There are a few known cases where controls can't be auto-generated:
- You're using a framework for which automatic generation [isn't supported](https://storybook.js.org/docs/react/api/frameworks-feature-support)
- You're trying to generate controls for a component defined in an external library
With a little manual work you can still use controls in such cases. Consider the following example:
```js
import { Button } from 'some-external-library';
export default {
title: 'Button',
argTypes: {
label: { control: 'text' },
borderWidth: { control: { type: 'number', min: 0, max: 10 }},
},
};
export const Basic = (args) => <Button {...args} />;
Basic.args = {
label: 'hello',
borderWidth: 1,
};
```
The `argTypes` annotation (which can also be applied to individual stories if needed), gives Storybook the hints it needs to generate controls in these unsupported cases. See [control annotations](https://storybook.js.org/docs/react/essentials/controls#annotation) for a full list of control types.
It's also possible that your Storybook is misconfigured. If you think this might be the case, please search through Storybook's [Github issues](https://github.com/storybookjs/storybook/issues), and file a new issue if you don't find one that matches your use case.
### How can I disable controls for certain fields on a particular story?
The `argTypes` annotation can be used to hide controls for a particular row, or even hide rows.
Suppose you have a `Button` component with `borderWidth` and `label` properties (auto-generated or otherwise) and you want to hide the `borderWidth` row completely and disable controls for the `label` row on a specific story. Here's how you'd do that:
```js
import { Button } from 'button';
export default {
title: 'Button',
component: Button,
};
export const CustomControls = (args) => <Button {...args} />;
CustomControls.argTypes = {
borderWidth: { table: { disable: true } },
label: { control: { disable: true } },
};
```
Like [story parameters](https://storybook.js.org/docs/react/writing-stories/parameters), `args` and `argTypes` annotations are hierarchically merged, so story-level annotations overwrite component-level annotations.
### How do controls work with MDX?
MDX compiles to component story format (CSF) under the hood, so there's a direct mapping for every example above using the `args` and `argTypes` props.
Consider this example in CSF:
```js
import { Button } from './Button';
export default {
title: 'Button',
component: Button,
argTypes: {
background: { control: 'color' },
},
};
const Template = (args) => <Button {...args} />;
export const Basic = Template.bind({});
Basic.args = { label: 'hello', background: '#ff0' };
```
Here's the MDX equivalent:
```jsx
import { Meta, Story } from '@storybook/addon-docs/blocks';
import { Button } from './Button';
<Meta title="Button" component={Button} argTypes={{ background: { control: 'color' } }} />
export const Template = (args) => <Button {...args} />
<Story name="Basic" args={{ label: 'hello', background: '#ff0' }}>
{Template.bind({})}
</Story>
```
For more info, see a full [Controls example in MDX for Vue](https://raw.githubusercontent.com/storybookjs/storybook/next/examples/vue-kitchen-sink/src/stories/addon-controls.stories.mdx).

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-controls",
"version": "6.0.0-beta.23",
"version": "6.1.0-alpha.14",
"description": "Controls for component properties",
"keywords": [
"addon",
@ -30,18 +30,19 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.0.0-beta.23",
"@storybook/api": "6.0.0-beta.23",
"@storybook/client-api": "6.0.0-beta.23",
"@storybook/components": "6.0.0-beta.23",
"@storybook/theming": "6.0.0-beta.23",
"core-js": "^3.0.1"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
"@storybook/addons": "6.1.0-alpha.14",
"@storybook/api": "6.1.0-alpha.14",
"@storybook/client-api": "6.1.0-alpha.14",
"@storybook/components": "6.1.0-alpha.14",
"@storybook/node-logger": "6.1.0-alpha.14",
"@storybook/theming": "6.1.0-alpha.14",
"core-js": "^3.0.1",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"ts-dedent": "^1.1.1"
},
"publishConfig": {
"access": "public"
}
},
"gitHead": "4571582a90b646e361cb37df525f62312486307a"
}

View File

@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { styled } from '@storybook/theming';
import { ArgsTable, Link } from '@storybook/components';
import { ArgsTable, NoControlsWarning } from '@storybook/components';
import { useArgs, useArgTypes, useParameter } from '@storybook/api';
import { PARAM_KEY } from '../constants';
interface ControlsParameters {
@ -9,36 +9,28 @@ interface ControlsParameters {
hideNoControlsWarning?: boolean;
}
const NoControlsWrapper = styled.div(({ theme }) => ({
background: theme.background.warning,
padding: 20,
}));
const NoControlsWarning = () => (
<NoControlsWrapper>
This story is not configured to handle controls.&nbsp;
<Link
href="https://github.com/storybookjs/storybook/blob/next/addons/controls/README.md#writing-stories"
target="_blank"
cancel={false}
>
Learn how to add controls »
</Link>
</NoControlsWrapper>
);
export const ControlsPanel: FC = () => {
const [args, updateArgs] = useArgs();
const [args, updateArgs, resetArgs] = useArgs();
const rows = useArgTypes();
const isArgsStory = useParameter<boolean>('__isArgsStory', false);
const { expanded, hideNoControlsWarning = false } = useParameter<ControlsParameters>(
PARAM_KEY,
{}
);
const hasControls = Object.values(rows).filter((argType) => argType?.control?.type).length > 0;
const hasControls = Object.values(rows).filter((argType) => !!argType?.control).length > 0;
return (
<>
{hasControls || hideNoControlsWarning ? null : <NoControlsWarning />}
<ArgsTable {...{ compact: !expanded && hasControls, rows, args, updateArgs }} />
{(hasControls && isArgsStory) || hideNoControlsWarning ? null : <NoControlsWarning />}
<ArgsTable
{...{
compact: !expanded && hasControls,
rows,
args,
updateArgs,
resetArgs,
inAddonPanel: true,
}}
/>
</>
);
};

View File

@ -0,0 +1,23 @@
import React from 'react';
import { styled } from '@storybook/theming';
import { Link } from '@storybook/components';
const NoControlsWrapper = styled.div(({ theme }) => ({
background: theme.background.warning,
padding: '10px 15px',
lineHeight: '20px',
boxShadow: `${theme.appBorderColor} 0 -1px 0 0 inset`,
}));
export const NoControlsWarning = () => (
<NoControlsWrapper>
This story is not configured to handle controls.&nbsp;
<Link
href="https://github.com/storybookjs/storybook/blob/next/addons/controls/README.md#writing-stories"
target="_blank"
cancel={false}
>
Learn how to add controls »
</Link>
</NoControlsWrapper>
);

View File

@ -0,0 +1,26 @@
import { verifyDocsBeforeControls } from './ensureDocsBeforeControls';
describe.each([
[[]],
[['@storybook/addon-controls']],
[['@storybook/addon-docs']],
[['@storybook/addon-controls', '@storybook/addon-docs']],
[['@storybook/addon-essentials', '@storybook/addon-docs']],
[['@storybook/addon-controls', '@storybook/addon-essentials']],
[['@storybook/addon-essentials', '@storybook/addon-controls', '@storybook/addon-docs']],
])('verifyDocsBeforeControls', (input) => {
it(`invalid ${input}`, () => {
expect(verifyDocsBeforeControls(input)).toBeFalsy();
});
});
describe.each([
[['@storybook/addon-docs', '@storybook/addon-controls']],
[[{ name: '@storybook/addon-docs' }, '@storybook/addon-controls']],
[['@storybook/addon-essentials', '@storybook/addon-controls']],
[['@storybook/addon-essentials']],
])('verifyDocsBeforeControls', (input) => {
it(`valid ${input}`, () => {
expect(verifyDocsBeforeControls(input)).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import path from 'path';
import { logger } from '@storybook/node-logger';
import dedent from 'ts-dedent';
type OptionsEntry = { name: string };
type Entry = string | OptionsEntry;
const findIndex = (addon: string, addons: Entry[]) =>
addons.findIndex((entry) => {
const name = (entry as OptionsEntry).name || (entry as string);
return name && name.startsWith(addon);
});
const indexOfAddonOrEssentials = (addon: string, addons: Entry[]) => {
const index = findIndex(addon, addons);
return index >= 0 ? index : findIndex('@storybook/addon-essentials', addons);
};
export const verifyDocsBeforeControls = (addons: Entry[]) => {
const docsIndex = indexOfAddonOrEssentials('@storybook/addon-docs', addons);
const controlsIndex = indexOfAddonOrEssentials('@storybook/addon-controls', addons);
return controlsIndex >= 0 && docsIndex >= 0 && docsIndex <= controlsIndex;
};
export const ensureDocsBeforeControls = (configDir: string) => {
const mainFile = path.isAbsolute(configDir)
? path.join(configDir, 'main')
: path.join(process.cwd(), configDir, 'main');
try {
// eslint-disable-next-line global-require,import/no-dynamic-require
const main = require(mainFile);
if (!main?.addons) {
logger.warn(`Unable to find main.js addons: ${mainFile}`);
return;
}
if (!verifyDocsBeforeControls(main.addons)) {
logger.warn(dedent`
Expected '@storybook/addon-docs' (or essentials) to be listed before '@storybook/addon-controls'. Check your main.js?
https://github.com/storybookjs/storybook/issues/11442
`);
}
} catch (err) {
logger.warn(`Unable to find main.js: ${mainFile}`);
}
};

View File

@ -1,3 +1,6 @@
export function managerEntries(entry: any[] = []) {
import { ensureDocsBeforeControls } from './ensureDocsBeforeControls';
export function managerEntries(entry: any[] = [], options: any) {
ensureDocsBeforeControls(options.configDir);
return [...entry, require.resolve('../register')];
}

View File

@ -2,12 +2,7 @@ import { useArgTypes } from '@storybook/api';
export function getTitle(): string {
const rows = useArgTypes();
const controlsCount = Object.values(rows).filter((argType) => argType?.control?.type).length;
if (controlsCount === 0) {
return 'Controls';
}
return `Controls (${controlsCount})`;
const controlsCount = Object.values(rows).filter((argType) => argType?.control).length;
const suffix = controlsCount === 0 ? '' : ` (${controlsCount})`;
return `Controls${suffix}`;
}

View File

@ -2,7 +2,7 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env", "jest"]
"types": ["webpack-env", "jest", "node"]
},
"include": ["src/**/*"],
"exclude": ["src/**.test.ts"]

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-cssresources",
"version": "6.0.0-beta.23",
"version": "6.1.0-alpha.14",
"description": "A storybook addon to switch between css resources at runtime for your story",
"keywords": [
"addon",
@ -26,40 +26,36 @@
"README.md",
"*.js",
"*.d.ts",
"ts3.5/**/*"
"ts3.4/**/*"
],
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.0.0-beta.23",
"@storybook/api": "6.0.0-beta.23",
"@storybook/components": "6.0.0-beta.23",
"@storybook/core-events": "6.0.0-beta.23",
"@storybook/theming": "6.0.0-beta.23",
"@storybook/addons": "6.1.0-alpha.14",
"@storybook/api": "6.1.0-alpha.14",
"@storybook/components": "6.1.0-alpha.14",
"@storybook/core-events": "6.1.0-alpha.14",
"@storybook/theming": "6.1.0-alpha.14",
"core-js": "^3.0.1",
"global": "^4.3.2",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"regenerator-runtime": "^0.13.3"
},
"devDependencies": {
"@types/webpack-env": "^1.15.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"react-dom": "^16.12.0"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
"enzyme-adapter-react-16": "^1.15.2"
},
"publishConfig": {
"access": "public"
},
"gitHead": "4b9d901add9452525135caae98ae5f78dd8da9ff",
"gitHead": "4571582a90b646e361cb37df525f62312486307a",
"typesVersions": {
"<=3.5": {
"<3.8": {
"*": [
"ts3.5/*"
"ts3.4/*"
]
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-design-assets",
"version": "6.0.0-beta.23",
"version": "6.1.0-alpha.14",
"description": "Design asset preview for storybook",
"keywords": [
"addon",
@ -28,36 +28,34 @@
"README.md",
"*.js",
"*.d.ts",
"ts3.5/**/*"
"ts3.4/**/*"
],
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.0.0-beta.23",
"@storybook/api": "6.0.0-beta.23",
"@storybook/client-logger": "6.0.0-beta.23",
"@storybook/components": "6.0.0-beta.23",
"@storybook/core-events": "6.0.0-beta.23",
"@storybook/theming": "6.0.0-beta.23",
"@storybook/addons": "6.1.0-alpha.14",
"@storybook/api": "6.1.0-alpha.14",
"@storybook/client-logger": "6.1.0-alpha.14",
"@storybook/components": "6.1.0-alpha.14",
"@storybook/core-events": "6.1.0-alpha.14",
"@storybook/theming": "6.1.0-alpha.14",
"core-js": "^3.0.1",
"global": "^4.3.2",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"regenerator-runtime": "^0.13.3",
"ts-dedent": "^1.1.1",
"use-image": "^1.0.3"
},
"peerDependencies": {
"react-dom": "*"
},
"publishConfig": {
"access": "public"
},
"gitHead": "4b9d901add9452525135caae98ae5f78dd8da9ff",
"gitHead": "4571582a90b646e361cb37df525f62312486307a",
"typesVersions": {
"<=3.5": {
"<3.8": {
"*": [
"ts3.5/*"
"ts3.4/*"
]
}
}

View File

@ -46,7 +46,7 @@ For more information on how it works, see the [`DocsPage` reference](./docs/docs
Here's an example file:
```md
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { Meta, Story, Canvas } from '@storybook/addon-docs/blocks';
import { Checkbox } from './Checkbox';
<Meta title="MDX/Checkbox" component={Checkbox} />
@ -56,7 +56,7 @@ import { Checkbox } from './Checkbox';
With `MDX` we can define a story for `Checkbox` right in the middle of our
markdown documentation.
<Preview>
<Canvas>
<Story name="all checkboxes">
<form>
<Checkbox id="Unchecked" label="Unchecked" />
@ -64,7 +64,7 @@ markdown documentation.
<Checkbox appearance="secondary" id="second" label="Secondary" checked />
</form>
</Story>
</Preview>
</Canvas>
```
And here's how that's rendered in Storybook:
@ -89,7 +89,7 @@ Storybook Docs supports all view layers that Storybook supports except for React
| Props table | + | + | + | + | + | | | | | | |
| Props controls | + | + | | | | | | | | | |
| Description | + | + | + | + | + | | | | | | |
| Inline stories | + | + | | | + | | | | | | |
| Inline stories | + | + | | | + | + | | | | | |
**Note:** `#` = WIP support
@ -226,7 +226,7 @@ addParameters({
## TypeScript configuration
As of SB6 [TypeScript is zero-config](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/configurations/typescript-config/index.md) and should work with SB Docs out of the box. For advanced configuration options, refer to the [Props documentation](./docs/props-tables.md).
As of SB6 [TypeScript is zero-config](https://storybook.js.org/docs/react/configure/typescript) and should work with SB Docs out of the box. For advanced configuration options, refer to the [Props documentation](./docs/props-tables.md).
## More resources

View File

@ -113,7 +113,7 @@ module.exports = {
Finally, you can create MDX files like this:
```md
import { Meta, Story, Props } from '@storybook/addon-docs/blocks';
import { Meta, Story, ArgsTable } from '@storybook/addon-docs/blocks';
import { AppComponent } from './app.component';
<Meta title='App Component' component={AppComponent} />
@ -127,9 +127,9 @@ Some **markdown** description, or whatever you want.
props: {},
}}</Story>
## Props
## ArgsTable
<Props of={AppComponent} />
<ArgsTable of={AppComponent} />
```
Yes, it's redundant to declare `component` twice. [Coming soon](https://github.com/storybookjs/storybook/issues/8673).
@ -139,7 +139,7 @@ Also, to use the `Props` doc block, you need to set up Compodoc, [as described a
When you are using `template`, `moduleMetadata` and/or `addDecorators` with `storiesOf` then you can easily translate your story to MDX, too:
```md
import { Meta, Story, Props } from '@storybook/addon-docs/blocks';
import { Meta, Story, ArgsTable } from '@storybook/addon-docs/blocks';
import { CheckboxComponent, RadioButtonComponent } from './my-components';
import { moduleMetadata } from '@storybook/angular';

View File

@ -1,21 +1,2 @@
export { ColorPalette, ColorItem, IconGallery, IconItem, Typeset } from '@storybook/components';
export * from './dist/blocks/Anchor';
export * from './dist/blocks/Description';
export * from './dist/blocks/DocsContext';
export * from './dist/blocks/DocsPage';
export * from './dist/blocks/DocsContainer';
export * from './dist/blocks/DocsStory';
export * from './dist/blocks/Heading';
export * from './dist/blocks/Meta';
export * from './dist/blocks/Preview';
export * from './dist/blocks/Primary';
export * from './dist/blocks/Props';
export * from './dist/blocks/Source';
export * from './dist/blocks/Stories';
export * from './dist/blocks/Story';
export * from './dist/blocks/Subheading';
export * from './dist/blocks/Subtitle';
export * from './dist/blocks/Title';
export * from './dist/blocks/Wrapper';
export * from './dist/blocks/types';
export * from './dist/blocks/mdx';
export * from './dist/blocks/index.d';

View File

@ -51,7 +51,7 @@ module.exports = {
Finally, you can create MDX files like this:
```md
import { Meta, Story, Props } from '@storybook/addon-docs/blocks';
import { Meta, Story, ArgsTable } from '@storybook/addon-docs/blocks';
<Meta title='App Component' />
@ -85,7 +85,7 @@ basic.parameters = {
}
```
And for `MDX` you can modify it as an attribute on the `Story` element:
And for `MDX` you can modify it, especially if you work with some components using fixed or sticky positions, as an attribute on the `Story` element:
```md
<Story name='basic' height='400px'>{...}</Story>

View File

@ -17,7 +17,7 @@ When you install [Storybook Docs](../README.md), `DocsPage` is the zero-config d
## Motivation
`DocsPage` is the successor to [`addon-info`](https://github.com/storybookjs/storybook/tree/next/addons/info), which was one of the most popular Storybook addons despite many limitations.
`DocsPage` is the successor to [`addon-info`](https://github.com/storybookjs/deprecated-addons/tree/master/addons/info), which was one of the most popular Storybook addons despite many limitations.
Like `addon-info`, `DocsPage` provides sensible defaults, meaning it adds documentation to your existing Storybook without requiring any additional work on your part.
@ -34,7 +34,7 @@ However, `DocsPage` brings the following improvements:
Storybook uses `component` to extract the component's description and props, and will rely on it further in future releases. We encourage you to add it to existing stories and use it in all new stories.
Here's how to set the component in [Component Story Format (CSF)](https://storybook.js.org/docs/formats/component-story-format/):
Here's how to set the component in [Component Story Format (CSF)](https://storybook.js.org/docs/react/api/csf):
```js
import { Badge } from './Badge';
@ -126,7 +126,7 @@ import {
Subtitle,
Description,
Primary,
Props,
ArgsTable,
Stories,
} from '@storybook/addon-docs/blocks';
import { DocgenButton } from '../../components/DocgenButton';
@ -142,7 +142,7 @@ export default {
<Subtitle />
<Description />
<Primary />
<Props />
<ArgsTable />
<Stories />
</>
),
@ -173,9 +173,9 @@ import { addParameters } from '@storybook/vue';
addParameters({
docs: {
prepareForInline: (storyFn) => {
prepareForInline: (storyFn, { args }) => {
const Story = toReact(storyFn());
return <Story />;
return <Story {...args} />;
},
},
});

View File

@ -20,7 +20,7 @@
Let's get started with an example that combines markdown with a single story:
```md
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { Meta, Story, Canvas } from '@storybook/addon-docs/blocks';
import { Checkbox } from './Checkbox';
<Meta title="MDX/Checkbox" component={Checkbox} />
@ -30,7 +30,7 @@ import { Checkbox } from './Checkbox';
With `MDX` we can define a story for `Checkbox` right in the middle of our
markdown documentation.
<Preview>
<Canvas>
<Story name="all checkboxes">
<form>
<Checkbox id="Unchecked" label="Unchecked" />
@ -38,7 +38,7 @@ markdown documentation.
<Checkbox appearance="secondary" id="second" label="Secondary" checked />
</form>
</Story>
</Preview>
</Canvas>
```
And here's how that's rendered in Storybook:
@ -79,7 +79,7 @@ There's a one-to-one mapping from the code in `MDX` to `CSF`, which in turn dire
Now let's look at a more realistic example to see a few more things we can do:
```md
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { Meta, Story, Canvas } from '@storybook/addon-docs/blocks';
import { Badge } from './Badge';
import { Icon } from './Icon';
@ -94,19 +94,19 @@ Let's define a story for our `Badge` component:
<Badge status="positive">Positive</Badge>
</Story>
We can drop it in a `Preview` to get a code snippet:
We can drop it in a `Canvas` to get a code snippet:
<Preview>
<Canvas>
<Story name="negative">
<Badge status="negative">Negative</Badge>
</Story>
</Preview>
</Canvas>
We can even preview multiple stories in a block. This
gets rendered as a group, but defines individual stories
with unique URLs and isolated snapshot tests.
<Preview>
<Canvas>
<Story name="warning">
<Badge status="warning">Warning</Badge>
</Story>
@ -122,7 +122,7 @@ with unique URLs and isolated snapshot tests.
with icon
</Badge>
</Story>
</Preview>
</Canvas>
```
And here's how that gets rendered in Storybook:
@ -149,7 +149,7 @@ You can also use the rest of the MDX features in conjunction with embedding. Tha
## Decorators and parameters
To add [decorators](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/basics/writing-stories/index.md#decorators) and [parameters](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/basics/writing-stories/index.md#parameters) in MDX:
To add [decorators](https://storybook.js.org/docs/react/writing-stories/decorators) and [parameters](https://storybook.js.org/docs/react/writing-stories/parameters) in MDX:
```md
<Meta

View File

@ -3,7 +3,7 @@
Storybook Docs [provides basic support for all non-RN Storybook view layers](../README.md#framework-support) out of the box. However, some frameworks have been docs-optimized, adding features like automatic props table generation and inline story rendering. This document is a dev guide for how to optimize a new framework in docs.
- [Framework-specific configuration](#framework-specific-configuration)
- [Props tables](#props-tables)
- [Arg tables](#arg-tables)
- [Component descriptions](#component-descriptions)
- [Inline story rendering](#inline-story-rendering)
- [More resources](#more-resources)
@ -16,7 +16,7 @@ Addon-docs handles this kind of customization by file naming convention. Its [co
For example, consider Storybook Docs for Vue, which needs `vue-docgen-loader` in its webpack config, and also has custom extraction functions for [props tables](#props-tables) and [component descriptions](#component-descriptions).
For webpack configuration, Docs for Vue defines [preset.ts](https://github.com/storybookjs/storybook/blob/next/addons/docs/src/frameworks/vue/preset.ts), which follows the [preset](https://storybook.js.org/docs/presets/introduction) file structure:
For webpack configuration, Docs for Vue defines [preset.ts](https://github.com/storybookjs/storybook/blob/next/addons/docs/src/frameworks/vue/preset.ts), which follows the [preset](https://storybook.js.org/docs/vue/api/presets) file structure:
```
export function webpack(webpackConfig: any = {}, options: any = {}) {
@ -33,48 +33,53 @@ This appends `vue-docgen-loader` to the existing configuration, which at this po
For props tables and descriptions, both of which are described in more detail below, it defines a file [config.tsx](https://github.com/storybookjs/storybook/blob/next/addons/docs/src/frameworks/vue/config.tsx).
## Props tables
## Arg tables
Props tables are enabled by the framework-specific `docs.extractProps` parameter, which extracts a component's props into a common data structure.
Each framework can auto-generate ArgTables by exporting one or more `ArgType` enhancers, which extracts a component's properties into a common data structure.
Here's how it's done in Vue's framework-specific `preview.js`:
```js
import { extractProps } from './extractProps';
import { enhanceArgTypes } from './enhanceArgTypes';
addParameters({
docs: {
// `container`, `page`, etc. here
extractProps,
},
});
export const argTypesEnhancers = [enhanceArgTypes];
```
The `extractProps`function receives a component as an argument, and returns an object of type [`PropsTableProps`](https://github.com/storybookjs/storybook/blob/next/lib/components/src/blocks/PropsTable/PropsTable.tsx#L147), which can either be an array of `PropDef` rows (React), or a mapping of section name to an array of `PropDef` rows (e.g. `Props`/`Events`/`Slots` in Vue).
The `enhanceArgTypes`function takes a `StoryContext` (including the story id, parameters, args, argTypes, etc.), and returns an updated [`ArgTypes` object](https://github.com/storybookjs/storybook/blob/master/lib/addons/src/types.ts#L38-L47):
```ts
export interface PropDef {
name: string;
type: any;
required: boolean;
export interface ArgType {
name?: string;
description?: string;
defaultValue?: any;
[key: string]: any;
}
export interface ArgTypes {
[key: string]: ArgType;
}
```
So far, in React and Vue, the implementation of this extraction is as follows:
For more information on what this generation looks like, see the [controls generation docs](https://github.com/storybookjs/storybook/blob/next/addons/controls/README.md#my-controls-arent-being-auto-generated-what-should-i-do).
For React and Vue, the extraction works as follows:
- A webpack loader is added to the user's config via the preset
- The loader annotates the component with a field, `__docgenInfo`, which contains some metadata
- The view-layer specific `extractProps` function translates that metadata into `PropsTableProps`
- The view-layer specific `enhanceArgTypes` function translates that metadata into `ArgTypes`
However, for your framework you may want to implement this in some other way. There is also an effort to load data from static JSON files for performance [#7942](https://github.com/storybookjs/storybook/issues/7942).
For Angular, Web components, and Ember, the extraction works as follows:
- Read JSON file in the user's `.storyboook/preview.json` and story it into a global variable
- The view-layer specific `enhanceArgTypes` function translates that metadata into `ArgTypes`
However, for your framework you may want to implement this in some other way.
## Component descriptions
Component descriptions are enabled by the `docs.extractComponentDescription` parameter, which extract's a component description (usually from source code comments) into a markdown string.
It follows the pattern of [Props tables](#props-tables) above, only it's even simpler because the function output is simply a string (or null if there no description).
It follows the pattern of [Arg tables](#arg-tables) above, only it's even simpler because the function output is simply a string (or null if there no description).
## Inline story rendering
@ -88,15 +93,15 @@ import toReact from '@egoist/vue-to-react';
addParameters({
docs: {
// `container`, `page`, etc. here
prepareForInline: (storyFn) => {
prepareForInline: (storyFn, { args }) => {
const Story = toReact(storyFn());
return <Story />;
return <Story {...args} />;
},
},
});
```
The input is the story function, and the output is a React element, because we render docs pages in react. In the case of Vue, all of the work is done by the `@egoist/vue-to-react` library. If there's no analogous library for your framework, you may need to figure it out yourself!
The input is the story function and the story context (id, parameters, args, etc.), and the output is a React element, because we render docs pages in react. In the case of Vue, all of the work is done by the `@egoist/vue-to-react` library. If there's no analogous library for your framework, you may need to figure it out yourself!
## More resources

View File

@ -38,21 +38,21 @@ export default {
### MDX
To use the props table in [MDX](./mdx.md), use the `Props` block:
To use the props table in [MDX](./mdx.md), use the `ArgsTable` block:
```js
// MyComponent.stories.mdx
import { Props } from '@storybook/addon-docs/blocks';
import { ArgsTable } from '@storybook/addon-docs/blocks';
import { MyComponent } from './MyComponent';
# My Component!
<Props of={MyComponent} />
<ArgsTable of={MyComponent} />
```
## Controls
Starting in SB 6.0, the `Props` block has built-in `Controls` (formerly known as "knobs") for editing stories dynamically.
Starting in SB 6.0, the `ArgsTable` block has built-in `Controls` (formerly known as "knobs") for editing stories dynamically.
<center>
<img src="./media/args-controls.gif" width="80%" />
@ -60,7 +60,7 @@ Starting in SB 6.0, the `Props` block has built-in `Controls` (formerly known as
<br/>
These controls are implemented appear automatically in the props table when your story accepts [Storybook Args](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/formats/component-story-format/index.md#args-story-inputs) as its input. This is done slightly differently depending on whether you're using `DocsPage` or `MDX`.
These controls are implemented appear automatically in the props table when your story accepts [Storybook Args](https://storybook.js.org/docs/react/api/csf#args-story-inputs) as its input. This is done slightly differently depending on whether you're using `DocsPage` or `MDX`.
**DocsPage.** In [DocsPage](./docspage.md), simply write your story to consume args and the auto-generated props table will display controls in the right-most column:
@ -73,14 +73,14 @@ export default {
export const WithControls = (args) => <MyComponent {...args} />;
```
**MDX.** In [MDX](./mdx.md), the `Props` controls are more configurable than in DocsPage. In order to show controls, `Props` must be a function of a story, not a component:
**MDX.** In [MDX](./mdx.md), the `ArgsTable` controls are more configurable than in DocsPage. In order to show controls, `ArgsTable` must be a function of a story, not a component:
```js
<Story name="WithControls">
{args => <MyComponent {...args} />}
</Story>
<Props story="Controls" />
<ArgsTable story="Controls" />
```
For a very detailed walkthrough of how to write stories that use controls, see the [addon-controls README](https://github.com/storybookjs/storybook/blob/next/addons/controls/README.md#writing-stories).
@ -91,13 +91,13 @@ Props tables are automatically inferred from your components and stories, but so
Props tables are rendered from an internal data structure called `ArgTypes`. When you declare a story's `component` metadata, Docs automatically extracts `ArgTypes` based on the component's properties.
You can can customize what's shown in the props table by [customizing the `ArgTypes` data](#customizing-argtypes). This is currently available for `DocsPage` and `<Props story="xxx">` construct, but not for the `<Props of={component} />` construct,
You can can customize what's shown in the props table by [customizing the `ArgTypes` data](#customizing-argtypes). This is currently available for `DocsPage` and `<ArgsTable story="xxx">` construct, but not for the `<ArgsTable of={component} />` construct,
### Customizing ArgTypes
> **NOTE:** This API is experimental and may change outside of the typical semver release cycle
When you declare a `component` in for your `DocsPage` [as described above](#docspage) or use the `<Props story="xxx" />` construct [in MDX](#controls), the props table shows the `story.argTypes` that gets extracted by Storybook.
When you declare a `component` in for your `DocsPage` [as described above](#docspage) or use the `<ArgsTable story="xxx" />` construct [in MDX](#controls), the props table shows the `story.argTypes` that gets extracted by Storybook.
Consider the following input:
@ -150,13 +150,13 @@ export default {
label: {
description: 'overwritten description',
table: {
type: { summary: 'something short' detail: 'something really really long' },
type: { summary: 'something short', detail: 'something really really long' },
},
control: {
type: null
}
}
}
type: null,
},
},
},
};
```
@ -170,7 +170,7 @@ const argTypes = {
defaultValue: 'Hello',
description: 'overwritten description',
table: {
type: { summary: 'something short' detail: 'something really really long' },
type: { summary: 'something short', detail: 'something really really long' },
defaultValue: { summary: 'Hello' },
}
control: {
@ -182,6 +182,11 @@ const argTypes = {
This would render a row with a modified description, a type display with a dropdown that shows the detail, and no control.
> **NOTE:** `@storybook/addon-docs` provide shorthand for common tasks:
>
> - `type: 'number'` is shorthand for `type: { name: 'number' }`
> - `control: 'radio'` is shorthand for `control: { type: 'radio' }`
Controls customization has an entire section in the [`addon-controls` README](https://github.com/storybookjs/storybook/blob/next/addons/controls/README.md#configuration).
Here are the possible customizations for the rest of the prop table:

View File

@ -14,6 +14,7 @@
- [DocsPage](#docspage)
- [MDX Stories](#mdx-stories)
- [Controlling a story's view mode](#controlling-a-storys-view-mode)
- [Reordering Docs tab first](#reordering-docs-tab-first)
- [Customizing source snippets](#customizing-source-snippets)
- [Overwriting docs container](#overwriting-docs-container)
- [More resources](#more-resources)
@ -44,11 +45,12 @@ Perhaps you want to write your stories in CSF, but document them in MDX? Here's
import React from 'react';
import { Button } from './Button';
export default {
title: 'Demo/Button',
component: Button,
includeStories: [], // or don't load this file at all
};
// NOTE: no default export since `Button.stories.mdx` is the story file for `Button` now
//
// export default {
// title: 'Demo/Button',
// component: Button,
// };
export const basic = () => <Button>Basic</Button>;
basic.parameters = {
@ -60,7 +62,7 @@ basic.parameters = {
```md
import { Meta, Story } from '@storybook/addon-docs/blocks';
import \* as stories from './Button.stories.js';
import * as stories from './Button.stories.js';
import { SomeComponent } from 'path/to/SomeComponent';
<Meta title="Demo/Button" component={Button} />
@ -69,7 +71,7 @@ import { SomeComponent } from 'path/to/SomeComponent';
I can define a story with the function imported from CSF:
<Story name="basic">{stories.basic()}</Story>
<Story story={stories.basic} />
And I can also embed arbitrary markdown & JSX in this file.
@ -79,7 +81,8 @@ And I can also embed arbitrary markdown & JSX in this file.
What's happening here:
- Your stories are defined in CSF, but because of `includeStories: []`, they are not actually added to Storybook.
- The MDX file is simply importing stories as functions in the MDX, and other aspects of the CSF file, such as decorators, parameters, and any other metadata should be applied as needed in the MDX from the import.
- The named story exports are annotated with story-level decorators, parameters, args, and the `<Story story={}>` construct respects this.
- All component-level decorators, parameters, etc. from `Button.stories` default export must be manually copied over into `<Meta>` if desired.
## CSF Stories with arbitrary MDX
@ -226,13 +229,26 @@ Foo.parameters = {
};
```
This can also be applied globally in `preview.js`:
This can also be applied globally in `.storybook/preview.js`:
```js
// always reset the view mode to "docs" whenever the user navigates
addParameters({
export const parameters = {
viewMode: 'docs',
});
};
```
## Reordering Docs tab first
You can configure Storybook's preview tabs with the `previewTabs` story parameter.
Here's how to show the `Docs` tab first for a story (or globally in `.storybook/preview.js`):
```js
export const Foo = () => <Component />;
Foo.parameters = {
previewTabs: { 'storybook/docs/panel': { index: -1 } },
};
```
## Customizing source snippets
@ -254,7 +270,7 @@ Alternatively, you can provide a function in the `docs.transformSource` paramete
const SOURCE_REGEX = /^\(\) => `(.*)`$/;
export const parameters = {
docs: {
transformSource: (src, storyId) => {
transformSource: (src, storyContext) => {
const match = SOURCE_REGEX.exec(src);
return match ? match[1] : src;
},
@ -268,7 +284,7 @@ These two methods are complementary. The former is useful for story-specific, an
What happens if you want to add some wrapper for your MDX page, or add some other kind of React context?
When you're writing stories you can do this by adding a [decorator](https://storybook.js.org/docs/basics/writing-stories/#decorators), but when you're adding arbitrary JSX to your MDX documentation outside of a `<Story>` block, decorators no longer apply, and you need to use the `docs.container` parameter.
When you're writing stories you can do this by adding a [decorator](https://storybook.js.org/docs/react/writing-stories/decorators), but when you're adding arbitrary JSX to your MDX documentation outside of a `<Story>` block, decorators no longer apply, and you need to use the `docs.container` parameter.
The closest Docs equivalent of a decorator is the `container`, a wrapper element that is rendered around the page that is being rendered. Here's an example of adding a solid red border around the page. It uses Storybook's default page container (that sets up various contexts and other magic) and then inserts its own logic between that container and the contents of the page:

View File

@ -9,7 +9,7 @@
## Storybook theming
Storybook theming is the **recommended way** to theme your docs. Docs uses the same theme system as [Storybook UI](https://storybook.js.org/docs/configurations/theming/), but is themed independently from the main UI.
Storybook theming is the **recommended way** to theme your docs. Docs uses the same theme system as [Storybook UI](https://storybook.js.org/docs/react/configure/theming), but is themed independently from the main UI.
Supposing you have a Storybook theme defined for the main UI in `.storybook/manager.js`:

View File

@ -95,7 +95,7 @@ module.exports = {
Finally, you can create MDX files like this:
```md
import { Meta, Story, Props } from '@storybook/addon-docs/blocks';
import { Meta, Story, ArgsTable } from '@storybook/addon-docs/blocks';
import { hbs } from 'ember-cli-htmlbars';
<Meta title='App Component' component='AppComponent' />
@ -109,9 +109,9 @@ Some **markdown** description, or whatever you want.
context: { title: "Title" },
}}</Story>
## Props
## ArgsTable
<Props of='AppComponent' />
<ArgsTable of='AppComponent' />
```
Yes, it's redundant to declare `component` twice. [Coming soon](https://github.com/storybookjs/storybook/issues/8673).

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-docs",
"version": "6.0.0-beta.23",
"version": "6.1.0-alpha.14",
"description": "Superior documentation for your components",
"keywords": [
"addon",
@ -32,61 +32,63 @@
"README.md",
"*.js",
"*.d.ts",
"ts3.5/**/*"
"ts3.4/**/*"
],
"scripts": {
"createDlls": "node -r esm ./scripts/createDlls.js",
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@babel/generator": "^7.9.6",
"@babel/parser": "^7.9.6",
"@babel/plugin-transform-react-jsx": "^7.3.0",
"@babel/preset-env": "^7.9.6",
"@egoist/vue-to-react": "^1.1.0",
"@babel/generator": "^7.11.5",
"@babel/parser": "^7.11.5",
"@babel/plugin-transform-react-jsx": "^7.10.4",
"@babel/preset-env": "^7.11.5",
"@jest/transform": "^26.0.0",
"@mdx-js/loader": "^1.5.1",
"@mdx-js/mdx": "^1.5.1",
"@mdx-js/react": "^1.5.1",
"@storybook/addons": "6.0.0-beta.23",
"@storybook/api": "6.0.0-beta.23",
"@storybook/client-api": "6.0.0-beta.23",
"@storybook/client-logger": "6.0.0-beta.23",
"@storybook/components": "6.0.0-beta.23",
"@storybook/core": "6.0.0-beta.23",
"@storybook/core-events": "6.0.0-beta.23",
"@storybook/addons": "6.1.0-alpha.14",
"@storybook/api": "6.1.0-alpha.14",
"@storybook/client-api": "6.1.0-alpha.14",
"@storybook/client-logger": "6.1.0-alpha.14",
"@storybook/components": "6.1.0-alpha.14",
"@storybook/core": "6.1.0-alpha.14",
"@storybook/core-events": "6.1.0-alpha.14",
"@storybook/csf": "0.0.1",
"@storybook/node-logger": "6.0.0-beta.23",
"@storybook/postinstall": "6.0.0-beta.23",
"@storybook/source-loader": "6.0.0-beta.23",
"@storybook/theming": "6.0.0-beta.23",
"@storybook/node-logger": "6.1.0-alpha.14",
"@storybook/postinstall": "6.1.0-alpha.14",
"@storybook/source-loader": "6.1.0-alpha.14",
"@storybook/theming": "6.1.0-alpha.14",
"acorn": "^7.1.0",
"acorn-jsx": "^5.1.0",
"acorn-walk": "^7.0.0",
"core-js": "^3.0.1",
"doctrine": "^3.0.0",
"escodegen": "^1.12.0",
"fast-deep-equal": "^3.1.1",
"global": "^4.3.2",
"html-tags": "^3.1.0",
"js-string-escape": "^1.0.1",
"lodash": "^4.17.15",
"prop-types": "^15.7.2",
"react-element-to-jsx-string": "^14.1.0",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-element-to-jsx-string": "^14.3.1",
"react-is": "^16.12.0",
"regenerator-runtime": "^0.13.3",
"remark-external-links": "^6.0.0",
"remark-slug": "^6.0.0",
"ts-dedent": "^1.1.1",
"util-deprecate": "^1.0.2",
"vue-docgen-api": "^4.7.0",
"vue-docgen-loader": "^1.4.0"
"util-deprecate": "^1.0.2"
},
"devDependencies": {
"@angular/core": "^9.1.0",
"@babel/core": "^7.9.6",
"@babel/core": "^7.11.5",
"@emotion/core": "^10.0.20",
"@emotion/styled": "^10.0.17",
"@storybook/react": "6.0.0-beta.23",
"@storybook/web-components": "6.0.0-beta.23",
"@storybook/react": "6.1.0-alpha.14",
"@storybook/vue": "6.1.0-alpha.14",
"@storybook/web-components": "6.1.0-alpha.14",
"@types/cross-spawn": "^6.0.1",
"@types/doctrine": "^0.0.3",
"@types/enzyme": "^3.10.3",
@ -101,13 +103,10 @@
"cross-spawn": "^7.0.1",
"fs-extra": "^9.0.0",
"jest": "^26.0.0",
"jest-specific-snapshot": "^3.0.0",
"jest-specific-snapshot": "^4.0.0",
"lit-element": "^2.2.1",
"lit-html": "^1.0.0",
"prettier": "^2.0.5",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-is": "^16.12.0",
"prettier": "~2.0.5",
"require-from-string": "^2.0.2",
"rxjs": "^6.5.4",
"styled-components": "^5.0.1",
@ -119,15 +118,20 @@
"zone.js": "^0.10.2"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0",
"@babel/core": "^7.11.5",
"@storybook/vue": "6.1.0-alpha.14",
"babel-loader": "^8.0.0",
"react": ">=16.3.0",
"react-dom": "*",
"react-is": "^16.8.0",
"sveltedoc-parser": "^3.0.4",
"vue": "^2.6.10",
"webpack": ">=4"
},
"peerDependenciesMeta": {
"@storybook/vue": {
"optional": true
},
"sveltedoc-parser": {
"optional": true
},
"vue": {
"optional": true
},
@ -138,11 +142,11 @@
"publishConfig": {
"access": "public"
},
"gitHead": "4b9d901add9452525135caae98ae5f78dd8da9ff",
"gitHead": "4571582a90b646e361cb37df525f62312486307a",
"typesVersions": {
"<=3.5": {
"<3.8": {
"*": [
"ts3.5/*"
"ts3.4/*"
]
}
}

View File

@ -84,7 +84,7 @@ module.exports = {
Finally, you can create MDX files like this:
```md
import { Meta, Story, Props } from '@storybook/addon-docs/blocks';
import { Meta, Story, ArgsTable } from '@storybook/addon-docs/blocks';
import { Button } from './Button';
<Meta title='Button' component={Button} />
@ -97,9 +97,9 @@ Some **markdown** description, or whatever you want.
<Button>Label</Button>
</Story>
## Props
## ArgsTable
<Props of={Button} />
<ArgsTable of={Button} />
```
## Inline stories
@ -107,13 +107,11 @@ Some **markdown** description, or whatever you want.
Storybook Docs renders all React stories inline on the page by default. If you want to render stories in an `iframe` so that they are better isolated. To do this, update `.storybook/preview.js`:
```js
import { addParameters } from '@storybook/react';
addParameters({
export const parameters = {
docs: {
inlineStories: false,
},
});
};
```
## TypeScript props with `react-docgen`
@ -144,7 +142,7 @@ Neither option is perfect, so here's everything you should know if you're thinki
| Docgen | Build time |
| ----------------------- | ---------- |
| react-docgen-typescript | 59s |
| react-docgen-typescript | 33s |
| react-docgen | 29s |
| none | 28s |

View File

@ -0,0 +1,254 @@
/* eslint-disable no-underscore-dangle */
import React, { FC, useContext, useEffect, useState, useCallback } from 'react';
import mapValues from 'lodash/mapValues';
import pickBy from 'lodash/pickBy';
import {
ArgsTable as PureArgsTable,
ArgsTableProps as PureArgsTableProps,
ArgsTableError,
ArgTypes,
TabbedArgsTable,
} from '@storybook/components';
import { Args } from '@storybook/addons';
import { StoryStore } from '@storybook/client-api';
import Events from '@storybook/core-events';
import { DocsContext, DocsContextProps } from './DocsContext';
import { Component, CURRENT_SELECTION, PRIMARY_STORY } from './types';
import { getComponentName, getDocsStories } from './utils';
import { ArgTypesExtractor } from '../lib/docgen/types';
import { lookupStoryId } from './Story';
type PropDescriptor = string[] | RegExp;
interface BaseProps {
include?: PropDescriptor;
exclude?: PropDescriptor;
}
type OfProps = BaseProps & {
of: '.' | '^' | Component;
};
type ComponentsProps = BaseProps & {
components: {
[label: string]: Component;
};
};
type StoryProps = BaseProps & {
story: '.' | '^' | string;
showComponent?: boolean;
};
type ArgsTableProps = BaseProps | OfProps | ComponentsProps | StoryProps;
const useArgs = (
storyId: string,
storyStore: StoryStore
): [Args, (args: Args) => void, (argNames?: string[]) => void] => {
const story = storyStore.fromId(storyId);
if (!story) {
throw new Error(`Unknown story: ${storyId}`);
}
const { args: initialArgs } = story;
const [args, setArgs] = useState(initialArgs);
useEffect(() => {
const cb = (changed: { storyId: string; args: Args }) => {
if (changed.storyId === storyId) {
setArgs(changed.args);
}
};
storyStore._channel.on(Events.STORY_ARGS_UPDATED, cb);
return () => storyStore._channel.off(Events.STORY_ARGS_UPDATED, cb);
}, [storyId]);
const updateArgs = useCallback((newArgs) => storyStore.updateStoryArgs(storyId, newArgs), [
storyId,
]);
const resetArgs = useCallback(
(argNames?: string[]) => storyStore.resetStoryArgs(storyId, argNames),
[storyId]
);
return [args, updateArgs, resetArgs];
};
const matches = (name: string, descriptor: PropDescriptor) =>
Array.isArray(descriptor) ? descriptor.includes(name) : name.match(descriptor);
const filterArgTypes = (argTypes: ArgTypes, include?: PropDescriptor, exclude?: PropDescriptor) => {
if (!include && !exclude) {
return argTypes;
}
return (
argTypes &&
pickBy(argTypes, (argType, key) => {
const name = argType.name || key;
return (!include || matches(name, include)) && (!exclude || !matches(name, exclude));
})
);
};
export const extractComponentArgTypes = (
component: Component,
{ parameters }: DocsContextProps,
include?: PropDescriptor,
exclude?: PropDescriptor
): ArgTypes => {
const params = parameters || {};
const { extractArgTypes }: { extractArgTypes: ArgTypesExtractor } = params.docs || {};
if (!extractArgTypes) {
throw new Error(ArgsTableError.ARGS_UNSUPPORTED);
}
let argTypes = extractArgTypes(component);
argTypes = filterArgTypes(argTypes, include, exclude);
return argTypes;
};
const isShortcut = (value?: string) => {
return value && [CURRENT_SELECTION, PRIMARY_STORY].includes(value);
};
export const getComponent = (props: ArgsTableProps = {}, context: DocsContextProps): Component => {
const { of } = props as OfProps;
const { story } = props as StoryProps;
const { parameters = {} } = context;
const { component } = parameters;
if (isShortcut(of) || isShortcut(story)) {
return component || null;
}
if (!of) {
throw new Error(ArgsTableError.NO_COMPONENT);
}
return of;
};
const addComponentTabs = (
tabs: Record<string, PureArgsTableProps>,
components: Record<string, Component>,
context: DocsContextProps,
include?: PropDescriptor,
exclude?: PropDescriptor
) => ({
...tabs,
...mapValues(components, (comp) => ({
rows: extractComponentArgTypes(comp, context, include, exclude),
})),
});
export const StoryTable: FC<
StoryProps & { component: Component; subcomponents: Record<string, Component> }
> = (props) => {
const context = useContext(DocsContext);
const {
id: currentId,
parameters: { argTypes },
storyStore,
} = context;
const { story, component, subcomponents, showComponent, include, exclude } = props;
let storyArgTypes;
try {
let storyId;
switch (story) {
case CURRENT_SELECTION: {
storyId = currentId;
storyArgTypes = argTypes;
break;
}
case PRIMARY_STORY: {
const primaryStory = getDocsStories(context)[0];
storyId = primaryStory.id;
storyArgTypes = primaryStory.parameters.argTypes;
break;
}
default: {
storyId = lookupStoryId(story, context);
const data = storyStore.fromId(storyId);
storyArgTypes = data.parameters.argTypes;
}
}
storyArgTypes = filterArgTypes(storyArgTypes, include, exclude);
// eslint-disable-next-line prefer-const
let [args, updateArgs, resetArgs] = useArgs(storyId, storyStore);
let tabs = { Story: { rows: storyArgTypes, args, updateArgs, resetArgs } } as Record<
string,
PureArgsTableProps
>;
// Use the dynamically generated component tabs if there are no controls
const storyHasArgsWithControls =
storyArgTypes && Object.values(storyArgTypes).find((v) => !!v?.control);
if (!storyHasArgsWithControls) {
updateArgs = null;
resetArgs = null;
tabs = {};
}
if (component && (!storyHasArgsWithControls || showComponent)) {
const mainLabel = getComponentName(component);
tabs = addComponentTabs(tabs, { [mainLabel]: component }, context, include, exclude);
}
if (subcomponents) {
if (Array.isArray(subcomponents)) {
throw new Error(
`Unexpected subcomponents array. Expected an object whose keys are tab labels and whose values are components.`
);
}
tabs = addComponentTabs(tabs, subcomponents, context, include, exclude);
}
return <TabbedArgsTable tabs={tabs} />;
} catch (err) {
return <PureArgsTable error={err.message} />;
}
};
export const ComponentsTable: FC<ComponentsProps> = (props) => {
const context = useContext(DocsContext);
const { components, include, exclude } = props;
const tabs = addComponentTabs({}, components, context, include, exclude);
return <TabbedArgsTable tabs={tabs} />;
};
export const ArgsTable: FC<ArgsTableProps> = (props) => {
const context = useContext(DocsContext);
const { parameters: { subcomponents } = {} } = context;
const { include, exclude, components } = props as ComponentsProps;
const { story } = props as StoryProps;
const main = getComponent(props, context);
if (story) {
return <StoryTable {...(props as StoryProps)} component={main} subcomponents={subcomponents} />;
}
if (!components && !subcomponents) {
let mainProps;
try {
mainProps = { rows: extractComponentArgTypes(main, context, include, exclude) };
} catch (err) {
mainProps = { error: err.message };
}
return <PureArgsTable {...mainProps} />;
}
if (components) {
return <ComponentsTable {...(props as ComponentsProps)} components={components} />;
}
const mainLabel = getComponentName(main);
return (
<ComponentsTable
{...(props as ComponentsProps)}
components={{ [mainLabel]: main, ...subcomponents }}
/>
);
};
ArgsTable.defaultProps = {
of: CURRENT_SELECTION,
};

View File

@ -0,0 +1,71 @@
import React, { FC, ReactElement, ReactNode, ReactNodeArray, useContext } from 'react';
import { MDXProvider } from '@mdx-js/react';
import { toId, storyNameFromExport } from '@storybook/csf';
import { resetComponents } from '@storybook/components/html';
import { Preview as PurePreview, PreviewProps as PurePreviewProps } from '@storybook/components';
import { DocsContext, DocsContextProps } from './DocsContext';
import { SourceContext, SourceContextProps } from './SourceContainer';
import { getSourceProps } from './Source';
export enum SourceState {
OPEN = 'open',
CLOSED = 'closed',
NONE = 'none',
}
type CanvasProps = PurePreviewProps & {
withSource?: SourceState;
mdxSource?: string;
};
const getPreviewProps = (
{
withSource = SourceState.CLOSED,
mdxSource,
children,
...props
}: CanvasProps & { children?: ReactNode },
docsContext: DocsContextProps,
sourceContext: SourceContextProps
): PurePreviewProps => {
if (withSource === SourceState.NONE) {
return props;
}
if (mdxSource) {
return {
...props,
withSource: getSourceProps({ code: decodeURI(mdxSource) }, docsContext, sourceContext),
};
}
const childArray: ReactNodeArray = Array.isArray(children) ? children : [children];
const stories = childArray.filter(
(c: ReactElement) => c.props && (c.props.id || c.props.name)
) as ReactElement[];
const { mdxComponentMeta, mdxStoryNameToKey } = docsContext;
const targetIds = stories.map(
(s) =>
s.props.id ||
toId(
mdxComponentMeta.id || mdxComponentMeta.title,
storyNameFromExport(mdxStoryNameToKey[s.props.name])
)
);
const sourceProps = getSourceProps({ ids: targetIds }, docsContext, sourceContext);
return {
...props, // pass through columns etc.
withSource: sourceProps,
isExpanded: withSource === SourceState.OPEN,
};
};
export const Canvas: FC<CanvasProps> = (props) => {
const docsContext = useContext(DocsContext);
const sourceContext = useContext(SourceContext);
const previewProps = getPreviewProps(props, docsContext, sourceContext);
const { children } = props;
return (
<MDXProvider components={resetComponents}>
<PurePreview {...previewProps}>{children}</PurePreview>
</MDXProvider>
);
};

View File

@ -37,8 +37,15 @@ export const getDescriptionProps = (
return { markdown: children || markdown };
}
const { component, notes, info, docs } = parameters;
const { extractComponentDescription = noDescription } = docs || {};
const { extractComponentDescription = noDescription, description } = docs || {};
const target = of === CURRENT_SELECTION ? component : of;
// override component description
const componentDescriptionParameter = description?.component;
if (componentDescriptionParameter) {
return { markdown: componentDescriptionParameter };
}
switch (type) {
case DescriptionType.INFO:
return { markdown: getInfo(info) };

View File

@ -9,6 +9,7 @@ import { components as htmlComponents } from '@storybook/components/html';
import { DocsContextProps, DocsContext } from './DocsContext';
import { anchorBlockIdFromId } from './Anchor';
import { storyBlockIdFromId } from './Story';
import { SourceContainer } from './SourceContainer';
import { CodeOrSourceMdx, AnchorMdx, HeadersMdx } from './mdx';
import { scrollToElement } from './utils';
@ -23,18 +24,21 @@ const defaultComponents = {
...HeadersMdx,
};
const warnOptionsTheme = deprecate(
() => {},
dedent`
Deprecated parameter: options.theme => docs.theme
https://github.com/storybookjs/storybook/blob/next/addons/docs/docs/theming.md#storybook-theming
`
);
export const DocsContainer: FunctionComponent<DocsContainerProps> = ({ context, children }) => {
const { id: storyId = null, parameters = {} } = context || {};
const { options = {}, docs = {} } = parameters;
let themeVars = docs.theme;
if (!themeVars && options.theme) {
deprecate(
() => {},
dedent`
options.theme => Deprecated: use story.parameters.docs.theme instead.
See https://github.com/storybookjs/storybook/blob/next/addons/docs/docs/theming.md#storybook-theming for details.
`
)();
warnOptionsTheme();
themeVars = options.theme;
}
const theme = ensureTheme(themeVars);
@ -61,13 +65,14 @@ export const DocsContainer: FunctionComponent<DocsContainerProps> = ({ context,
document.getElementById(storyBlockIdFromId(storyId));
if (element) {
const allStories = element.parentElement.querySelectorAll('[id|="anchor-"]');
let block = 'start';
let scrollTarget = element;
if (allStories && allStories[0] === element) {
block = 'end'; // first story should be shown with the intro content above
// Include content above first story
scrollTarget = document.getElementById('docs-root');
}
// Introducing a delay to ensure scrolling works when it's a full refresh.
setTimeout(() => {
scrollToElement(element, block);
scrollToElement(scrollTarget, 'start');
}, 200);
}
}
@ -75,13 +80,15 @@ export const DocsContainer: FunctionComponent<DocsContainerProps> = ({ context,
return (
<DocsContext.Provider value={context}>
<ThemeProvider theme={theme}>
<MDXProvider components={allComponents}>
<DocsWrapper className="sbdocs sbdocs-wrapper">
<DocsContent className="sbdocs sbdocs-content">{children}</DocsContent>
</DocsWrapper>
</MDXProvider>
</ThemeProvider>
<SourceContainer>
<ThemeProvider theme={theme}>
<MDXProvider components={allComponents}>
<DocsWrapper className="sbdocs sbdocs-wrapper">
<DocsContent className="sbdocs sbdocs-content">{children}</DocsContent>
</DocsWrapper>
</MDXProvider>
</ThemeProvider>
</SourceContainer>
</DocsContext.Provider>
);
};

View File

@ -1,24 +1,10 @@
import { extractTitle } from './Title';
describe('defaultTitleSlot', () => {
it('showRoots', () => {
const parameters = {
options: { showRoots: true },
};
it('splits on last /', () => {
const parameters = {};
expect(extractTitle({ kind: 'a/b/c', parameters })).toBe('c');
expect(extractTitle({ kind: 'a|b', parameters })).toBe('a|b');
expect(extractTitle({ kind: 'a/b/c.d', parameters })).toBe('c.d');
});
it('no showRoots', () => {
const parameters = {};
expect(extractTitle({ kind: 'a/b/c', parameters })).toBe('c');
expect(extractTitle({ kind: 'a|b', parameters })).toBe('b');
expect(extractTitle({ kind: 'a/b/c.d', parameters })).toBe('d');
});
it('empty options', () => {
const parameters = { options: {} };
expect(extractTitle({ kind: 'a/b/c', parameters })).toBe('c');
expect(extractTitle({ kind: 'a|b', parameters })).toBe('b');
expect(extractTitle({ kind: 'a/b/c.d', parameters })).toBe('d');
});
});

View File

@ -3,9 +3,9 @@ import { Title } from './Title';
import { Subtitle } from './Subtitle';
import { Description } from './Description';
import { Primary } from './Primary';
import { Props } from './Props';
import { Stories } from './Stories';
import { PRIMARY_STORY } from './types';
import { ArgsTable } from './ArgsTable';
import { Stories } from './Stories';
export const DocsPage: FC = () => (
<>
@ -13,7 +13,7 @@ export const DocsPage: FC = () => (
<Subtitle />
<Description />
<Primary />
<Props story={PRIMARY_STORY} />
<ArgsTable story={PRIMARY_STORY} />
<Stories />
</>
);

View File

@ -1,10 +1,21 @@
import React, { FunctionComponent } from 'react';
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
import { Subheading } from './Subheading';
import { DocsStoryProps } from './types';
import { Anchor } from './Anchor';
import { Description } from './Description';
import { Story } from './Story';
import { Preview } from './Preview';
import { Canvas } from './Canvas';
const warnStoryDescription = deprecate(
() => {},
dedent`
Deprecated parameter: docs.storyDescription => docs.description.story
https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#docs-description-parameter
`
);
export const DocsStory: FunctionComponent<DocsStoryProps> = ({
id,
@ -12,14 +23,21 @@ export const DocsStory: FunctionComponent<DocsStoryProps> = ({
expanded = true,
withToolbar = false,
parameters,
}) => (
<Anchor storyId={id}>
{expanded && <Subheading>{name}</Subheading>}
{expanded && parameters && parameters.docs && parameters.docs.storyDescription && (
<Description markdown={parameters.docs.storyDescription} />
)}
<Preview withToolbar={withToolbar}>
<Story id={id} />
</Preview>
</Anchor>
);
}) => {
let description = expanded && parameters?.docs?.description?.story;
if (!description) {
description = parameters?.docs?.storyDescription;
if (description) warnStoryDescription();
}
const subheading = expanded && name;
return (
<Anchor storyId={id}>
{subheading && <Subheading>{subheading}</Subheading>}
{description && <Description markdown={description} />}
<Canvas withToolbar={withToolbar}>
<Story id={id} />
</Canvas>
</Anchor>
);
};

View File

@ -10,7 +10,7 @@ type Decorator = (...args: any) => any;
interface MetaProps {
title: string;
component?: Component;
subcomponents: Record<string, Component>;
subcomponents?: Record<string, Component>;
decorators?: [Decorator];
parameters?: any;
}

View File

@ -1,69 +1,13 @@
import React, { FunctionComponent, ReactElement, ReactNode, ReactNodeArray } from 'react';
import { MDXProvider } from '@mdx-js/react';
import { toId, storyNameFromExport } from '@storybook/csf';
import { resetComponents } from '@storybook/components/html';
import { Preview as PurePreview, PreviewProps as PurePreviewProps } from '@storybook/components';
import { getSourceProps } from './Source';
import { DocsContext, DocsContextProps } from './DocsContext';
import React, { ComponentProps } from 'react';
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
import { Canvas } from './Canvas';
export enum SourceState {
OPEN = 'open',
CLOSED = 'closed',
NONE = 'none',
}
export const Preview = deprecate(
(props: ComponentProps<typeof Canvas>) => <Canvas {...props} />,
dedent`
Preview doc block has been renamed to Canvas.
type PreviewProps = PurePreviewProps & {
withSource?: SourceState;
mdxSource?: string;
};
const getPreviewProps = (
{
withSource = SourceState.CLOSED,
mdxSource,
children,
...props
}: PreviewProps & { children?: ReactNode },
{ mdxStoryNameToKey, mdxComponentMeta, storyStore }: DocsContextProps
): PurePreviewProps => {
if (withSource === SourceState.NONE) {
return props;
}
if (mdxSource) {
return {
...props,
withSource: getSourceProps({ code: decodeURI(mdxSource) }, { storyStore }),
};
}
const childArray: ReactNodeArray = Array.isArray(children) ? children : [children];
const stories = childArray.filter(
(c: ReactElement) => c.props && (c.props.id || c.props.name)
) as ReactElement[];
const targetIds = stories.map(
(s) =>
s.props.id ||
toId(
mdxComponentMeta.id || mdxComponentMeta.title,
storyNameFromExport(mdxStoryNameToKey[s.props.name])
)
);
const sourceProps = getSourceProps({ ids: targetIds }, { storyStore });
return {
...props, // pass through columns etc.
withSource: sourceProps,
isExpanded: withSource === SourceState.OPEN,
};
};
export const Preview: FunctionComponent<PreviewProps> = (props) => (
<DocsContext.Consumer>
{(context) => {
const previewProps = getPreviewProps(props, context);
return (
<MDXProvider components={resetComponents}>
<PurePreview {...previewProps}>{props.children}</PurePreview>
</MDXProvider>
);
}}
</DocsContext.Consumer>
https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#previewprops-renamed
`
);

View File

@ -1,237 +1,19 @@
/* eslint-disable no-underscore-dangle */
import React, { FC, useContext, useEffect, useState, useCallback } from 'react';
import mapValues from 'lodash/mapValues';
import pickBy from 'lodash/pickBy';
import {
ArgsTable,
ArgsTableProps,
ArgsTableError,
ArgTypes,
TabbedArgsTable,
} from '@storybook/components';
import { Args } from '@storybook/addons';
import { StoryStore } from '@storybook/client-api';
import Events from '@storybook/core-events';
import React, { ComponentProps } from 'react';
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
import { ArgsTable } from './ArgsTable';
import { CURRENT_SELECTION } from './types';
import { DocsContext, DocsContextProps } from './DocsContext';
import { Component, CURRENT_SELECTION, PRIMARY_STORY } from './types';
import { getComponentName, getDocsStories } from './utils';
import { ArgTypesExtractor } from '../lib/docgen/types';
import { lookupStoryId } from './Story';
export const Props = deprecate(
(props: ComponentProps<typeof ArgsTable>) => <ArgsTable {...props} />,
dedent`
Props doc block has been renamed to ArgsTable.
type PropDescriptor = string[] | RegExp;
interface BaseProps {
include?: PropDescriptor;
exclude?: PropDescriptor;
}
type OfProps = BaseProps & {
of: '.' | Component;
};
type ComponentsProps = BaseProps & {
components: {
[label: string]: Component;
};
};
type StoryProps = BaseProps & {
story: '.' | string;
showComponents?: boolean;
};
type PropsProps = BaseProps | OfProps | ComponentsProps | StoryProps;
const useArgs = (storyId: string, storyStore: StoryStore): [Args, (args: Args) => void] => {
const story = storyStore.fromId(storyId);
if (!story) {
throw new Error(`Unknown story: ${storyId}`);
}
const { args: initialArgs } = story;
const [args, setArgs] = useState(initialArgs);
useEffect(() => {
const cb = (changedId: string, newArgs: Args) => {
if (changedId === storyId) {
setArgs(newArgs);
}
};
storyStore._channel.on(Events.STORY_ARGS_UPDATED, cb);
return () => storyStore._channel.off(Events.STORY_ARGS_UPDATED, cb);
}, [storyId]);
const updateArgs = useCallback((newArgs) => storyStore.updateStoryArgs(storyId, newArgs), [
storyId,
]);
return [args, updateArgs];
};
const matches = (name: string, descriptor: PropDescriptor) =>
Array.isArray(descriptor) ? descriptor.includes(name) : name.match(descriptor);
const filterArgTypes = (argTypes: ArgTypes, include?: PropDescriptor, exclude?: PropDescriptor) => {
if (!include && !exclude) {
return argTypes;
}
return (
argTypes &&
pickBy(argTypes, (argType, key) => {
const name = argType.name || key;
return (!include || matches(name, include)) && (!exclude || !matches(name, exclude));
})
);
};
export const extractComponentArgTypes = (
component: Component,
{ parameters }: DocsContextProps,
include?: PropDescriptor,
exclude?: PropDescriptor
): ArgTypes => {
const params = parameters || {};
const { extractArgTypes }: { extractArgTypes: ArgTypesExtractor } = params.docs || {};
if (!extractArgTypes) {
throw new Error(ArgsTableError.ARGS_UNSUPPORTED);
}
let argTypes = extractArgTypes(component);
argTypes = filterArgTypes(argTypes, include, exclude);
return argTypes;
};
export const getComponent = (props: PropsProps = {}, context: DocsContextProps): Component => {
const { of } = props as OfProps;
const { parameters = {} } = context;
const { component } = parameters;
const target = of === CURRENT_SELECTION ? component : of;
if (!target) {
if (of === CURRENT_SELECTION) {
return null;
}
throw new Error(ArgsTableError.NO_COMPONENT);
}
return target;
};
const addComponentTabs = (
tabs: Record<string, ArgsTableProps>,
components: Record<string, Component>,
context: DocsContextProps,
include?: PropDescriptor,
exclude?: PropDescriptor
) => ({
...tabs,
...mapValues(components, (comp) => ({
rows: extractComponentArgTypes(comp, context, include, exclude),
})),
});
export const StoryTable: FC<StoryProps & { components: Record<string, Component> }> = (props) => {
const context = useContext(DocsContext);
const {
id: currentId,
parameters: { argTypes },
storyStore,
} = context;
const { story, showComponents, components, include, exclude } = props;
let storyArgTypes;
try {
let storyId;
switch (story) {
case CURRENT_SELECTION: {
storyId = currentId;
storyArgTypes = argTypes;
break;
}
case PRIMARY_STORY: {
const primaryStory = getDocsStories(context)[0];
storyId = primaryStory.id;
storyArgTypes = primaryStory.parameters.argTypes;
break;
}
default: {
storyId = lookupStoryId(story, context);
const data = storyStore.fromId(storyId);
storyArgTypes = data.parameters.argTypes;
}
}
storyArgTypes = filterArgTypes(storyArgTypes, include, exclude);
// This code handles three cases:
// 1. the story has args, in which case we want to show controls for the story
// 2. the story has args, and the user specifies showComponents, in which case
// we want to show controls for the primary component AND show props for each component
// 3. the story has NO args, in which case we want to show props for each component
// eslint-disable-next-line prefer-const
let [args, updateArgs] = useArgs(storyId, storyStore);
let tabs = { Story: { rows: storyArgTypes, args, updateArgs } } as Record<
string,
ArgsTableProps
>;
// Use the dynamically generated component tabs if there are no controls
const storyHasArgsWithControls =
storyArgTypes && Object.values(storyArgTypes).find((v) => !!v?.control);
if (!storyHasArgsWithControls) {
updateArgs = null;
tabs = {};
}
if (showComponents || !storyHasArgsWithControls) {
tabs = addComponentTabs(tabs, components, context, include, exclude);
}
return <TabbedArgsTable tabs={tabs} />;
} catch (err) {
return <ArgsTable error={err.message} />;
}
};
export const ComponentsTable: FC<ComponentsProps> = (props) => {
const context = useContext(DocsContext);
const { components, include, exclude } = props;
const tabs = addComponentTabs({}, components, context, include, exclude);
return <TabbedArgsTable tabs={tabs} />;
};
export const Props: FC<PropsProps> = (props) => {
const context = useContext(DocsContext);
const {
parameters: { subcomponents },
} = context;
const { include, exclude, components } = props as ComponentsProps;
const { story } = props as StoryProps;
let allComponents = components;
const main = getComponent(props, context);
if (!allComponents && main) {
const mainLabel = getComponentName(main);
allComponents = { [mainLabel]: main, ...subcomponents };
}
if (story) {
return <StoryTable {...(props as StoryProps)} components={allComponents} />;
}
if (!components && !subcomponents) {
let mainProps;
try {
mainProps = { rows: extractComponentArgTypes(main, context, include, exclude) };
} catch (err) {
mainProps = { error: err.message };
}
return <ArgsTable {...mainProps} />;
}
return <ComponentsTable {...(props as ComponentsProps)} components={allComponents} />;
};
https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#previewprops-renamed
`
);
// @ts-ignore
Props.defaultProps = {
of: CURRENT_SELECTION,
};

View File

@ -1,12 +1,24 @@
import React, { FunctionComponent } from 'react';
import { Source, SourceProps as PureSourceProps, SourceError } from '@storybook/components';
import React, { FC, useContext } from 'react';
import {
Source as PureSource,
SourceError,
SourceProps as PureSourceProps,
} from '@storybook/components';
import { StoryId } from '@storybook/api';
import { logger } from '@storybook/client-logger';
import { StoryContext } from '@storybook/addons';
import { DocsContext, DocsContextProps } from './DocsContext';
import { SourceContext, SourceContextProps } from './SourceContainer';
import { CURRENT_SELECTION } from './types';
import { SourceType } from '../shared';
import { enhanceSource } from './enhanceSource';
interface CommonProps {
language?: string;
dark?: boolean;
code?: string;
}
type SingleSourceProps = {
@ -25,10 +37,70 @@ type NoneProps = CommonProps;
type SourceProps = SingleSourceProps | MultiSourceProps | CodeProps | NoneProps;
const getStoryContext = (storyId: StoryId, docsContext: DocsContextProps): StoryContext | null => {
const { storyStore } = docsContext;
const storyContext = storyStore?.fromId(storyId);
if (!storyContext) {
// Fallback if we can't get the story data for this story
logger.warn(`Unable to find information for story ID '${storyId}'`);
return null;
}
return storyContext;
};
const getStorySource = (storyId: StoryId, sourceContext: SourceContextProps): string => {
const { sources } = sourceContext;
const source = sources?.[storyId];
if (!source) {
logger.warn(`Unable to find source for story ID '${storyId}'`);
return '';
}
return source;
};
const getSnippet = (snippet: string, storyContext?: StoryContext): string => {
if (!storyContext) {
return snippet;
}
const { parameters } = storyContext;
// eslint-disable-next-line no-underscore-dangle
const isArgsStory = parameters.__isArgsStory;
const type = parameters.docs?.source?.type || SourceType.AUTO;
// if user has hard-coded the snippet, that takes precedence
const userCode = parameters.docs?.source?.code;
if (userCode) {
return userCode;
}
// if user has explicitly set this as dynamic, use snippet
if (type === SourceType.DYNAMIC) {
return parameters.docs?.transformSource?.(snippet, storyContext) || snippet;
}
// if this is an args story and there's a snippet
if (type === SourceType.AUTO && snippet && isArgsStory) {
return parameters.docs?.transformSource?.(snippet, storyContext) || snippet;
}
// otherwise, use the source code logic
const enhanced = enhanceSource(storyContext) || parameters;
return enhanced?.docs?.source?.code || '';
};
export const getSourceProps = (
props: SourceProps,
{ id: currentId, storyStore }: DocsContextProps
docsContext: DocsContextProps,
sourceContext: SourceContextProps
): PureSourceProps => {
const { id: currentId } = docsContext;
const codeProps = props as CodeProps;
const singleProps = props as SingleSourceProps;
const multiProps = props as MultiSourceProps;
@ -39,10 +111,10 @@ export const getSourceProps = (
singleProps.id === CURRENT_SELECTION || !singleProps.id ? currentId : singleProps.id;
const targetIds = multiProps.ids || [targetId];
source = targetIds
.map((sourceId) => {
const data = storyStore.fromId(sourceId);
const enhanced = data && (enhanceSource(data) || data.parameters);
return enhanced?.docs?.source?.code || '';
.map((storyId) => {
const storySource = getStorySource(storyId, sourceContext);
const storyContext = getStoryContext(storyId, docsContext);
return getSnippet(storySource, storyContext);
})
.join('\n\n');
}
@ -56,13 +128,9 @@ export const getSourceProps = (
* or the source for a story if `storyId` is provided, or
* the source for the current story if nothing is provided.
*/
const SourceContainer: FunctionComponent<SourceProps> = (props) => (
<DocsContext.Consumer>
{(context) => {
const sourceProps = getSourceProps(props, context);
return <Source {...sourceProps} />;
}}
</DocsContext.Consumer>
);
export { SourceContainer as Source };
export const Source: FC<SourceProps> = (props) => {
const sourceContext = useContext(SourceContext);
const docsContext = useContext(DocsContext);
const sourceProps = getSourceProps(props, docsContext, sourceContext);
return <PureSource {...sourceProps} />;
};

View File

@ -0,0 +1,44 @@
import React, { FC, Context, createContext, useEffect, useState } from 'react';
import deepEqual from 'fast-deep-equal';
import { addons } from '@storybook/addons';
import { StoryId } from '@storybook/api';
import { SNIPPET_RENDERED } from '../shared';
export type SourceItem = string;
export type StorySources = Record<StoryId, SourceItem>;
export interface SourceContextProps {
sources: StorySources;
setSource?: (id: StoryId, item: SourceItem) => void;
}
export const SourceContext: Context<SourceContextProps> = createContext({ sources: {} });
export const SourceContainer: FC<{}> = ({ children }) => {
const [sources, setSources] = useState<StorySources>({});
const channel = addons.getChannel();
const sourcesRef = React.useRef<StorySources>();
const handleSnippetRendered = (id: StoryId, newItem: SourceItem) => {
if (newItem !== sources[id]) {
const newSources = { ...sourcesRef.current, [id]: newItem };
sourcesRef.current = newSources;
}
};
// Bind this early (instead of inside `useEffect`), because the `SNIPPET_RENDERED` event
// is triggered *during* the rendering process, not after. We have to use the ref
// to ensure we don't end up calling setState outside the effect though.
channel.on(SNIPPET_RENDERED, handleSnippetRendered);
useEffect(() => {
const current = sourcesRef.current || {};
if (!deepEqual(sources, current)) {
setSources(current);
}
return () => channel.off(SNIPPET_RENDERED, handleSnippetRendered);
});
return <SourceContext.Provider value={{ sources }}>{children}</SourceContext.Provider>;
};

View File

@ -1,4 +1,4 @@
import React, { FunctionComponent, ReactNode, ComponentProps } from 'react';
import React, { FunctionComponent, ReactNode, ElementType, ComponentProps } from 'react';
import { MDXProvider } from '@mdx-js/react';
import { resetComponents } from '@storybook/components/html';
import { Story as PureStory } from '@storybook/components';
@ -25,6 +25,11 @@ type StoryRefProps = {
id?: string;
} & CommonProps;
type StoryImportProps = {
name: string;
story: ElementType;
} & CommonProps;
export type StoryProps = StoryDefProps | StoryRefProps;
export const lookupStoryId = (
@ -64,7 +69,7 @@ export const getStoryProps = (props: StoryProps, context: DocsContextProps): Pur
parameters,
inline: storyIsInline,
id: previewId,
storyFn: prepareForInline && storyFn ? () => prepareForInline(storyFn) : storyFn,
storyFn: prepareForInline && storyFn ? () => prepareForInline(storyFn, data) : storyFn,
height: height || (storyIsInline ? undefined : iframeHeight),
title: storyName,
};

View File

@ -1,5 +1,4 @@
import React, { useContext, FunctionComponent } from 'react';
import { parseKind } from '@storybook/csf';
import { Title as PureTitle } from '@storybook/components';
import { DocsContext, DocsContextProps } from './DocsContext';
@ -7,22 +6,7 @@ interface TitleProps {
children?: JSX.Element | string;
}
export const extractTitle = ({ kind, parameters }: DocsContextProps) => {
const {
showRoots,
hierarchyRootSeparator: rootSeparator = '|',
hierarchySeparator: groupSeparator = /\/|\./,
} = (parameters && parameters.options) || {};
let groups;
if (typeof showRoots !== 'undefined') {
groups = kind.split('/');
} else {
// This covers off all the remaining cases:
// - If the separators were set above, we should use them
// - If they weren't set, we should only should use the old defaults if the kind contains '.' or '|',
// which for this particular splitting is the only case in which it actually matters.
({ groups } = parseKind(kind, { rootSeparator, groupSeparator }));
}
const groups = kind.split('/');
return (groups && groups[groups.length - 1]) || kind;
};

View File

@ -6,12 +6,11 @@ const emptyContext: StoryContext = {
kind: 'foo',
name: 'bar',
args: {},
globalArgs: {},
argTypes: {},
globals: {},
parameters: {},
};
const transformSource = (src?: string) => (src ? `formatted: ${src}` : 'no src');
describe('addon-docs enhanceSource', () => {
describe('no source loaded', () => {
const baseContext = emptyContext;
@ -19,14 +18,16 @@ describe('addon-docs enhanceSource', () => {
expect(enhanceSource(baseContext)).toBeNull();
});
it('transformSource', () => {
const transformSource = (src?: string) => (src ? `formatted: ${src}` : 'no src');
const parameters = { ...baseContext.parameters, docs: { transformSource } };
expect(enhanceSource({ ...baseContext, parameters })).toBeNull();
});
});
describe('custom/mdx source loaded', () => {
const source = 'storySource.source';
const baseContext = {
...emptyContext,
parameters: { storySource: { source: 'storySource.source' } },
parameters: { storySource: { source } },
};
it('no transformSource', () => {
expect(enhanceSource(baseContext)).toEqual({
@ -34,11 +35,19 @@ describe('addon-docs enhanceSource', () => {
});
});
it('transformSource', () => {
const transformSource = (src?: string) => (src ? `formatted: ${src}` : 'no src');
const parameters = { ...baseContext.parameters, docs: { transformSource } };
expect(enhanceSource({ ...baseContext, parameters }).docs.source).toEqual({
code: 'formatted: storySource.source',
});
});
it('receives context as 2nd argument', () => {
const transformSource = jest.fn();
const parameters = { ...baseContext.parameters, docs: { transformSource } };
const context = { ...baseContext, parameters };
enhanceSource(context);
expect(transformSource).toHaveBeenCalledWith(source, context);
});
});
describe('storysource source loaded w/ locationsMap', () => {
const baseContext = {
@ -47,7 +56,7 @@ describe('addon-docs enhanceSource', () => {
storySource: {
source: 'storySource.source',
locationsMap: {
'foo--bar': { startBody: { line: 1, col: 5 }, endBody: { line: 1, col: 11 } },
bar: { startBody: { line: 1, col: 5 }, endBody: { line: 1, col: 11 } },
},
},
},
@ -56,11 +65,19 @@ describe('addon-docs enhanceSource', () => {
expect(enhanceSource(baseContext)).toEqual({ docs: { source: { code: 'Source' } } });
});
it('transformSource', () => {
const transformSource = (src?: string) => (src ? `formatted: ${src}` : 'no src');
const parameters = { ...baseContext.parameters, docs: { transformSource } };
expect(enhanceSource({ ...baseContext, parameters }).docs.source).toEqual({
code: 'formatted: Source',
});
});
it('receives context as 2nd argument', () => {
const transformSource = jest.fn();
const parameters = { ...baseContext.parameters, docs: { transformSource } };
const context = { ...baseContext, parameters };
enhanceSource(context);
expect(transformSource).toHaveBeenCalledWith('Source', context);
});
});
describe('custom docs.source provided', () => {
const baseContext = {
@ -74,6 +91,7 @@ describe('addon-docs enhanceSource', () => {
expect(enhanceSource(baseContext)).toBeNull();
});
it('transformSource', () => {
const transformSource = (src?: string) => (src ? `formatted: ${src}` : 'no src');
const { source } = baseContext.parameters.docs;
const parameters = { ...baseContext.parameters, docs: { source, transformSource } };
expect(enhanceSource({ ...baseContext, parameters })).toBeNull();

View File

@ -1,40 +1,28 @@
import { combineParameters } from '@storybook/client-api';
import { StoryContext, Parameters } from '@storybook/addons';
interface Location {
line: number;
col: number;
}
import { extractSource, LocationsMap } from '@storybook/source-loader/extract-source';
interface StorySource {
source: string;
locationsMap: { [id: string]: { startBody: Location; endBody: Location } };
locationsMap: LocationsMap;
}
/**
* Replaces full story id name like: story-kind--story-name -> story-name
* @param id
*/
const storyIdToSanitizedStoryName = (id: string) => id.replace(/^.*?--/, '');
const extract = (targetId: string, { source, locationsMap }: StorySource) => {
if (!locationsMap) {
return source;
}
const location = locationsMap[targetId];
// FIXME: bad locationsMap generated for module export functions whose titles are overridden
if (!location) return null;
const { startBody: start, endBody: end } = location;
const sanitizedStoryName = storyIdToSanitizedStoryName(targetId);
const location = locationsMap[sanitizedStoryName];
const lines = source.split('\n');
if (start.line === end.line && lines[start.line - 1] !== undefined) {
return lines[start.line - 1].substring(start.col, end.col);
}
// NOTE: storysource locations are 1-based not 0-based!
const startLine = lines[start.line - 1];
const endLine = lines[end.line - 1];
if (startLine === undefined || endLine === undefined) {
return source;
}
return [
startLine.substring(start.col),
...lines.slice(start.line, end.line - 1),
endLine.substring(0, end.col),
].join('\n');
return extractSource(location, lines);
};
export const enhanceSource = (context: StoryContext): Parameters => {
@ -48,7 +36,7 @@ export const enhanceSource = (context: StoryContext): Parameters => {
}
const input = extract(id, storySource);
const code = transformSource ? transformSource(input, id) : input;
const code = transformSource ? transformSource(input, context) : input;
return { docs: combineParameters(docs, { source: { code } }) };
};

View File

@ -1,6 +1,8 @@
export { ColorPalette, ColorItem, IconGallery, IconItem, Typeset } from '@storybook/components';
export * from './Anchor';
export * from './ArgsTable';
export * from './Canvas';
export * from './Description';
export * from './DocsContext';
export * from './DocsPage';

View File

@ -93,7 +93,11 @@ const getComponentData = (component: Component | Directive) => {
const compodocJson = getCompdocJson();
checkValidCompodocJson(compodocJson);
const { name } = component;
return findComponentByName(name, compodocJson);
const metadata = findComponentByName(name, compodocJson);
if (!metadata) {
logger.warn(`Component not found in compodoc JSON: '${name}'`);
}
return metadata;
};
const displaySignature = (item: Method): string => {
@ -221,8 +225,5 @@ export const extractArgTypes = (component: Component | Directive) => {
export const extractComponentDescription = (component: Component | Directive) => {
const componentData = getComponentData(component);
if (!componentData) {
return null;
}
return componentData.rawdescription || componentData.description;
return componentData && (componentData.rawdescription || componentData.description);
};

View File

@ -1,9 +1,8 @@
import { addParameters } from '@storybook/client-api';
import { extractArgTypes, extractComponentDescription } from './compodoc';
addParameters({
export const parameters = {
docs: {
extractArgTypes,
extractComponentDescription,
},
});
};

View File

@ -1,4 +1,4 @@
import { ArgType, ArgTypes, Args } from '@storybook/api';
import { ArgType, ArgTypes } from '@storybook/api';
import { enhanceArgTypes } from './enhanceArgTypes';
expect.addSnapshotSerializer({
@ -10,20 +10,20 @@ const enhance = ({
argType,
arg,
extractedArgTypes,
storyFn = (args: Args) => 0,
isArgsStory = true,
}: {
argType?: ArgType;
arg?: any;
extractedArgTypes?: ArgTypes;
storyFn?: any;
isArgsStory?: boolean;
}) => {
const context = {
id: 'foo--bar',
kind: 'foo',
name: 'bar',
storyFn,
parameters: {
component: 'dummy',
__isArgsStory: isArgsStory,
docs: {
extractArgTypes: extractedArgTypes && (() => extractedArgTypes),
},
@ -35,7 +35,8 @@ const enhance = ({
},
},
args: {},
globalArgs: {},
argTypes: {},
globals: {},
};
return enhanceArgTypes(context);
};
@ -46,7 +47,7 @@ describe('enhanceArgTypes', () => {
expect(
enhance({
argType: { foo: 'unmodified', type: { name: 'number' } },
storyFn: () => 0,
isArgsStory: false,
}).input
).toMatchInlineSnapshot(`
{
@ -69,25 +70,6 @@ describe('enhanceArgTypes', () => {
}).input
).toMatchInlineSnapshot(`
{
"control": {
"type": "number"
},
"name": "input",
"type": {
"name": "number"
}
}
`);
});
});
describe('args input', () => {
it('number', () => {
expect(enhance({ arg: 5 }).input).toMatchInlineSnapshot(`
{
"control": {
"type": "number"
},
"name": "input",
"type": {
"name": "number"
@ -104,9 +86,6 @@ describe('enhanceArgTypes', () => {
.input
).toMatchInlineSnapshot(`
{
"control": {
"type": "number"
},
"name": "input",
"type": {
"name": "number"
@ -163,9 +142,6 @@ describe('enhanceArgTypes', () => {
}).input
).toMatchInlineSnapshot(`
{
"control": {
"type": "number"
},
"type": {
"name": "number"
},
@ -182,9 +158,6 @@ describe('enhanceArgTypes', () => {
}).input
).toMatchInlineSnapshot(`
{
"control": {
"type": "number"
},
"name": "input",
"type": {
"name": "number"
@ -201,10 +174,6 @@ describe('enhanceArgTypes', () => {
}).input
).toMatchInlineSnapshot(`
{
"control": {
"type": "text"
},
"name": "input",
"type": {
"name": "string"
}
@ -221,15 +190,12 @@ describe('enhanceArgTypes', () => {
}).input
).toMatchInlineSnapshot(`
{
"name": "input",
"defaultValue": 5,
"control": {
"type": "range",
"step": 50
},
"name": "input",
"type": {
"name": "number"
},
"defaultValue": 5
}
}
`);
});
@ -243,18 +209,9 @@ describe('enhanceArgTypes', () => {
).toMatchInlineSnapshot(`
{
"input": {
"control": {
"type": "number"
},
"name": "input",
"type": {
"name": "number"
}
"name": "input"
},
"foo": {
"control": {
"type": "number"
},
"type": {
"name": "number"
}
@ -272,18 +229,12 @@ describe('enhanceArgTypes', () => {
).toMatchInlineSnapshot(`
{
"input": {
"control": {
"type": "number"
},
"name": "input",
"type": {
"name": "number"
}
},
"foo": {
"control": {
"type": "number"
},
"type": {
"name": "number"
}
@ -300,10 +251,12 @@ describe('enhanceArgTypes', () => {
})
).toMatchInlineSnapshot(`
{
"foo": {
"type": {
"name": "number"
}
},
"input": {
"control": {
"type": "number"
},
"name": "input",
"type": {
"name": "number"

View File

@ -1,41 +1,17 @@
import mapValues from 'lodash/mapValues';
import { ArgTypesEnhancer, combineParameters } from '@storybook/client-api';
import { ArgTypes } from '@storybook/api';
import { inferArgTypes } from './inferArgTypes';
import { inferControls } from './inferControls';
const isSubset = (kind: string, subset: object, superset: object) => {
const keys = Object.keys(subset);
// eslint-disable-next-line no-prototype-builtins
const overlap = keys.filter((key) => superset.hasOwnProperty(key));
return overlap.length === keys.length;
};
import { normalizeArgTypes } from './normalizeArgTypes';
export const enhanceArgTypes: ArgTypesEnhancer = (context) => {
const { component, argTypes: userArgTypes = {}, docs = {}, args = {} } = context.parameters;
const { extractArgTypes, forceExtractedArgTypes = false } = docs;
const { component, argTypes: userArgTypes = {}, docs = {} } = context.parameters;
const { extractArgTypes } = docs;
const namedArgTypes = mapValues(userArgTypes, (val, key) => ({ name: key, ...val }));
const inferredArgTypes = inferArgTypes(args);
let extractedArgTypes: ArgTypes = extractArgTypes && component ? extractArgTypes(component) : {};
const normalizedArgTypes = normalizeArgTypes(userArgTypes);
const namedArgTypes = mapValues(normalizedArgTypes, (val, key) => ({ name: key, ...val }));
const extractedArgTypes = extractArgTypes && component ? extractArgTypes(component) : {};
const withExtractedTypes = extractedArgTypes
? combineParameters(extractedArgTypes, namedArgTypes)
: namedArgTypes;
if (
!forceExtractedArgTypes &&
((Object.keys(userArgTypes).length > 0 &&
!isSubset(context.kind, userArgTypes, extractedArgTypes)) ||
(Object.keys(inferredArgTypes).length > 0 &&
!isSubset(context.kind, inferredArgTypes, extractedArgTypes)))
) {
extractedArgTypes = {};
}
const withArgTypes = combineParameters(inferredArgTypes, extractedArgTypes, namedArgTypes);
if (context.storyFn.length === 0) {
return withArgTypes;
}
const withControls = inferControls(withArgTypes);
const result = combineParameters(withControls, withArgTypes);
return result;
return withExtractedTypes;
};

View File

@ -0,0 +1,18 @@
import mapValues from 'lodash/mapValues';
import { ArgTypes } from '@storybook/api';
import { SBType } from '@storybook/client-api';
const normalizeType = (type: SBType | string) => (typeof type === 'string' ? { name: type } : type);
const normalizeControl = (control?: any) =>
typeof control === 'string' ? { type: control } : control;
export const normalizeArgTypes = (argTypes: ArgTypes) =>
mapValues(argTypes, (argType) => {
if (!argType) return argType;
const normalized = { ...argType };
const { type, control } = argType;
if (type) normalized.type = normalizeType(type);
if (control) normalized.control = normalizeControl(control);
return normalized;
});

View File

@ -13,18 +13,28 @@ const context = coreDirName.includes('node_modules')
? path.join(coreDirName, '../../') // Real life case, already in node_modules
: path.join(coreDirName, '../../node_modules'); // SB Monorepo
function createBabelOptions(babelOptions?: any, configureJSX?: boolean) {
if (!configureJSX) {
return babelOptions;
}
const babelPlugins = (babelOptions && babelOptions.plugins) || [];
// for frameworks that are not working with react, we need to configure
// the jsx to transpile mdx, for now there will be a flag for that
// for more complex solutions we can find alone that we need to add '@babel/plugin-transform-react-jsx'
type BabelParams = {
babelOptions?: any;
mdxBabelOptions?: any;
configureJSX?: boolean;
};
function createBabelOptions({ babelOptions, mdxBabelOptions, configureJSX }: BabelParams) {
const babelPlugins = mdxBabelOptions?.plugins || babelOptions?.plugins || [];
const jsxPlugin = [
require.resolve('@babel/plugin-transform-react-jsx'),
{ pragma: 'React.createElement', pragmaFrag: 'React.Fragment' },
];
const plugins = configureJSX ? [...babelPlugins, jsxPlugin] : babelPlugins;
return {
// don't use the root babelrc by default (users can override this in mdxBabelOptions)
babelrc: false,
configFile: false,
...babelOptions,
// for frameworks that are not working with react, we need to configure
// the jsx to transpile mdx, for now there will be a flag for that
// for more complex solutions we can find alone that we need to add '@babel/plugin-transform-react-jsx'
plugins: [...babelPlugins, '@babel/plugin-transform-react-jsx'],
...mdxBabelOptions,
plugins,
};
}
@ -38,8 +48,10 @@ export function webpack(webpackConfig: any = {}, options: any = {}) {
// also, these babel options are chained with other presets.
const {
babelOptions,
configureJSX = options.framework !== 'react', // if not user-specified
sourceLoaderOptions = {},
mdxBabelOptions,
configureJSX = true,
sourceLoaderOptions = { injectStoryParameters: true },
transcludeMarkdown = false,
} = options;
const mdxLoaderOptions = {
@ -58,12 +70,32 @@ export function webpack(webpackConfig: any = {}, options: any = {}) {
]
: [];
let rules = module.rules || [];
if (transcludeMarkdown) {
rules = [
...rules.filter((rule: any) => rule.test.toString() !== '/\\.md$/'),
{
test: /\.md$/,
use: [
{
loader: require.resolve('babel-loader'),
options: createBabelOptions({ babelOptions, mdxBabelOptions, configureJSX }),
},
{
loader: require.resolve('@mdx-js/loader'),
options: mdxLoaderOptions,
},
],
},
];
}
const result = {
...webpackConfig,
module: {
...module,
rules: [
...(module.rules || []),
...rules,
{
test: /\.js$/,
include: new RegExp(`node_modules\\${path.sep}acorn-jsx`),
@ -81,7 +113,7 @@ export function webpack(webpackConfig: any = {}, options: any = {}) {
use: [
{
loader: require.resolve('babel-loader'),
options: createBabelOptions(babelOptions, configureJSX),
options: createBabelOptions({ babelOptions, mdxBabelOptions, configureJSX }),
},
{
loader: require.resolve('@mdx-js/loader'),
@ -98,7 +130,7 @@ export function webpack(webpackConfig: any = {}, options: any = {}) {
use: [
{
loader: require.resolve('babel-loader'),
options: createBabelOptions(babelOptions, configureJSX),
options: createBabelOptions({ babelOptions, mdxBabelOptions, configureJSX }),
},
{
loader: require.resolve('@mdx-js/loader'),

View File

@ -1,10 +1,9 @@
import { addParameters } from '@storybook/client-api';
import { extractArgTypes, extractComponentDescription } from './jsondoc';
addParameters({
export const parameters = {
docs: {
iframeHeight: 80,
extractArgTypes,
extractComponentDescription,
},
});
};

View File

@ -18,20 +18,21 @@ export const extractArgTypes = (componentName) => {
if (!componentDoc) {
return null;
}
const rows = componentDoc.attributes.arguments.map((prop) => {
return {
return componentDoc.attributes.arguments.reduce((acc, prop) => {
acc[prop.name] = {
name: prop.name,
defaultValue: prop.defaultValue,
description: prop.description,
table: {
defaultValue: { summary: prop.defaultValue },
type: {
summary: prop.type,
required: prop.tags.length ? prop.tags.some((tag) => tag.name === 'required') : false,
},
},
};
});
return { rows };
return acc;
}, {});
};
export const extractComponentDescription = (componentName) => {

View File

@ -0,0 +1,16 @@
import React from 'react';
import { StoryFn } from '@storybook/addons';
export const parameters = {
docs: {
inlineStories: true,
prepareForInline: (storyFn: StoryFn<string>) => {
const html = storyFn();
if (typeof html === 'string') {
// eslint-disable-next-line react/no-danger
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
return <div ref={(node) => (node ? node.appendChild(html) : null)} />;
},
},
};

View File

@ -2,7 +2,7 @@
exports[`react component properties 8894-9511-ts-forward-ref 1`] = `
"import React, { forwardRef } from 'react';
const Button = forwardRef(({
const Button = /*#__PURE__*/forwardRef(({
disabled = false,
variant = 'small',
children

View File

@ -18,7 +18,7 @@ Button.propTypes = {
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired
};
const MemoButton = React.memo(Button);
const MemoButton = /*#__PURE__*/React.memo(Button);
export const component = MemoButton;
Button.__docgenInfo = {
\\"description\\": \\"\\",

View File

@ -1,6 +1,7 @@
import { StoryFn } from '@storybook/addons';
import { extractArgTypes } from './extractArgTypes';
import { extractComponentDescription } from '../../lib/docgen';
import { jsxDecorator } from './jsxDecorator';
export const parameters = {
docs: {
@ -11,3 +12,5 @@ export const parameters = {
extractComponentDescription,
},
};
export const decorators = [jsxDecorator];

View File

@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
import { isForwardRef, isMemo } from 'react-is';
import { isMemo } from 'react-is';
import {
PropDef,
hasDocgen,
@ -29,13 +29,8 @@ function getPropDefs(component: Component, section: string): PropDef[] {
let processedComponent = component;
// eslint-disable-next-line react/forbid-foreign-prop-types
if (!hasDocgen(component) && !component.propTypes) {
if (isForwardRef(component) || component.render) {
processedComponent = component.render({}).type;
}
if (isMemo(component)) {
processedComponent = component.type().type;
}
if (!hasDocgen(component) && !component.propTypes && isMemo(component)) {
processedComponent = component.type().type;
}
const extractedProps = extractComponentProps(processedComponent, section);

View File

@ -0,0 +1,182 @@
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
import React from 'react';
import range from 'lodash/range';
import addons, { StoryContext } from '@storybook/addons';
import { renderJsx, jsxDecorator } from './jsxDecorator';
import { SNIPPET_RENDERED } from '../../shared';
jest.mock('@storybook/addons');
const mockedAddons = addons as jest.Mocked<typeof addons>;
expect.addSnapshotSerializer({
print: (val: any) => val,
test: (val) => typeof val === 'string',
});
describe('renderJsx', () => {
it('basic', () => {
expect(renderJsx(<div>hello</div>, {})).toMatchInlineSnapshot(`
<div>
hello
</div>
`);
});
it('functions', () => {
// eslint-disable-next-line no-console
const onClick = () => console.log('onClick');
expect(renderJsx(<div onClick={onClick}>hello</div>, {})).toMatchInlineSnapshot(`
<div onClick={() => {}}>
hello
</div>
`);
});
it('undefined values', () => {
expect(renderJsx(<div className={undefined}>hello</div>, {})).toMatchInlineSnapshot(`
<div>
hello
</div>
`);
});
it('null values', () => {
expect(renderJsx(<div className={null}>hello</div>, {})).toMatchInlineSnapshot(`
<div className={null}>
hello
</div>
`);
});
it('large objects', () => {
const obj: Record<string, string> = {};
range(20).forEach((i) => {
obj[`key_${i}`] = `val_${i}`;
});
expect(renderJsx(<div data-val={obj} />, {})).toMatchInlineSnapshot(`
<div
data-val={{
key_0: 'val_0',
key_1: 'val_1',
key_10: 'val_10',
key_11: 'val_11',
key_12: 'val_12',
key_13: 'val_13',
key_14: 'val_14',
key_15: 'val_15',
key_16: 'val_16',
key_17: 'val_17',
key_18: 'val_18',
key_19: 'val_19',
key_2: 'val_2',
key_3: 'val_3',
key_4: 'val_4',
key_5: 'val_5',
key_6: 'val_6',
key_7: 'val_7',
key_8: 'val_8',
key_9: 'val_9'
}}
/>
`);
});
it('long arrays', () => {
const arr = range(20).map((i) => `item ${i}`);
expect(renderJsx(<div data-val={arr} />, {})).toMatchInlineSnapshot(`
<div
data-val={[
'item 0',
'item 1',
'item 2',
'item 3',
'item 4',
'item 5',
'item 6',
'item 7',
'item 8',
'item 9',
'item 10',
'item 11',
'item 12',
'item 13',
'item 14',
'item 15',
'item 16',
'item 17',
'item 18',
'item 19'
]}
/>
`);
});
});
// @ts-ignore
const makeContext = (name: string, parameters: any, args: any): StoryContext => ({
id: `jsx-test--${name}`,
kind: 'js-text',
name,
parameters,
args,
});
describe('jsxDecorator', () => {
let mockChannel: { on: jest.Mock; emit?: jest.Mock };
beforeEach(() => {
mockedAddons.getChannel.mockReset();
mockChannel = { on: jest.fn(), emit: jest.fn() };
mockedAddons.getChannel.mockReturnValue(mockChannel as any);
});
it('should render dynamically for args stories', () => {
const storyFn = (args: any) => <div>args story</div>;
const context = makeContext('args', { __isArgsStory: true }, {});
jsxDecorator(storyFn, context);
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'jsx-test--args',
'<div>\n args story\n</div>'
);
});
it('should skip dynamic rendering for no-args stories', () => {
const storyFn = () => <div>classic story</div>;
const context = makeContext('classic', {}, {});
jsxDecorator(storyFn, context);
expect(mockChannel.emit).not.toHaveBeenCalled();
});
// This is deprecated, but still test it
it('allows the snippet output to be modified by onBeforeRender', () => {
const storyFn = (args: any) => <div>args story</div>;
const onBeforeRender = (dom: string) => `<p>${dom}</p>`;
const jsx = { onBeforeRender };
const context = makeContext('args', { __isArgsStory: true, jsx }, {});
jsxDecorator(storyFn, context);
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'jsx-test--args',
'<p><div>\n args story\n</div></p>'
);
});
it('allows the snippet output to be modified by transformSource', () => {
const storyFn = (args: any) => <div>args story</div>;
const transformSource = (dom: string) => `<p>${dom}</p>`;
const jsx = { transformSource };
const context = makeContext('args', { __isArgsStory: true, jsx }, {});
jsxDecorator(storyFn, context);
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'jsx-test--args',
'<p><div>\n args story\n</div></p>'
);
});
it('provides the story context to transformSource', () => {
const storyFn = (args: any) => <div>args story</div>;
const transformSource = jest.fn();
const jsx = { transformSource };
const context = makeContext('args', { __isArgsStory: true, jsx }, {});
jsxDecorator(storyFn, context);
expect(transformSource).toHaveBeenCalledWith('<div>\n args story\n</div>', context);
});
});

View File

@ -0,0 +1,165 @@
import React from 'react';
import reactElementToJSXString, { Options } from 'react-element-to-jsx-string';
import dedent from 'ts-dedent';
import deprecate from 'util-deprecate';
import { addons, StoryContext } from '@storybook/addons';
import { logger } from '@storybook/client-logger';
import { SourceType, SNIPPET_RENDERED } from '../../shared';
type JSXOptions = Options & {
/** How many wrappers to skip when rendering the jsx */
skip?: number;
/** Whether to show the function in the jsx tab */
showFunctions?: boolean;
/** Whether to format HTML or Vue markup */
enableBeautify?: boolean;
/** Override the display name used for a component */
displayName?: string | Options['displayName'];
/** Deprecated: A function ran after the story is rendered */
onBeforeRender?(dom: string): string;
/** A function ran after a story is rendered (prefer this over `onBeforeRender`) */
transformSource?(dom: string, context?: StoryContext): string;
};
/** Run the user supplied onBeforeRender function if it exists */
const applyBeforeRender = (domString: string, options: JSXOptions) => {
if (typeof options.onBeforeRender !== 'function') {
return domString;
}
const deprecatedOnBeforeRender = deprecate(
options.onBeforeRender,
dedent`
StoryFn.parameters.jsx.onBeforeRender was deprecated.
Prefer StoryFn.parameters.jsx.transformSource instead.
See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-onbeforerender for details.
`
);
return deprecatedOnBeforeRender(domString);
};
/** Run the user supplied transformSource function if it exists */
const applyTransformSource = (domString: string, options: JSXOptions, context?: StoryContext) => {
if (typeof options.transformSource !== 'function') {
return domString;
}
return options.transformSource(domString, context);
};
/** Apply the users parameters and render the jsx for a story */
export const renderJsx = (code: React.ReactElement, options: JSXOptions) => {
if (typeof code === 'undefined') {
logger.warn('Too many skip or undefined component');
return null;
}
let renderedJSX = code;
const Type = renderedJSX.type;
for (let i = 0; i < options.skip; i += 1) {
if (typeof renderedJSX === 'undefined') {
logger.warn('Cannot skip undefined element');
return null;
}
if (React.Children.count(renderedJSX) > 1) {
logger.warn('Trying to skip an array of elements');
return null;
}
if (typeof renderedJSX.props.children === 'undefined') {
logger.warn('Not enough children to skip elements.');
if (typeof Type === 'function' && Type.name === '') {
renderedJSX = <Type {...renderedJSX.props} />;
}
} else if (typeof renderedJSX.props.children === 'function') {
renderedJSX = renderedJSX.props.children();
} else {
renderedJSX = renderedJSX.props.children;
}
}
const displayNameDefaults =
typeof options.displayName === 'string'
? { showFunctions: true, displayName: () => options.displayName }
: {};
const filterDefaults = {
filterProps: (value: any, key: string): boolean => value !== undefined,
};
const opts = {
...displayNameDefaults,
...filterDefaults,
...options,
};
const result = React.Children.map(code, (c) => {
// @ts-ignore FIXME: workaround react-element-to-jsx-string
const child = typeof c === 'number' ? c.toString() : c;
let string = applyBeforeRender(reactElementToJSXString(child, opts as Options), options);
const matches = string.match(/\S+=\\"([^"]*)\\"/g);
if (matches) {
matches.forEach((match) => {
string = string.replace(match, match.replace(/&quot;/g, "'"));
});
}
return string;
}).join('\n');
return result.replace(/function\s+noRefCheck\(\)\s+\{\}/, '() => {}');
};
const defaultOpts = {
skip: 0,
showFunctions: false,
enableBeautify: true,
};
export const skipJsxRender = (context: StoryContext) => {
const sourceParams = context?.parameters.docs?.source;
const isArgsStory = context?.parameters.__isArgsStory;
// always render if the user forces it
if (sourceParams?.type === SourceType.DYNAMIC) {
return false;
}
// never render if the user is forcing the block to render code, or
// if the user provides code, or if it's not an args story.
return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE;
};
export const jsxDecorator = (storyFn: any, context: StoryContext) => {
const story = storyFn();
// We only need to render JSX if the source block is actually going to
// consume it. Otherwise it's just slowing us down.
if (skipJsxRender(context)) {
return story;
}
const channel = addons.getChannel();
const options = {
...defaultOpts,
...(context?.parameters.jsx || {}),
} as Required<JSXOptions>;
let jsx = '';
const rendered = renderJsx(story, options);
if (rendered) {
jsx = applyTransformSource(rendered, options, context);
}
channel.emit(SNIPPET_RENDERED, (context || {}).id, jsx);
return story;
};

View File

@ -45,7 +45,7 @@ function generateReactObject(rawDefaultProp: any) {
const { type } = rawDefaultProp;
const { displayName } = type;
const jsx = reactElementToJSXString(rawDefaultProp);
const jsx = reactElementToJSXString(rawDefaultProp, {});
if (displayName != null) {
const prettyIdentifier = getPrettyElementIdentifier(displayName);

View File

@ -1,18 +1,17 @@
import React, { useState } from 'react';
import mapValues from 'lodash/mapValues';
import { storiesOf } from '@storybook/react';
import { storiesOf, StoryContext } from '@storybook/react';
import { ArgsTable } from '@storybook/components';
import { Args } from '@storybook/api';
import { combineParameters } from '@storybook/client-api';
import { inferControls } from '@storybook/client-api';
import { extractArgTypes } from './extractArgTypes';
import { inferControls } from '../common/inferControls';
import { Component } from '../../blocks';
const argsTableProps = (component: Component) => {
const argTypes = extractArgTypes(component);
const controls = inferControls(argTypes);
const rows = combineParameters(argTypes, controls);
const parameters = { __isArgsStory: true, argTypes };
const rows = inferControls(({ parameters } as unknown) as StoryContext);
return { rows };
};
@ -55,12 +54,13 @@ const typescriptFixtures = [
'scalars',
'tuples',
'unions',
'optionals',
];
const typescriptStories = storiesOf('ArgTypes/TypeScript', module);
typescriptFixtures.forEach((fixture) => {
// eslint-disable-next-line import/no-dynamic-require, global-require, no-shadow
const { Component } = require(`../../lib/sbtypes/__testfixtures__/typescript/${fixture}`);
const { Component } = require(`../../lib/convert/__testfixtures__/typescript/${fixture}`);
typescriptStories.add(fixture, () => <ArgsStory component={Component} />);
});
@ -69,7 +69,7 @@ const proptypesFixtures = ['arrays', 'enums', 'misc', 'objects', 'react', 'scala
const proptypesStories = storiesOf('ArgTypes/PropTypes', module);
proptypesFixtures.forEach((fixture) => {
// eslint-disable-next-line import/no-dynamic-require, global-require, no-shadow
const { Component } = require(`../../lib/sbtypes/__testfixtures__/proptypes/${fixture}`);
const { Component } = require(`../../lib/convert/__testfixtures__/proptypes/${fixture}`);
proptypesStories.add(fixture, () => <ArgsStory component={Component} />);
});

View File

@ -0,0 +1,19 @@
<script>
import { onMount } from 'svelte';
export let component;
export let props;
let child;
const hash = `svelte mounter ${Math.floor(Math.random() * 100)}`;
onMount(() => {
child = new component({
target: document.getElementById(hash),
props,
});
});
</script>
<svelte:options accessors={true} />
<div id={hash} />

View File

@ -0,0 +1,12 @@
import { extractArgTypes } from './extractArgTypes';
import { extractComponentDescription } from '../../lib/docgen';
import { prepareForInline } from './prepareForInline';
export const parameters = {
docs: {
inlineStories: true,
prepareForInline,
extractArgTypes,
extractComponentDescription,
},
};

View File

@ -0,0 +1,94 @@
import svelteDoc from 'sveltedoc-parser';
import * as fs from 'fs';
import { createArgTypes } from './extractArgTypes';
const content = fs.readFileSync(`${__dirname}/sample/MockButton.svelte`, 'utf-8');
describe('Extracting Arguments', () => {
it('should be svelte', () => {
expect(content).toMatchInlineSnapshot(`
<script>
import { createEventDispatcher, afterUpdate } from 'svelte';
export let text = '';
export let rounded = true;
const dispatch = createEventDispatcher();
function onClick(event) {
rounded = !rounded;
dispatch('click', event);
}
afterUpdate(() => {
dispatch('afterUpdate');
});
</script>
<style>
.rounded {
border-radius: 35px;
}
.button {
border: 3px solid;
padding: 10px 20px;
background-color: white;
outline: none;
}
</style>
<svelte:options accessors="{true}">
</svelte:options>
<button class="button"
class:rounded
on:click="{onClick}"
>
<strong>
{rounded ? 'Round' : 'Square'} corners
</strong>
<br>
{text}
<slot>
</slot>
</button>
`);
});
it('should generate ArgTypes', async () => {
const doc = await svelteDoc.parse({ fileContent: content, version: 3 });
const results = createArgTypes(doc);
expect(results).toMatchInlineSnapshot(`
Object {
"rounded": Object {
"control": Object {
"type": "boolean",
},
"defaultValue": true,
"description": null,
"name": "rounded",
"table": Object {
"defaultValue": Object {
"summary": true,
},
},
"type": Object {},
},
"text": Object {
"control": Object {
"type": "text",
},
"defaultValue": "",
"description": null,
"name": "text",
"table": Object {
"defaultValue": Object {
"summary": "",
},
},
"type": Object {},
},
}
`);
});
});

View File

@ -0,0 +1,83 @@
import { ArgTypes } from '@storybook/api';
import { ArgTypesExtractor } from '../../lib/docgen';
type ComponentWithDocgen = {
__docgen: Docgen;
};
type Docgen = {
components: [];
computed: [];
data: [
{
defaultValue: any;
description: string;
keywords: [];
kind: string;
name: string;
readonly: boolean;
static: boolean;
type: { kind: string; text: string; type: string };
visibility: string;
}
];
description: null;
events: [];
keywords: [];
methods: [];
name: string;
refs: [];
slots: [];
version: number;
};
export const extractArgTypes: ArgTypesExtractor = (component) => {
// eslint-disable-next-line new-cap
const comp: ComponentWithDocgen = new component({ props: {} });
// eslint-disable-next-line no-underscore-dangle
const docs = comp.__docgen;
const results = createArgTypes(docs);
return results;
};
export const createArgTypes = (docs: Docgen) => {
const results: ArgTypes = {};
docs.data.forEach((item) => {
results[item.name] = {
control: { type: parseType(item.type.type) },
name: item.name,
description: item.description,
type: {},
defaultValue: item.defaultValue,
table: {
defaultValue: {
summary: item.defaultValue,
},
},
};
});
return results;
};
/**
* Function to convert the type from sveltedoc-parser to a storybook type
* @param typeName
* @returns string
*/
const parseType = (typeName: string) => {
switch (typeName) {
case 'string':
return 'text';
case 'enum':
return 'radio';
case 'any':
return 'object';
default:
return typeName;
}
};

View File

@ -0,0 +1,26 @@
import { StoryFn, StoryContext } from '@storybook/addons';
import React from 'react';
// @ts-ignore
import HOC from './HOC.svelte';
export const prepareForInline = (storyFn: StoryFn, context: StoryContext) => {
// @ts-ignore
const story: { Component: any; props: any } = storyFn();
const el = React.useRef(null);
React.useEffect(() => {
const root = new HOC({
target: el.current,
props: {
component: story.Component,
context,
props: story.props,
slot: story.Component,
},
});
return () => root.$destroy();
});
return React.createElement('div', { ref: el });
};

View File

@ -0,0 +1,13 @@
import path from 'path';
import { Configuration } from 'webpack';
export function webpackFinal(webpackConfig: Configuration, options: any = {}) {
webpackConfig.module.rules.push({
test: /\.svelte$/,
loader: path.resolve(`${__dirname}/svelte-docgen-loader`),
enforce: 'pre',
});
return webpackConfig;
}

View File

@ -0,0 +1,38 @@
<script>
import { createEventDispatcher, afterUpdate } from 'svelte';
export let text = '';
export let rounded = true;
const dispatch = createEventDispatcher();
function onClick(event) {
rounded = !rounded;
dispatch('click', event);
}
afterUpdate(() => {
dispatch('afterUpdate');
});
</script>
<style>
.rounded {
border-radius: 35px;
}
.button {
border: 3px solid;
padding: 10px 20px;
background-color: white;
outline: none;
}
</style>
<svelte:options accessors={true} />
<button class="button" class:rounded on:click={onClick}>
<strong>{rounded ? 'Round' : 'Square'} corners</strong>
<br />
{text}
<slot />
</button>

View File

@ -0,0 +1,38 @@
import svelteDoc from 'sveltedoc-parser';
import * as path from 'path';
/**
* webpack loader for sveltedoc-parser
* @param source raw svelte component
*/
export default async function svelteDocgen(source: string) {
// get filename for source content
// eslint-disable-next-line no-underscore-dangle
const file = path.basename(this._module.resource);
// set SvelteDoc options
const options = {
fileContent: source,
version: 3,
};
let docgen = '';
try {
const componentDoc = await svelteDoc.parse(options);
// populate filename in docgen
componentDoc.name = path.basename(file);
docgen = `
export const __docgen = ${JSON.stringify(componentDoc)};
`;
} catch (error) {
console.error(error);
}
// inject __docgen prop in svelte component
const output = source.replace('</script>', `${docgen}</script>`);
return output;
}

View File

@ -0,0 +1,12 @@
import { extractArgTypes } from './extractArgTypes';
import { extractComponentDescription } from '../../lib/docgen';
import { prepareForInline } from './prepareForInline';
export const parameters = {
docs: {
inlineStories: true,
prepareForInline,
extractArgTypes,
extractComponentDescription,
},
};

View File

@ -1,18 +0,0 @@
import React from 'react';
import toReact from '@egoist/vue-to-react';
import { StoryFn } from '@storybook/addons';
import { addParameters } from '@storybook/client-api';
import { extractArgTypes } from './extractArgTypes';
import { extractComponentDescription } from '../../lib/docgen';
addParameters({
docs: {
inlineStories: true,
prepareForInline: (storyFn: StoryFn) => {
const Story = toReact(storyFn());
return <Story />;
},
extractArgTypes,
extractComponentDescription,
},
});

View File

@ -1,7 +1,7 @@
import { ArgTypes } from '@storybook/api';
import { ArgTypesExtractor, hasDocgen, extractComponentProps } from '../../lib/docgen';
import { convert } from '../../lib/sbtypes';
import { trimQuotes } from '../../lib/sbtypes/utils';
import { convert } from '../../lib/convert';
import { trimQuotes } from '../../lib/convert/utils';
const SECTIONS = ['props', 'events', 'slots'];
@ -15,17 +15,24 @@ export const extractArgTypes: ArgTypesExtractor = (component) => {
SECTIONS.forEach((section) => {
const props = extractComponentProps(component, section);
props.forEach(({ propDef, docgenInfo, jsDocTags }) => {
const { name, type, description, defaultValue, required } = propDef;
const { name, type, description, defaultValue: defaultSummary, required } = propDef;
const sbType = section === 'props' ? convert(docgenInfo) : { name: 'void' };
let defaultValue = defaultSummary && (defaultSummary.detail || defaultSummary.summary);
try {
// eslint-disable-next-line no-eval
defaultValue = eval(defaultValue);
// eslint-disable-next-line no-empty
} catch {}
results[name] = {
name,
description,
type: { required, ...sbType },
defaultValue: defaultValue && trim(defaultValue.detail || defaultValue.summary),
defaultValue,
table: {
type,
jsDocTags,
defaultValue,
defaultValue: defaultSummary,
category: section,
},
};

View File

@ -0,0 +1,36 @@
/* eslint-disable react/no-this-in-sfc */
import React from 'react';
import Vue from 'vue';
import { StoryFn, StoryContext } from '@storybook/addons';
// Inspired by https://github.com/egoist/vue-to-react,
// modified to store args as props in the root store
// FIXME get this from @storybook/vue
const COMPONENT = 'STORYBOOK_COMPONENT';
const VALUES = 'STORYBOOK_VALUES';
export const prepareForInline = (storyFn: StoryFn, { args }: StoryContext) => {
const component = storyFn();
const el = React.useRef(null);
// FIXME: This recreates the Vue instance every time, which should be optimized
React.useEffect(() => {
const root = new Vue({
el: el.current,
data() {
return {
[COMPONENT]: component,
[VALUES]: args,
};
},
render(h) {
const children = this[COMPONENT] ? [h(this[COMPONENT])] : undefined;
return h('div', { attrs: { id: 'root' } }, children);
},
});
return () => root.$destroy();
});
return React.createElement('div', null, React.createElement('div', { ref: el }));
};

View File

@ -1,7 +1,7 @@
export function webpackFinal(webpackConfig: any = {}, options: any = {}) {
webpackConfig.module.rules.push({
test: /\.vue$/,
loader: 'vue-docgen-loader',
loader: require.resolve('vue-docgen-loader', { paths: [require.resolve('@storybook/vue')] }),
enforce: 'post',
options: {
docgenOptions: {

View File

@ -1,11 +1,10 @@
/* global window */
/* eslint-disable import/no-extraneous-dependencies */
import { addParameters } from '@storybook/client-api';
import React from 'react';
import { render } from 'lit-html';
import { extractArgTypes, extractComponentDescription } from './custom-elements';
addParameters({
export const parameters = {
docs: {
extractArgTypes,
extractComponentDescription,
@ -25,8 +24,7 @@ addParameters({
return React.createElement('div', { ref: this.wrapperRef });
}
}
return React.createElement(Story);
},
},
});
};

View File

@ -1,6 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import { getCustomElements, isValidComponent, isValidMetaData } from '@storybook/web-components';
import { ArgTypes } from '@storybook/api';
import { logger } from '@storybook/client-logger';
interface TagItem {
name: string;
@ -16,6 +17,7 @@ interface Tag {
attributes?: TagItem[];
properties?: TagItem[];
events?: TagItem[];
methods?: TagItem[];
slots?: TagItem[];
cssProperties?: TagItem[];
cssParts?: TagItem[];
@ -55,40 +57,39 @@ function mapData(data: TagItem[], category: string) {
);
}
function isEmpty(obj: object) {
return Object.entries(obj).length === 0 && obj.constructor === Object;
}
export const extractArgTypesFromElements = (tagName: string, customElements: CustomElements) => {
const getMetaData = (tagName: string, customElements: CustomElements) => {
if (!isValidComponent(tagName) || !isValidMetaData(customElements)) {
return null;
}
const metaData = customElements.tags.find(
(tag) => tag.name.toUpperCase() === tagName.toUpperCase()
);
const argTypes = {
...mapData(metaData.attributes, 'attributes'),
...mapData(metaData.properties, 'properties'),
...mapData(metaData.events, 'events'),
...mapData(metaData.slots, 'slots'),
...mapData(metaData.cssProperties, 'css custom properties'),
...mapData(metaData.cssParts, 'css shadow parts'),
};
return argTypes;
if (!metaData) {
logger.warn(`Component not found in custom-elements.json: ${tagName}`);
}
return metaData;
};
export const extractArgTypesFromElements = (tagName: string, customElements: CustomElements) => {
const metaData = getMetaData(tagName, customElements);
return (
metaData && {
...mapData(metaData.attributes, 'attributes'),
...mapData(metaData.properties, 'properties'),
...mapData(metaData.events, 'events'),
...mapData(metaData.methods, 'methods'),
...mapData(metaData.slots, 'slots'),
...mapData(metaData.cssProperties, 'css custom properties'),
...mapData(metaData.cssParts, 'css shadow parts'),
}
);
};
export const extractArgTypes = (tagName: string) => {
const customElements: CustomElements = getCustomElements();
return extractArgTypesFromElements(tagName, customElements);
return extractArgTypesFromElements(tagName, getCustomElements());
};
export const extractComponentDescription = (tagName: string) => {
const customElements: CustomElements = getCustomElements();
if (!isValidComponent(tagName) || !isValidMetaData(customElements)) {
return null;
}
const metaData = customElements.tags.find(
(tag) => tag.name.toUpperCase() === tagName.toUpperCase()
);
const metaData = getMetaData(tagName, getCustomElements());
return metaData && metaData.description;
};

Some files were not shown because too many files have changed in this diff Show More