Merge branch 'next' into pr/15822

This commit is contained in:
Michael Shilman 2022-02-07 19:12:39 +08:00
commit 3a3561e3c6
2074 changed files with 80988 additions and 31449 deletions

View File

@ -6,7 +6,7 @@ const withTests = {
],
],
plugins: [
'babel-plugin-require-context-hook',
'@storybook/babel-plugin-require-context-hook',
'babel-plugin-dynamic-import-node',
'@babel/plugin-transform-runtime',
],
@ -24,6 +24,7 @@ module.exports = {
ignore: [
'./lib/codemod/src/transforms/__testfixtures__',
'./lib/postinstall/src/__testfixtures__',
'**/typings.d.ts',
],
presets: [
[
@ -49,6 +50,7 @@ module.exports = {
],
['@babel/plugin-proposal-class-properties', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
@ -85,6 +87,7 @@ module.exports = {
['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
['@babel/plugin-proposal-class-properties', { loose: true }],
'babel-plugin-macros',
['emotion', { sourceMap: true, autoLabel: true }],
@ -128,6 +131,7 @@ module.exports = {
'@babel/plugin-transform-shorthand-properties',
'@babel/plugin-transform-block-scoping',
'@babel/plugin-transform-destructuring',
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-export-default-from',

View File

@ -27,7 +27,7 @@ executors:
environment:
NODE_OPTIONS: --max_old_space_size=3076
resource_class: <<parameters.class>>
sb_cypress_6_node_12:
sb_cypress_8_node_14:
parameters:
class:
description: The Resource class
@ -37,8 +37,8 @@ executors:
working_directory: /tmp/storybook
docker:
# ⚠️ The Cypress docker image is based on Node.js one so be careful when updating it because it can also
# cause an upgrade of the Node.
- image: cypress/included:6.8.0
# cause an upgrade of Node.js version too. Cypress 8.5 image is based on Node.js 14
- image: cypress/included:8.7.0
environment:
NODE_OPTIONS: --max_old_space_size=3076
resource_class: <<parameters.class>>
@ -145,7 +145,7 @@ jobs:
- run:
name: examples
command: |
yarn build-storybooks
yarn build-storybooks --all
- persist_to_workspace:
root: .
paths:
@ -169,7 +169,7 @@ jobs:
e2e-tests-extended:
executor:
class: medium
name: sb_cypress_6_node_12
name: sb_cypress_8_node_14
parallelism: 4
steps:
- when:
@ -195,15 +195,15 @@ jobs:
command: yarn wait-on http://localhost:6000
- run:
name: Run E2E tests
command: yarn test:e2e-framework --clean --all --skip angular11 --skip angular --skip vue3 --skip web_components_typescript --skip cra
command: yarn test:e2e-framework --clean --all --skip angular11 --skip angular --skip angular12 --skip vue3 --skip web_components_typescript --skip cra
no_output_timeout: 5m
- store_artifacts:
path: /tmp/cypress-record
destination: cypress
e2e-tests-core:
executor:
class: medium
name: sb_cypress_6_node_12
class: large
name: sb_cypress_8_node_14
parallelism: 2
steps:
- git-shallow-clone/checkout_advanced:
@ -221,7 +221,7 @@ jobs:
name: Run E2E tests
# Do not test CRA here because it's done in PnP part
# TODO: Remove `web_components_typescript` as soon as Lit 2 stable is released
command: yarn test:e2e-framework vue3 angular angular11 web_components_typescript web_components_lit2
command: yarn test:e2e-framework vue3 angular130 angular13 angular12 angular11 web_components_typescript web_components_lit2
no_output_timeout: 5m
- store_artifacts:
path: /tmp/cypress-record
@ -229,7 +229,7 @@ jobs:
cra-bench:
executor:
class: medium
name: sb_cypress_6_node_12
name: sb_cypress_8_node_14
working_directory: /tmp/storybook
steps:
- git-shallow-clone/checkout_advanced:
@ -249,11 +249,11 @@ jobs:
cd ..
npx create-react-app cra-bench
cd cra-bench
npx @storybook/bench 'npx sb init' --label cra --extra-flags "--modern"
npx @storybook/bench@latest 'npx sb init' --label cra --extra-flags "--modern"
e2e-tests-pnp:
executor:
class: medium
name: sb_cypress_6_node_12
name: sb_cypress_8_node_14
working_directory: /tmp/storybook
steps:
- git-shallow-clone/checkout_advanced:
@ -276,7 +276,7 @@ jobs:
e2e-tests-examples:
executor:
class: small
name: sb_cypress_6_node_12
name: sb_cypress_8_node_14
steps:
- git-shallow-clone/checkout_advanced:
clone_options: '--depth 1 --verbose'
@ -353,17 +353,6 @@ jobs:
command: |
cd examples/cra-react15
yarn storybook --smoke-test --quiet
frontpage:
executor: sb_node_12_browsers
steps:
- git-shallow-clone/checkout_advanced:
clone_options: '--depth 1 --verbose'
- run:
name: Install dependencies
command: yarn install --immutable
- run:
name: Trigger build
command: ./scripts/build-frontpage.js
lint:
executor:
class: small
@ -446,6 +435,3 @@ workflows:
- cra-bench:
requires:
- publish
deploy:
jobs:
- frontpage

View File

@ -11,8 +11,8 @@ lib/manager-webpack4/prebuilt
lib/manager-webpack5/prebuilt
lib/core-server/prebuilt
lib/codemod/src/transforms/__testfixtures__
lib/components/src/controls/react-editable-json-tree
scripts/storage
scripts/repros-generator
*.bundle.js
*.js.map
*.d.ts

View File

@ -1,10 +1,26 @@
module.exports = {
root: true,
extends: ['@storybook/eslint-config-storybook'],
extends: ['@storybook/eslint-config-storybook', 'plugin:storybook/recommended'],
rules: {
'@typescript-eslint/ban-ts-comment': 'warn',
'jest/no-standalone-expect': [
'error',
{ additionalTestBlockFunctions: ['it.skipWindows', 'it.onWindows'] },
],
},
overrides: [
{
// this package uses pre-bundling, dependencies will be bundled, and will be in devDepenencies
files: [
'**/lib/theming/**/*',
'**/lib/router/**/*',
'**/lib/ui/**/*',
'**/lib/components/**/*',
],
rules: {
'import/no-extraneous-dependencies': ['error', { bundledDependencies: false }],
},
},
{
files: [
'**/__tests__/**',
@ -42,6 +58,16 @@ module.exports = {
'react/prop-types': 'off', // we should use types
'react/forbid-prop-types': 'off', // we should use types
'no-dupe-class-members': 'off', // this is called overloads in typescript
'react/no-unused-prop-types': 'off', // we should use types
'react/default-props-match-prop-types': 'off', // we should use types
'import/no-named-as-default': 'warn',
'import/no-named-as-default-member': 'warn',
'react/destructuring-assignment': 'warn',
// This warns about importing interfaces and types in a normal import, it's arguably better to import with the `type` prefix separate from the runtime imports,
// I leave this as a warning right now because we haven't really decided yet, and the codebase is riddled with errors if I set to 'error'.
// It IS set to 'error' for JS files.
'import/named': 'warn',
},
},
{

View File

@ -4,9 +4,9 @@ Issue:
## How to test
- Is this testable with Jest or Chromatic screenshots?
- Does this need a new example in the kitchen sink apps?
- Does this need an update to the documentation?
- [ ] Is this testable with Jest or Chromatic screenshots?
- [ ] Does this need a new example in the kitchen sink apps?
- [ ] Does this need an update to the documentation?
If your answer is yes to any of these, please make sure to include it in your PR.

1
.github/stale.yml vendored
View File

@ -4,6 +4,7 @@ daysUntilStale: 21
daysUntilClose: 30
# Issues with these labels will never be considered stale
exemptLabels:
- linear
- todo
- ready
- 'in progress'

28
.github/workflows/generate-repros.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Generate And Push Repros
on:
schedule:
- cron: '2 2 */1 * *'
workflow_dispatch:
# To remove when the branch will be merged
push:
branches:
- generate-repros
jobs:
update:
runs-on: ubuntu-latest
env:
YARN_ENABLE_IMMUTABLE_INSTALLS: false
steps:
- uses: actions/checkout@v2
- name: Setup git user
run: |
git config --global user.name "Storybook Bot"
git config --global user.email "bot@storybook.js.org"
- name: Install dependencies
run: yarn install
- name: Generate repros with Latest Storybook CLI
run: yarn generate-repros --remote=https://storybook-bot:${{ secrets.PAT_STORYBOOK_BOT}}@github.com/storybookjs/repro-templates.git --push --force-push
- name: Generate repros with Next Storybook CLI
run: yarn generate-repros --next --remote=https://storybook-bot:${{ secrets.PAT_STORYBOOK_BOT}}@github.com/storybookjs/repro-templates.git --push --force-push

View File

@ -0,0 +1,99 @@
name: Handle Release Branches
on:
push:
jobs:
branch-checks:
runs-on: ubuntu-latest
steps:
- id: get-branch
run: |
BRANCH=($(echo ${{ github.ref }} | sed -E 's/refs\/heads\///'))
echo "branch=$BRANCH" >> $GITHUB_ENV
outputs:
branch: ${{ env.branch }}
is-latest-branch: ${{ env.branch == 'main' }}
is-next-branch: ${{ env.branch == 'next' }}
is-release-branch: ${{ startsWith(env.branch, 'release-') }}
is-actionable-branch: ${{ env.branch == 'main' || env.branch == 'next' || startsWith(env.branch, 'release-') }}
handle-latest:
needs: branch-checks
if: ${{ needs.branch-checks.outputs.is-latest-branch == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: curl -X POST "https://api.netlify.com/build_hooks/${{ secrets.FRONTPAGE_HOOK }}"
get-next-release-branch:
needs: branch-checks
if: ${{ needs.branch-checks.outputs.is-next-branch == 'true' || needs.branch-checks.outputs.is-release-branch == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: next
path: next
- id: next-version
uses: notiz-dev/github-action-json-property@release
with:
path: ${{ github.workspace }}/next/package.json
prop_path: version
- run: |
NEXT_RELEASE_BRANCH=($(echo ${{ steps.next-version.outputs.prop }} | sed -E 's/([0-9]+)\.([0-9]+).*/release-\1-\2/'))
echo "next-release-branch=$NEXT_RELEASE_BRANCH" >> $GITHUB_ENV
outputs:
branch: ${{ env.next-release-branch }}
create-next-release-branch:
needs: [branch-checks, get-next-release-branch]
if: ${{ needs.branch-checks.outputs.is-next-branch == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- run: |
set +e
REMOTE_BRANCH=$(git branch -r | grep origin/${{ needs.get-next-release-branch.outputs.branch }})
if [[ ! -z $REMOTE_BRANCH ]]; then git push origin --delete ${{ needs.get-next-release-branch.outputs.branch }}; fi
echo 'Pushing branch ${{ needs.get-next-release-branch.outputs.branch }}...'
git push -f origin ${{ needs.branch-checks.outputs.branch }}:${{ needs.get-next-release-branch.outputs.branch }}
outputs:
branch: ${{ needs.get-next-release-branch.outputs.branch }}
next-release-branch-check:
if: ${{ always() }}
needs: [branch-checks, get-next-release-branch]
runs-on: ubuntu-latest
steps:
- run: |
IS_NEXT_RELEASE_BRANCH=${{ needs.branch-checks.outputs.branch == needs.get-next-release-branch.outputs.branch }}
echo "is-next-release-branch=$IS_NEXT_RELEASE_BRANCH" >> $GITHUB_ENV
- if: ${{ env.is-next-release-branch == 'true' }}
run: echo "relevant-base-branch=next" >> $GITHUB_ENV
- if: ${{ env.is-next-release-branch == 'true' }}
run: |
echo 'WARNING: Do not push directly to the `${{ needs.branch-checks.outputs.branch }}` branch. This branch is created and force-pushed over after pushing to the `${{ env.relevant-base-branch }}` branch and the changes you just pushed will be lost.'
exit 1
outputs:
check: ${{ env.is-next-release-branch }}
request-create-frontpage-branch:
if: ${{ always() }}
needs: [branch-checks, next-release-branch-check, create-next-release-branch]
runs-on: ubuntu-latest
steps:
- if: ${{ needs.branch-checks.outputs.is-actionable-branch == 'true' && needs.branch-checks.outputs.is-latest-branch == 'false' && needs.next-release-branch-check.outputs.check == 'false' }}
run: |
curl -X POST https://api.github.com/repos/storybookjs/frontpage/dispatches \
-H 'Accept: application/vnd.github.v3+json' \
-u ${{ secrets.FRONTPAGE_ACCESS_TOKEN }} \
--data '{"event_type": "request-create-frontpage-branch", "client_payload": { "branch": "${{ needs.create-next-release-branch.outputs.branch || needs.branch-checks.outputs.branch }}" }}'

27
.github/workflows/linear-export.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Export to linear
on:
issues:
types: [labeled]
pull_request:
types: [labeled]
jobs:
trigger:
if: github.event.label.name == 'linear'
name: Export to linear
runs-on: ubuntu-latest
steps:
# - uses: hmarr/debug-action@v2
- name: Linear action
uses: shilman/linear-action@v1
with:
ghIssueNumber: ${{ github.event.number || github.event.issue.number }}
ghRepoOwner: ${{ github.event.repository.owner.login }}
ghRepoName: ${{ github.event.repository.name }}
ghToken: ${{ secrets.LINEAR_GH_TOKEN }}
linearIssuePrefix: SB
linearLabel: Storybook
linearPRLabel: PR
linearTeam: SB
linearApiKey: ${{ secrets.LINEAR_API_KEY }}

View File

@ -1,16 +1,33 @@
name: Unit tests
on: [push]
on:
push:
branches:
- next
pull_request:
types: [opened, reopened, labeled, synchronize]
jobs:
build:
name: Core Unit Tests
runs-on: ubuntu-latest
name: Core Unit Tests node-${{ matrix.node_version }}, ${{ matrix.os }}
if: github.event_name == 'push' || github.event.label.name == 'ci:matrix'
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
node_version: [12, 14, 16]
include:
- os: macos-latest
node_version: 16
- os: windows-latest
node_version: 16
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Set node version to ${{ matrix.node_version }}
uses: actions/setup-node@v2
with:
node-version: "12.x"
node-version: ${{ matrix.node_version }}
cache: yarn
- name: install, bootstrap
run: |

3
.gitignore vendored
View File

@ -8,6 +8,7 @@ dist
.tern-port
*.DS_Store
.cache
.parcel-cache
coverage/
*.lerna_backup
build
@ -41,4 +42,4 @@ examples/angular-cli/addon-jest.testresults.json
!/**/.yarn/plugins
!/**/.yarn/sdks
!/**/.yarn/versions
/**/.pnp.*
/**/.pnp.*

View File

@ -45,7 +45,6 @@ project {
buildType(Build)
buildType(E2E)
buildType(SmokeTests)
buildType(Frontpage)
buildType(Test)
buildType(Coverage)
@ -56,7 +55,6 @@ project {
RelativeId("Build"),
RelativeId("E2E"),
RelativeId("SmokeTests"),
RelativeId("Frontpage"),
RelativeId("Test"),
RelativeId("Coverage")
)
@ -177,7 +175,7 @@ object ExamplesTemplate : Template({
rm -rf built-storybooks
mkdir -p built-storybooks
yarn build-storybooks
yarn build-storybooks --all
""".trimIndent()
dockerImage = "buildkite/puppeteer"
dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux
@ -403,34 +401,6 @@ object SmokeTests : BuildType({
}
})
object Frontpage : BuildType({
name = "Frontpage"
type = Type.DEPLOYMENT
steps {
script {
scriptContent = """
#!/bin/bash
set -e -x
yarn install --immutable
yarn bootstrap --install
node ./scripts/build-frontpage.js
""".trimIndent()
dockerImage = "node:12"
dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux
}
}
triggers {
vcs {
quietPeriodMode = VcsTrigger.QuietPeriodMode.USE_DEFAULT
triggerRules = "-:.teamcity/**"
branchFilter = "+:main"
}
}
})
object Test : BuildType({
name = "Test"

363
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs generated vendored Normal file

File diff suppressed because one or more lines are too long

768
.yarn/releases/yarn-3.1.1.cjs generated vendored Executable file

File diff suppressed because one or more lines are too long

631
.yarn/releases/yarn-sources.cjs generated vendored

File diff suppressed because one or more lines are too long

View File

@ -9,8 +9,10 @@ npmRegistryServer: "https://registry.yarnpkg.com"
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
unsafeHttpWhitelist:
- localhost
yarnPath: .yarn/releases/yarn-sources.cjs
yarnPath: .yarn/releases/yarn-3.1.1.cjs

View File

@ -1,32 +0,0 @@
## Addon / Framework Support Table
| | [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>)
## Deprecated Addons
| | [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/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

@ -133,7 +133,7 @@ This should enable auto-fix for all source files, and give linting warnings and
First make sure the repo is bootstrapped.
Then run `yarn build-storybooks`, this creates a static website from all examples.
Then run `yarn build-storybooks --all`, this creates a static website from all examples.
Then run `yarn serve-storybooks`, this will run the static site on the port cypress expects.

View File

@ -1,7 +1,35 @@
<h1>Migration</h1>
- [From version 6.4.x to 6.5.0](#from-version-64x-to-650)
- [CSF3 auto-title redundant filename](#csf3-auto-title-redundant-filename)
- [From version 6.3.x to 6.4.0](#from-version-63x-to-640)
- [Automigrate](#automigrate)
- [CRA5 upgrade](#cra5-upgrade)
- [CSF3 enabled](#csf3-enabled)
- [Optional titles](#optional-titles)
- [String literal titles](#string-literal-titles)
- [StoryObj type](#storyobj-type)
- [Story Store v7](#story-store-v7)
- [Behavioral differences](#behavioral-differences)
- [Main.js framework field](#mainjs-framework-field)
- [Using the v7 store](#using-the-v7-store)
- [v7-style story sort](#v7-style-story-sort)
- [v7 Store API changes for addon authors](#v7-store-api-changes-for-addon-authors)
- [Storyshots compatibility in the v7 store](#storyshots-compatibility-in-the-v7-store)
- [Emotion11 quasi-compatibility](#emotion11-quasi-compatibility)
- [Babel mode v7](#babel-mode-v7)
- [Loader behavior with args changes](#loader-behavior-with-args-changes)
- [6.4 Angular changes](#64-angular-changes)
- [SB Angular builder](#sb-angular-builder)
- [Angular13](#angular13)
- [Angular component parameter removed](#angular-component-parameter-removed)
- [6.4 deprecations](#64-deprecations)
- [Deprecated --static-dir CLI flag](#deprecated---static-dir-cli-flag)
- [From version 6.2.x to 6.3.0](#from-version-62x-to-630)
- [Webpack 5 manager build](#webpack-5-manager-build)
- [Webpack 5](#webpack-5)
- [Fixing hoisting issues](#fixing-hoisting-issues)
- [Webpack 5 manager build](#webpack-5-manager-build)
- [Wrong webpack version](#wrong-webpack-version)
- [Angular 12 upgrade](#angular-12-upgrade)
- [Lit support](#lit-support)
- [No longer inferring default values of args](#no-longer-inferring-default-values-of-args)
@ -162,36 +190,55 @@
- [Packages renaming](#packages-renaming)
- [Deprecated embedded addons](#deprecated-embedded-addons)
## From version 6.2.x to 6.3.0
## From version 6.4.x to 6.5.0
### Webpack 5 manager build
### CSF3 auto-title redundant filename
Storybook 6.2 introduced **experimental** webpack5 support for building user components. Storybook 6.3 also supports building the manager UI in webpack 5 to avoid strange hoisting issues.
SB 6.4 introduced experimental "auto-title", in which a story's location in the sidebar (aka `title`) can be automatically inferred from its location on disk. For example, the file `atoms/Button.stories.js` might result in the title `Atoms/Button`.
If you're upgrading from 6.2 and already using the experimental webpack5 feature, this might be a breaking change (hence the 'experimental' label) and you should try adding the manager builder:
The heuristic failed in the common scenario in which each component gets its own directory, e.g. `atoms/Button/Button.stories.js`, which would result in the redundant title `Atoms/Button/Button`. Alternatively, `atoms/Button/index.stories.js` would result in `Atoms/Button/Index`.
```shell
yarn add @storybook/manager-webpack5 --dev
# Or
npm install @storybook/manager-webpack5 --save-dev
To address this problem, 6.5 introduces a new heuristic to removes the filename if it matches the directory name (case insensitive) or `index`. So `atoms/Button/Button.stories.js` and `atoms/Button/index.stories.js` would both result in the title `Atoms/Button`.
Since CSF3 is experimental, we are introducing this technically breaking change in a minor release. If you desire the old structure, you can manually specify the title in file. For example:
```js
// atoms/Button/Button.stories.js
export default { title: 'Atoms/Button/Button' };
```
Because Storybook uses `webpack@4` as the default, it's possible for the wrong version of webpack to get hoisted by your package manager. If you receive an error that looks like you might be using the wrong version of webpack, install `webpack@5` explicitly as a dev dependency to force it to be hoisted:
## From version 6.3.x to 6.4.0
```shell
yarn add webpack@5 --dev
# Or
npm install webpack@5 --save-dev
### Automigrate
Automigrate is a new 6.4 feature that provides zero-config upgrades to your dependencies, configurations, and story files.
Each automigration analyzes your project, and if it's is applicable, propose a change alongside relevant documentation. If you accept the changes, the automigration will update your files accordingly.
For example, if you're in a webpack5 project but still use Storybook's default webpack4 builder, the automigration can detect this and propose an upgrade. If you opt-in, it will install the webpack5 builder and update your `main.js` configuration automatically.
You can run the existing suite of automigrations to see which ones apply to your project. This won't update any files unless you accept the changes:
```
npx sb@next automigrate
```
### Angular 12 upgrade
The automigration suite also runs when you create a new project (`sb init`) or when you update storybook (`sb upgrade`).
Storybook 6.3 supports Angular 12 out of the box when you install it fresh. However, if you're upgrading your project from a previous version, you'll need to do the following steps to force Storybook to use webpack 5 for building your project:
### CRA5 upgrade
Storybook 6.3 supports CRA5 out of the box when you install it fresh. However, if you're upgrading your project from a previous version, you'll need to upgrade the configuration. You can do this automatically by running:
```
npx sb@next automigrate
```
Or you can do the following steps manually to force Storybook to use webpack 5 for building your project:
```shell
yarn add @storybook/builder-webpack5@next @storybook/manager-webpack5@next --dev
yarn add @storybook/builder-webpack5 @storybook/manager-webpack5 --dev
# Or
npm install @storybook/builder-webpack5@next @storybook/manager-webpack5@next --save-dev
npm install @storybook/builder-webpack5 @storybook/manager-webpack5 --save-dev
```
Then edit your `.storybook/main.js` config:
@ -204,6 +251,398 @@ module.exports = {
};
```
### CSF3 enabled
SB6.3 introduced a feature flag, `features.previewCsfV3`, to opt-in to experimental [CSF3 syntax support](https://storybook.js.org/blog/component-story-format-3-0/). In SB6.4, CSF3 is supported regardless of `previewCsfV3`'s value. This should be a fully backwards-compatible change. The `previewCsfV3` flag has been deprecated and will be removed in SB7.0.
#### Optional titles
In SB6.3 and earlier, component titles were required in CSF default exports. Starting in 6.4, they are optional.
If you don't specify a component file, it will be inferred from the file's location on disk.
Consider a project configuration `/path/to/project/.storybook/main.js` containing:
```js
module.exports = { stories: ['../src/**/*.stories.*'] };
```
And the file `/path/to/project/src/components/Button.stories.tsx` containing the default export:
```js
import { Button } from './Button';
export default { component: Button };
// named exports...
```
The inferred title of this file will be `components/Button` based on the stories glob in the configuration file.
We will provide more documentation soon on how to configure this.
#### String literal titles
Starting in 6.4 CSF component [titles are optional](#optional-titles). However, if you do specify titles, title handing is becoming more strict in V7 and is limited to string literals.
Earlier versions of Storybook supported story titles that are dynamic Javascript expressions
```js
// ✅ string literals 6.3 OK / 7.0 OK
export default {
title: 'Components/Atoms/Button',
};
// ✅ undefined 6.3 OK / 7.0 OK
export default {
component: Button,
};
// ❌ expressions: 6.3 OK / 7.0 KO
export default {
title: foo('bar'),
};
// ❌ template literals 6.3 OK / 7.0 KO
export default {
title: `${bar}`,
};
```
#### StoryObj type
The TypeScript type for CSF3 story objects is `StoryObj`, and this will become the default in Storybook 7.0. In 6.x, the `StoryFn` type is the default, and `Story` is aliased to `StoryFn`.
If you are migrating to experimental CSF3, the following is compatible with 6.4 and requires the least amount of change to your code today:
```ts
// CSF2 function stories, current API, will break in 7.0
import type { Story } from '@storybook/<framework>';
// CSF3 object stories, will persist in 7.0
import type { StoryObj } from '@storybook/<framework>';
```
The following is compatible with 6.4 and also forward-compatible with anticipated 7.0 changes:
```ts
// CSF2 function stories, forward-compatible mode
import type { StoryFn } from '@storybook/<framework>';
// CSF3 object stories, using future 7.0 types
import type { Story } from '@storybook/<framework>/types-7-0';
```
### Story Store v7
SB6.4 introduces an opt-in feature flag, `features.storyStoreV7`, which loads stories in an "on demand" way (that is when rendered), rather than up front when the Storybook is booted. This way of operating will become the default in 7.0 and will likely be switched to opt-out in that version.
The key benefit of the on demand store is that stories are code-split automatically (in `builder-webpack4` and `builder-webpack5`), which allows for much smaller bundle sizes, faster rendering, and improved general performance via various opt-in Webpack features.
The on-demand store relies on the "story index" data structure which is generated in the server (node) via static code analysis. As such, it has the following limitations:
- Does not work with `storiesOf()`
- Does not work if you use dynamic story names or component titles.
However, the `autoTitle` feature is supported.
#### Behavioral differences
The key behavioral differences of the v7 store are:
- `SET_STORIES` is not emitted on boot up. Instead the manager loads the story index independently.
- A new event `STORY_PREPARED` is emitted when a story is rendered for the first time, which contains metadata about the story, such as `parameters`.
- All "entire" store APIs such as `extract()` need to be proceeded by an async call to `loadAllCSFFiles()` which fetches all CSF files and processes them.
#### Main.js framework field
In earlier versions of Storybook, each framework package (e.g. `@storybook/react`) provided its own `start-storybook` and `build-storybook` binaries, which automatically filled in various settings.
In 7.0, we're moving towards a model where the user specifies their framework in `main.js`.
```js
module.exports = {
// ... your existing config
framework: '@storybook/react', // OR whatever framework you're using
};
```
Each framework must export a `renderToDOM` function and `parameters.framework`. We'll be adding more documentation for framework authors in a future release.
#### Using the v7 store
To activate the v7 mode set the feature flag in your `.storybook/main.js` config:
```js
module.exports = {
// ... your existing config
framework: '@storybook/react', // OR whatever framework you're using
features: {
storyStoreV7: true,
},
};
```
NOTE: `features.storyStoreV7` implies `features.buildStoriesJson` and has the same limitations.
#### v7-style story sort
If you've written a custom `storySort` function, you'll need to rewrite it for V7.
SB6.x supports a global story function specified in `.storybook/preview.js`. It accepts two arrays which each contain:
- The story ID
- A story object that contains the name, title, etc.
- The component's parameters
- The project-level parameters
SB 7.0 streamlines the story function. It now accepts a `StoryIndexEntry` which is
an object that contains only the story's `id`, `title`, `name`, and `importPath`.
Consider the following example, before and after:
```js
// v6-style sort
function storySort(a, b) {
return a[1].kind === b[1].kind
? 0
: a[1].id.localeCompare(b[1].id, undefined, { numeric: true });
},
```
And the after version using `title` instead of `kind` and not receiving the full parameters:
```js
// v7-style sort
function storySort(a, b) {
return a.title === b.title
? 0
: a.id.localeCompare(b.id, undefined, { numeric: true });
},
```
#### v7 Store API changes for addon authors
The Story Store in v7 mode is async, so synchronous story loading APIs no longer work. In particular:
- `store.fromId()` has been replaced by `store.loadStory()`, which is async (i.e. returns a `Promise` you will need to await).
- `store.raw()/store.extract()` and friends that list all stories require a prior call to `store.cacheAllCSFFiles()` (which is async). This will load all stories, and isn't generally a good idea in an addon, as it will force the whole store to load.
#### Storyshots compatibility in the v7 store
Storyshots is not currently compatible with the v7 store. However, you can use the following workaround to opt-out of the v7 store when running storyshots; in your `main.js`:
```js
module.exports = {
features: {
storyStoreV7: !global.navigator?.userAgent?.match?.('jsdom'),
},
};
```
There are some caveats with the above approach:
- The code path in the v6 store is different to the v7 store and your mileage may vary in identical behavior. Buyer beware.
- The story sort API [changed between the stores](#v7-style-story-sort). If you are using a custom story sort function, you will need to ensure it works in both contexts (perhaps using the check `global.navigator.userAgent.match('jsdom')`).
### Emotion11 quasi-compatibility
Now that the web is moving to Emotion 11 for styling, popular libraries like MUI5 and ChakraUI are breaking with Storybook 6.3 which only supports emotion@10.
Unfortunately we're unable to upgrade Storybook to Emotion 11 without a semver major release, and we're not ready for that. So, as a workaround, we've created a feature flag which opts-out of the previous behavior of pinning the Emotion version to v10. To enable this workaround, add the following to your `.storybook/main.js` config:
```js
module.exports = {
features: {
emotionAlias: false,
},
};
```
Setting this should unlock theming for emotion11-based libraries in Storybook 6.4.
### Babel mode v7
SB6.4 introduces an opt-in feature flag, `features.babelModeV7`, that reworks the way Babel is configured in Storybook to make it more consistent with the Babel is configured in your app. This breaking change will become the default in SB 7.0, but we encourage you to migrate today.
> NOTE: CRA apps using `@storybook/preset-create-react-app` use CRA's handling, so the new flag has no effect on CRA apps.
In SB6.x and earlier, Storybook provided its own default configuration and inconsistently handled configurations from the user's babelrc file. This resulted in a final configuration that differs from your application's configuration AND is difficult to debug.
In `babelModeV7`, Storybook no longer provides its own default configuration and is primarily configured via babelrc file, with small, incremental updates from Storybook addons.
In 6.x, Storybook supported a `.storybook/babelrc` configuration option. This is no longer supported and it's up to you to reconcile this with your project babelrc.
To activate the v7 mode set the feature flag in your `.storybook/main.js` config:
```js
module.exports = {
// ... your existing config
features: {
babelModeV7: true,
},
};
```
In the new mode, Storybook expects you to provide a configuration file. If you want a configuration file that's equivalent to the 6.x default, you can run the following command in your project directory:
```sh
npx sb@next babelrc
```
This will create a `.babelrc.json` file. This file includes a bunch of babel plugins, so you may need to add new package devDependencies accordingly.
### Loader behavior with args changes
In 6.4 the behavior of loaders when arg changes occurred was tweaked so loaders do not re-run. Instead the previous value of the loader is passed to the story, irrespective of the new args.
### 6.4 Angular changes
#### SB Angular builder
Since SB6.3, Storybook for Angular supports a builder configuration in your project's `angular.json`. This provides an Angular-style configuration for running and building your Storybook. The full builder documentation will be shown in the [main documentation page](https://storybook.js.org/docs/angular) soon, but for now you can check out an example here:
- `start-storybook`: https://github.com/storybookjs/storybook/blob/next/examples/angular-cli/angular.json#L78
- `build-storybook`: https://github.com/storybookjs/storybook/blob/next/examples/angular-cli/angular.json#L86
#### Angular13
Angular 13 introduces breaking changes that require updating your Storybook configuration if you are migrating from a previous version of Angular.
Most notably, the documented way of including global styles is no longer supported by Angular13. Previously you could write the following in your `.storybook/preview.js` config:
```
import '!style-loader!css-loader!sass-loader!./styles.scss';
```
If you use Angular 13 and above, you should use the builder configuration instead:
```json
"my-default-project": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"styles": ["src/styles.css", "src/styles.scss"],
}
}
},
},
```
If you need storybook-specific styles separate from your app, you can configure the styles in the [SB Angular builder](#sb-angular-builder), which completely overrides your project's styles:
```json
"storybook": {
"builder": "@storybook/angular:start-storybook",
"options": {
"browserTarget": "my-default-project:build",
"styles": [".storybook/custom-styles.scss"],
},
}
```
#### Angular component parameter removed
In SB6.3 and earlier, the `default.component` metadata was implemented as a parameter, meaning that stories could set `parameters.component` to override the default export. This was an internal implementation that was never documented, but it was mistakenly used in some Angular examples.
If you have Angular stories of the form:
```js
export const MyStory = () => ({ ... })
SomeStory.parameters = { component: MyComponent };
```
You should rewrite them as:
```js
export const MyStory = () => ({ component: MyComponent, ... })
```
[More discussion here.](https://github.com/storybookjs/storybook/pull/16010#issuecomment-917378595)
### 6.4 deprecations
#### Deprecated --static-dir CLI flag
In 6.4 we've replaced the `--static-dir` CLI flag with the the `staticDirs` field in `.storybook/main.js`. Note that the CLI directories are relative to the current working directory, whereas the `staticDirs` are relative to the location of `main.js`.
Before:
```sh
start-storybook --static-dir ./public,./static,./foo/assets:/assets
```
After:
```js
// .storybook/main.js
module.exports = {
staticDirs: ['../public', '../static', { from: '../foo/assets', to: '/assets' }],
};
```
The `--static-dir` flag has been deprecated and will be removed in Storybook 7.0.
## From version 6.2.x to 6.3.0
### Webpack 5
Storybook 6.3 brings opt-in support for building both your project and the manager UI with webpack 5. To do so:
```shell
yarn add @storybook/builder-webpack5 @storybook/manager-webpack5 --dev
# Or
npm install @storybook/builder-webpack5 @storybook/manager-webpack5 --save-dev
```
Then edit your `.storybook/main.js` config:
```js
module.exports = {
core: {
builder: 'webpack5',
},
};
```
> NOTE: If you're using `@storybook/preset-create-react-app` make sure to update it to version 4.0.0 as well.
#### Fixing hoisting issues
##### Webpack 5 manager build
Storybook 6.2 introduced **experimental** webpack5 support for building user components. Storybook 6.3 also supports building the manager UI in webpack 5 to avoid strange hoisting issues.
If you're upgrading from 6.2 and already using the experimental webpack5 feature, this might be a breaking change (hence the 'experimental' label) and you should try adding the manager builder:
```shell
yarn add @storybook/manager-webpack5 --dev
# Or
npm install @storybook/manager-webpack5 --save-dev
```
##### Wrong webpack version
Because Storybook uses `webpack@4` as the default, it's possible for the wrong version of webpack to get hoisted by your package manager. If you receive an error that looks like you might be using the wrong version of webpack, install `webpack@5` explicitly as a dev dependency to force it to be hoisted:
```shell
yarn add webpack@5 --dev
# Or
npm install webpack@5 --save-dev
```
Alternatively or additionally you might need to add a resolution to your package.json to ensure that a consistent webpack version is provided across all of storybook packages. Replacing the {app} with the app (react, vue, etc.) that you're using:
```js
// package.json
...
resolutions: {
"@storybook/{app}/webpack": "^5"
}
...
```
### Angular 12 upgrade
Storybook 6.3 supports Angular 12 out of the box when you install it fresh. However, if you're upgrading your project from a previous version, you'll need to [follow the steps for opting-in to webpack 5](#webpack-5).
### Lit support
Storybook 6.3 introduces Lit 2 support in a non-breaking way to ease migration from `lit-html`/`lit-element` to `lit`.
@ -312,7 +751,7 @@ export const Basic = () => ({
});
```
The new convention is consistent with how other frameworks and addons work in Storybook. The old way will be supported until 7.0. For a full discussion see https://github.com/storybookjs/storybook/issues/8673.
The new convention is consistent with how other frameworks and addons work in Storybook. The old way will be supported until 7.0. For a full discussion see <https://github.com/storybookjs/storybook/issues/8673>.
#### New Angular renderer
@ -349,7 +788,7 @@ Instead of continuing to include PostCSS inside the core library, it has been mo
If you require PostCSS support, please install `@storybook/addon-postcss` in your project, add it to your list of addons inside `.storybook/main.js`, and configure a `postcss.config.js` file.
Further information is available at https://github.com/storybookjs/storybook/issues/12668 and https://github.com/storybookjs/storybook/pull/13669.
Further information is available at <https://github.com/storybookjs/storybook/issues/12668> and <https://github.com/storybookjs/storybook/pull/13669>.
If you're not using Postcss and you don't want to see the warning, you can disable it by adding the following to your `.storybook/main.js`:
@ -490,7 +929,7 @@ Starting in 6.1, `react` and `react-dom` are required peer dependencies of `@sto
Error: Cannot find module 'react-dom/package.json'
```
They were also peer dependencies in earlier versions, but due to the package structure they would be installed by Storybook if they were not required by the user's project. For more discussion: https://github.com/storybookjs/storybook/issues/13269
They were also peer dependencies in earlier versions, but due to the package structure they would be installed by Storybook if they were not required by the user's project. For more discussion: <https://github.com/storybookjs/storybook/issues/13269>
### 6.1 deprecations
@ -519,7 +958,7 @@ console.log(unboundStoryFn(context));
If you're not using loaders, `storyFn` will work as before. If you are, you'll need to use the new approach.
> NOTE: If you're using `@storybook/addon-docs`, this deprecation warning is triggered by the Docs tab in 6.1. It's safe to ignore and we will be providing a proper fix in a future release. You can track the issue at https://github.com/storybookjs/storybook/issues/13074.
> NOTE: If you're using `@storybook/addon-docs`, this deprecation warning is triggered by the Docs tab in 6.1. It's safe to ignore and we will be providing a proper fix in a future release. You can track the issue at <https://github.com/storybookjs/storybook/issues/13074>.
#### Deprecated onBeforeRender
@ -1346,20 +1785,20 @@ The description doc block on DocsPage has also been updated. To see how to confi
### React Native Async Storage
Starting from version React Native 0.59, Async Storage is deprecated in React Native itself. The new @react-native-community/async-storage module requires native installation, and we don't want to have it as a dependency for React Native Storybook.
Starting from version React Native 0.59, Async Storage is deprecated in React Native itself. The new @react-native-async-storage/async-storage module requires native installation, and we don't want to have it as a dependency for React Native Storybook.
To avoid that now you have to manually pass asyncStorage to React Native Storybook with asyncStorage prop. To notify users we are displaying a warning about it.
Solution:
- Use `require('@react-native-community/async-storage').default` for React Native v0.59 and above.
- Use `require('@react-native-async-storage/async-storage').default` for React Native v0.59 and above.
- Use `require('react-native').AsyncStorage` for React Native v0.58 or below.
- Use `null` to disable Async Storage completely.
```javascript
getStorybookUI({
...
asyncStorage: require('@react-native-community/async-storage').default || require('react-native').AsyncStorage || null
asyncStorage: require('@react-native-async-storage/async-storage').default || require('react-native').AsyncStorage || null
});
```
@ -1379,7 +1818,7 @@ Addon-docs configuration gets simpler in 5.3. In 5.2, each framework had its own
We've deprecated the ability to specify the hierarchy separators (how you control the grouping of story kinds in the sidebar). From Storybook 6.0 we will have a single separator `/`, which cannot be configured.
If you are currently using 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 all your components.
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 all your components.
```
yarn sb migrate upgrade-hierarchy-separators --glob="*.stories.js"
@ -1989,16 +2428,16 @@ The `@storybook/react-native` had built-in addons (`addon-actions` and `addon-li
### Storyshots Changes
1. `imageSnapshot` test function was extracted from `addon-storyshots`
and moved to a new package - `addon-storyshots-puppeteer` that now will
be dependant on puppeteer. [README](https://github.com/storybookjs/storybook/tree/master/addons/storyshots/storyshots-puppeteer)
2. `getSnapshotFileName` export was replaced with the `Stories2SnapsConverter`
class that now can be overridden for a custom implementation of the
snapshot-name generation. [README](https://github.com/storybookjs/storybook/tree/master/addons/storyshots/storyshots-core#stories2snapsconverter)
3. Storybook that was configured with Webpack's `require.context()` feature
will need to add a babel plugin to polyfill this functionality.
A possible plugin might be [babel-plugin-require-context-hook](https://github.com/smrq/babel-plugin-require-context-hook).
[README](https://github.com/storybookjs/storybook/tree/master/addons/storyshots/storyshots-core#configure-jest-to-work-with-webpacks-requirecontext)
1. `imageSnapshot` test function was extracted from `addon-storyshots`
and moved to a new package - `addon-storyshots-puppeteer` that now will
be dependant on puppeteer. [README](https://github.com/storybookjs/storybook/tree/master/addons/storyshots/storyshots-puppeteer)
2. `getSnapshotFileName` export was replaced with the `Stories2SnapsConverter`
class that now can be overridden for a custom implementation of the
snapshot-name generation. [README](https://github.com/storybookjs/storybook/tree/master/addons/storyshots/storyshots-core#stories2snapsconverter)
3. Storybook that was configured with Webpack's `require.context()` feature
will need to add a babel plugin to polyfill this functionality.
A possible plugin might be [babel-plugin-require-context-hook](https://github.com/smrq/babel-plugin-require-context-hook).
[README](https://github.com/storybookjs/storybook/tree/master/addons/storyshots/storyshots-core#configure-jest-to-work-with-webpacks-requirecontext)
### Webpack 4
@ -2009,9 +2448,11 @@ Storybook now uses webpack 4. If you have a [custom webpack config](https://stor
Storybook now uses Babel 7. There's a couple of cases when it can break with your app:
- If you aren't using Babel yourself, and don't have .babelrc, install following dependencies:
```
npm i -D @babel/core babel-loader@next
```
- If you're using Babel 6, make sure that you have direct dependencies on `babel-core@6` and `babel-loader@7` and that you have a `.babelrc` in your project directory.
### Create-react-app
@ -2262,11 +2703,14 @@ If you **are** using these addons, it takes two steps to migrate:
- add the addons you use to your `package.json`.
- update your code:
change `addons.js` like so:
```js
import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';
```
change `x.story.js` like so:
```js
import React from 'react';
import { storiesOf } from '@storybook/react';

View File

@ -94,21 +94,21 @@ For additional help, join us in the [Storybook Discord](https://discord.gg/story
| Framework | Demo | |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| [React](app/react) | [v6.3.x](https://storybookjs.netlify.com/official-storybook/?path=/story/*) | [![React](https://img.shields.io/npm/dm/@storybook/react.svg)](app/react) |
| [Vue](app/vue) | [v6.3.x](https://storybookjs.netlify.com/vue-kitchen-sink/) | [![Vue](https://img.shields.io/npm/dm/@storybook/vue.svg)](app/vue) |
| [Angular](app/angular) | [v6.3.x](https://storybookjs.netlify.com/angular-cli/) | [![Angular](https://img.shields.io/npm/dm/@storybook/angular.svg)](app/angular) |
| [Web components](app/web-components) | [v6.3.x](https://storybookjs.netlify.com/web-components-kitchen-sink/) | [![Svelte](https://img.shields.io/npm/dm/@storybook/web-components.svg)](app/web-components) |
| [React](app/react) | [v6.4.x](https://storybookjs.netlify.com/official-storybook/?path=/story/*) | [![React](https://img.shields.io/npm/dm/@storybook/react.svg)](app/react) |
| [Vue](app/vue) | [v6.4.x](https://storybookjs.netlify.com/vue-kitchen-sink/) | [![Vue](https://img.shields.io/npm/dm/@storybook/vue.svg)](app/vue) |
| [Angular](app/angular) | [v6.4.x](https://storybookjs.netlify.com/angular-cli/) | [![Angular](https://img.shields.io/npm/dm/@storybook/angular.svg)](app/angular) |
| [Web components](app/web-components) | [v6.4.x](https://storybookjs.netlify.com/web-components-kitchen-sink/) | [![Svelte](https://img.shields.io/npm/dm/@storybook/web-components.svg)](app/web-components) |
| [React Native](https://github.com/storybookjs/react-native) | - | [![React Native](https://img.shields.io/npm/dm/@storybook/react-native.svg)](app/react-native) |
| [HTML](app/html) | [v6.3.x](https://storybookjs.netlify.com/html-kitchen-sink/) | [![HTML](https://img.shields.io/npm/dm/@storybook/html.svg)](app/html) |
| [Ember](app/ember) | [v6.3.x](https://storybookjs.netlify.com/ember-cli/) | [![Ember](https://img.shields.io/npm/dm/@storybook/ember.svg)](app/ember) |
| [Svelte](app/svelte) | [v6.3.x](https://storybookjs.netlify.com/svelte-kitchen-sink/) | [![Svelte](https://img.shields.io/npm/dm/@storybook/svelte.svg)](app/svelte) |
| [Preact](app/preact) | [v6.3.x](https://storybookjs.netlify.com/preact-kitchen-sink/) | [![Preact](https://img.shields.io/npm/dm/@storybook/preact.svg)](app/preact) |
| [Marionette.js](app/marionette) | - | [![Marionette.js](https://img.shields.io/npm/dm/@storybook/marionette.svg)](app/marionette) |
| [Mithril](app/mithril) | [v6.3.x](https://storybookjs.netlify.com/mithril-kitchen-sink/) | [![Mithril](https://img.shields.io/npm/dm/@storybook/mithril.svg)](app/mithril) |
| [Marko](app/marko) | [v6.3.x](https://storybookjs.netlify.com/marko-cli/) | [![Marko](https://img.shields.io/npm/dm/@storybook/marko.svg)](app/marko) |
| [Riot](app/riot) | [v6.3.x](https://storybookjs.netlify.com/riot-kitchen-sink/) | [![Riot](https://img.shields.io/npm/dm/@storybook/riot.svg)](app/riot) |
| [Rax](app/rax) | [v6.3.x](https://storybookjs.netlify.com/rax-kitchen-sink/) | [![Rax](https://img.shields.io/npm/dm/@storybook/rax.svg)](app/rax) |
| [Android, iOS, Flutter](https://github.com/storybookjs/native) | [v6.3.x](https://storybookjs.github.io/native/@storybook/native-flutter-example/index.html) | [![Rax](https://img.shields.io/npm/dm/@storybook/native.svg)](https://github.com/storybookjs/native) |
| [HTML](app/html) | [v6.4.x](https://storybookjs.netlify.com/html-kitchen-sink/) | [![HTML](https://img.shields.io/npm/dm/@storybook/html.svg)](app/html) |
| [Ember](app/ember) | [v6.4.x](https://storybookjs.netlify.com/ember-cli/) | [![Ember](https://img.shields.io/npm/dm/@storybook/ember.svg)](app/ember) |
| [Svelte](app/svelte) | [v6.4.x](https://storybookjs.netlify.com/svelte-kitchen-sink/) | [![Svelte](https://img.shields.io/npm/dm/@storybook/svelte.svg)](app/svelte) |
| [Preact](app/preact) | [v6.4.x](https://storybookjs.netlify.com/preact-kitchen-sink/) | [![Preact](https://img.shields.io/npm/dm/@storybook/preact.svg)](app/preact) |
| [Marionette.js](https://github.com/storybookjs/marionette) | - | [![Marionette.js](https://img.shields.io/npm/dm/@storybook/marionette.svg)](app/marionette) |
| [Mithril](https://github.com/storybookjs/mithril) | [v6.4.x](https://storybookjs.netlify.com/mithril-kitchen-sink/) | [![Mithril](https://img.shields.io/npm/dm/@storybook/mithril.svg)](app/mithril) |
| [Marko](https://github.com/storybookjs/marko) | [v6.4.x](https://storybookjs.netlify.com/marko-cli/) | [![Marko](https://img.shields.io/npm/dm/@storybook/marko.svg)](app/marko) |
| [Riot](https://github.com/storybookjs/riot) | [v6.4.x](https://storybookjs.netlify.com/riot-kitchen-sink/) | [![Riot](https://img.shields.io/npm/dm/@storybook/riot.svg)](app/riot) |
| [Rax](https://github.com/storybookjs/rax) | [v6.4.x](https://storybookjs.netlify.com/rax-kitchen-sink/) | [![Rax](https://img.shields.io/npm/dm/@storybook/rax.svg)](app/rax) |
| [Android, iOS, Flutter](https://github.com/storybookjs/native) | [v6.4.x](https://storybookjs.github.io/native/@storybook/native-flutter-example/index.html) | [![Rax](https://img.shields.io/npm/dm/@storybook/native.svg)](https://github.com/storybookjs/native) |
### Sub Projects
@ -141,13 +141,13 @@ See [Addon / Framework Support Table](https://storybook.js.org/docs/react/api/fr
### Deprecated Addons
| Addons | |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| [info](https://github.com/storybookjs/deprecated-addons/tree/main/addons/info) | Annotate stories with extra component usage information |
| [notes](https://github.com/storybookjs/deprecated-addons/tree/main/addons/notes) | Annotate Storybook stories with notes |
| [contexts](https://storybook.js.org/addons/@storybook/addon-contexts/) | Addon for driving your components under dynamic contexts |
| [options](https://www.npmjs.com/package/@storybook/addon-options) | Customize the Storybook UI in code |
| [knobs](https://github.com/storybookjs/addon-knobs) | Interactively edit component prop data in the Storybook UI |
| 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://storybook.js.org/addons/@storybook/addon-contexts/) | Addon for driving your components under dynamic contexts |
| [options](https://www.npmjs.com/package/@storybook/addon-options) | Customize the Storybook UI in code |
| [knobs](https://github.com/storybookjs/addon-knobs) | Interactively edit component prop data in the Storybook UI |
In order to continue improving your experience, we have to eventually deprecate certain addons in favor of new, better tools.

View File

@ -2,17 +2,12 @@
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 6.0.x | :white_check_mark: |
| 5.3.x | :white_check_mark: |
| Version | Supported |
| ---------- | ------------------ |
| 6.3, 6.4 | :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
To report a vulnerability, you can 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.gg/storybook).

29
__mocks__/fs-extra.js Normal file
View File

@ -0,0 +1,29 @@
const fs = jest.createMockFromModule('fs-extra');
// This is a custom function that our tests can use during setup to specify
// what the files on the "mock" filesystem should look like when any of the
// `fs` APIs are used.
let mockFiles = Object.create(null);
// eslint-disable-next-line no-underscore-dangle
function __setMockFiles(newMockFiles) {
mockFiles = newMockFiles;
}
// A custom version of `readdirSync` that reads from the special mocked out
// file list set via __setMockFiles
const readFile = async (filePath) => mockFiles[filePath];
const readFileSync = (filePath = '') => mockFiles[filePath];
const existsSync = (filePath) => !!mockFiles[filePath];
const lstatSync = (filePath) => ({
isFile: () => !!mockFiles[filePath],
});
// eslint-disable-next-line no-underscore-dangle
fs.__setMockFiles = __setMockFiles;
fs.readFile = readFile;
fs.readFileSync = readFileSync;
fs.existsSync = existsSync;
fs.lstatSync = lstatSync;
module.exports = fs;

View File

@ -2,7 +2,7 @@
This Storybook addon can be helpful to make your UI components more accessible.
[Framework Support](https://github.com/storybookjs/storybook/blob/main/ADDONS_SUPPORT.md)
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
![Screenshot](https://raw.githubusercontent.com/storybookjs/storybook/next/addons/a11y/docs/screenshot.png)
@ -22,6 +22,8 @@ module.exports = {
};
```
And here's a sample story file to test the addon:
```js
import React from 'react';
@ -94,7 +96,7 @@ Tip: clearly explain in a comment why a rule was overridden, itll help you an
```js
MyStory.parameters = {
a11y: {
options: {
config: {
rules: [
{
// Allow `autocomplete="nope"` on form elements,

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-a11y",
"version": "6.4.0-alpha.28",
"version": "6.5.0-alpha.36",
"description": "Test component compliance with web accessibility standards",
"keywords": [
"a11y",
@ -30,7 +30,7 @@
"types": "dist/ts3.9/index.d.ts",
"typesVersions": {
"<3.8": {
"*": [
"dist/ts3.9/*": [
"dist/ts3.4/*"
]
}
@ -45,18 +45,18 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.4.0-alpha.28",
"@storybook/api": "6.4.0-alpha.28",
"@storybook/channels": "6.4.0-alpha.28",
"@storybook/client-api": "6.4.0-alpha.28",
"@storybook/client-logger": "6.4.0-alpha.28",
"@storybook/components": "6.4.0-alpha.28",
"@storybook/core-events": "6.4.0-alpha.28",
"@storybook/theming": "6.4.0-alpha.28",
"@storybook/addons": "6.5.0-alpha.36",
"@storybook/api": "6.5.0-alpha.36",
"@storybook/channels": "6.5.0-alpha.36",
"@storybook/client-logger": "6.5.0-alpha.36",
"@storybook/components": "6.5.0-alpha.36",
"@storybook/core-events": "6.5.0-alpha.36",
"@storybook/csf": "0.0.2--canary.87bc651.0",
"@storybook/theming": "6.5.0-alpha.36",
"axe-core": "^4.2.0",
"core-js": "^3.8.2",
"global": "^4.4.0",
"lodash": "^4.17.20",
"lodash": "^4.17.21",
"react-sizeme": "^3.0.1",
"regenerator-runtime": "^0.13.7",
"ts-dedent": "^2.0.0",
@ -81,7 +81,7 @@
"publishConfig": {
"access": "public"
},
"gitHead": "921d1b75b7bf5876088fd6c5870122474df28190",
"gitHead": "7332caf9f83fe7ab1bb1e80fb52747fb4cf4cdf1",
"sbmodern": "dist/modern/index.js",
"storybook": {
"displayName": "Accessibility",

View File

@ -1,5 +1,4 @@
import global from 'global';
import axe from 'axe-core';
import { addons } from '@storybook/addons';
import { EVENTS } from './constants';
import { A11yParameters } from './params';
@ -18,28 +17,29 @@ let activeStoryId: string | undefined;
const getElement = () => {
const storyRoot = document.getElementById('story-root');
return storyRoot ? storyRoot.children : document.getElementById('root');
return storyRoot ? storyRoot.childNodes : 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);
const handleRequest = async (storyId: string) => {
const { manual } = await getParams(storyId);
if (!manual) {
run(storyId);
await run(storyId);
}
};
const run = async (storyId: string) => {
activeStoryId = storyId;
try {
const input = getParams(storyId);
const input = await getParams(storyId);
if (!active) {
active = true;
channel.emit(EVENTS.RUNNING);
const axe = await import('axe-core');
const { element = getElement(), config, options = {} } = input;
axe.reset();
@ -67,8 +67,9 @@ const run = async (storyId: string) => {
};
/** Returns story parameters or default ones. */
const getParams = (storyId: string): A11yParameters => {
const { parameters } = globalWindow.__STORYBOOK_STORY_STORE__.fromId(storyId) || {};
const getParams = async (storyId: string): Promise<A11yParameters> => {
const { parameters } =
(await globalWindow.__STORYBOOK_STORY_STORE__.loadStory({ storyId })) || {};
return (
parameters.a11y || {
config: {},

View File

@ -129,11 +129,14 @@ describe('A11YPanel', () => {
const { getByText } = render(<ThemedA11YPanel />);
const useChannelArgs = mockedApi.useChannel.mock.calls[0][0];
act(() => useChannelArgs[EVENTS.RESULT](axeResult));
await waitFor(() => {
expect(getByText(/Tests completed/)).toBeTruthy();
expect(getByText(/Violations/)).toBeTruthy();
expect(getByText(/Passes/)).toBeTruthy();
expect(getByText(/Incomplete/)).toBeTruthy();
});
await waitFor(
() => {
expect(getByText(/Tests completed/)).toBeTruthy();
expect(getByText(/Violations/)).toBeTruthy();
expect(getByText(/Passes/)).toBeTruthy();
expect(getByText(/Incomplete/)).toBeTruthy();
},
{ timeout: 2000 }
);
});
});

View File

@ -94,9 +94,10 @@ export const A11YPanel: React.FC = () => {
emit(EVENTS.MANUAL, storyId);
}, [storyId]);
const manualActionItems = useMemo(() => [{ title: 'Run test', onClick: handleManual }], [
handleManual,
]);
const manualActionItems = useMemo(
() => [{ title: 'Run test', onClick: handleManual }],
[handleManual]
);
const readyActionItems = useMemo(
() => [
{

View File

@ -1,15 +1,16 @@
import { DecoratorFunction } from '@storybook/addons';
import { AnyFramework, DecoratorFunction } from '@storybook/csf';
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
export { PARAM_KEY } from './constants';
export * from './highlight';
export * from './params';
if (module && module.hot && module.hot.decline) {
module.hot.decline();
}
export const withA11y: DecoratorFunction = deprecate(
export const withA11y: DecoratorFunction<AnyFramework> = deprecate(
(storyFn, storyContext) => {
return storyFn(storyContext);
},

View File

@ -2,7 +2,10 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env", "jest"],
"types": [
"webpack-env",
"jest"
],
"forceConsistentCasingInFileNames": true,
"strict": true,
"noUnusedLocals": true,
@ -10,7 +13,9 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*"],
"include": [
"src/**/*"
],
"exclude": [
"src/**/*.test.*",
"src/**/tests/**/*",
@ -19,4 +24,4 @@
"src/**/*.mockdata.*",
"src/**/__testfixtures__/**"
]
}
}

View File

@ -60,7 +60,7 @@ To apply the configuration globally use the `configureActions` function in your
import { configureActions } from '@storybook/addon-actions';
configureActions({
depth: 100,
maxDepth: 100,
// Limit the number of items logged into the actions panel
limit: 20,
});
@ -70,7 +70,7 @@ To apply the configuration per action use:
```js
action('my-action', {
depth: 5,
maxDepth: 5,
});
```
@ -78,6 +78,6 @@ action('my-action', {
| Name | Type | Description | Default |
| -------------------- | ------- | ----------------------------------------------------------------------------------- | ------- |
| `depth` | Number | Configures the transferred depth of any logged objects. | `10` |
| `maxDepth` | 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

@ -4,7 +4,7 @@ Storybook Addon Actions can be used to display data received by event handlers i
[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)
![Screenshot](https://raw.githubusercontent.com/storybookjs/storybook/next/addons/actions/docs/screenshot.png)
## Installation

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-actions",
"version": "6.4.0-alpha.28",
"version": "6.5.0-alpha.36",
"description": "Get UI feedback when an action is performed on an interactive element",
"keywords": [
"storybook",
@ -26,7 +26,7 @@
"types": "dist/ts3.9/index.d.ts",
"typesVersions": {
"<3.8": {
"*": [
"dist/ts3.9/*": [
"dist/ts3.4/*"
]
}
@ -41,20 +41,21 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.4.0-alpha.28",
"@storybook/api": "6.4.0-alpha.28",
"@storybook/client-api": "6.4.0-alpha.28",
"@storybook/components": "6.4.0-alpha.28",
"@storybook/core-events": "6.4.0-alpha.28",
"@storybook/theming": "6.4.0-alpha.28",
"@storybook/addons": "6.5.0-alpha.36",
"@storybook/api": "6.5.0-alpha.36",
"@storybook/components": "6.5.0-alpha.36",
"@storybook/core-events": "6.5.0-alpha.36",
"@storybook/csf": "0.0.2--canary.87bc651.0",
"@storybook/theming": "6.5.0-alpha.36",
"core-js": "^3.8.2",
"fast-deep-equal": "^3.1.3",
"global": "^4.4.0",
"lodash": "^4.17.20",
"lodash": "^4.17.21",
"polished": "^4.0.5",
"prop-types": "^15.7.2",
"react-inspector": "^5.1.0",
"regenerator-runtime": "^0.13.7",
"telejson": "^5.3.3",
"ts-dedent": "^2.0.0",
"util-deprecate": "^1.0.2",
"uuid-browser": "^3.1.0"
@ -78,7 +79,7 @@
"publishConfig": {
"access": "public"
},
"gitHead": "921d1b75b7bf5876088fd6c5870122474df28190",
"gitHead": "7332caf9f83fe7ab1bb1e80fb52747fb4cf4cdf1",
"sbmodern": "dist/modern/index.js",
"storybook": {
"displayName": "Actions",

View File

@ -1,6 +1,9 @@
export interface ActionOptions {
depth?: number;
clearOnStoryChange?: boolean;
limit?: number;
allowFunction?: boolean;
import type { Options as TelejsonOptions } from 'telejson';
interface Options {
depth: number; // backards compatibility, remove in 7.0
clearOnStoryChange: boolean;
limit: number;
}
export type ActionOptions = Partial<Options> & Partial<TelejsonOptions>;

View File

@ -7,11 +7,11 @@ describe('actions parameter enhancers', () => {
const argTypes = { onClick: {}, onFocus: {}, somethingElse: {} };
it('should add actions that match a pattern', () => {
const args = inferActionsFromArgTypesRegex(({
args: {},
const args = inferActionsFromArgTypesRegex({
initialArgs: {},
argTypes,
parameters,
} as unknown) as StoryContext);
} as unknown as StoryContext);
expect(args).toEqual({
onClick: expect.any(Function),
onFocus: expect.any(Function),
@ -19,41 +19,41 @@ describe('actions parameter enhancers', () => {
});
it('should NOT override pre-existing args', () => {
const args = inferActionsFromArgTypesRegex(({
args: { onClick: 'pre-existing value' },
const args = inferActionsFromArgTypesRegex({
initialArgs: { onClick: 'pre-existing value' },
argTypes,
parameters,
} as unknown) as StoryContext);
} as unknown as StoryContext);
expect(args).toEqual({ onFocus: expect.any(Function) });
});
it('should NOT override pre-existing args, if null', () => {
const args = inferActionsFromArgTypesRegex(({
args: { onClick: null },
const args = inferActionsFromArgTypesRegex({
initialArgs: { onClick: null },
argTypes,
parameters,
} as unknown) as StoryContext);
} as unknown as StoryContext);
expect(args).toEqual({ onFocus: expect.any(Function) });
});
it('should override pre-existing args, if undefined', () => {
const args = inferActionsFromArgTypesRegex(({
args: { onClick: undefined },
const args = inferActionsFromArgTypesRegex({
initialArgs: { onClick: undefined },
argTypes,
parameters,
} as unknown) as StoryContext);
} as unknown as StoryContext);
expect(args).toEqual({ onClick: expect.any(Function), onFocus: expect.any(Function) });
});
it('should do nothing if actions are disabled', () => {
const args = inferActionsFromArgTypesRegex(({
args: {},
const args = inferActionsFromArgTypesRegex({
initialArgs: {},
argTypes,
parameters: {
...parameters,
actions: { ...parameters.actions, disable: true },
},
} as unknown) as StoryContext);
} as unknown as StoryContext);
expect(args).toEqual({});
});
});
@ -65,7 +65,11 @@ describe('actions parameter enhancers', () => {
};
it('should add actions based on action.args', () => {
expect(
addActionsFromArgTypes(({ args: {}, argTypes, parameters: {} } as unknown) as StoryContext)
addActionsFromArgTypes({
initialArgs: {},
argTypes,
parameters: {},
} as unknown as StoryContext)
).toEqual({
onClick: expect.any(Function),
onBlur: expect.any(Function),
@ -74,41 +78,41 @@ describe('actions parameter enhancers', () => {
it('should NOT override pre-existing args', () => {
expect(
addActionsFromArgTypes(({
addActionsFromArgTypes({
argTypes: { onClick: { action: 'clicked!' } },
args: { onClick: 'pre-existing value' },
initialArgs: { onClick: 'pre-existing value' },
parameters: {},
} as unknown) as StoryContext)
} as unknown as StoryContext)
).toEqual({});
});
it('should NOT override pre-existing args, if null', () => {
expect(
addActionsFromArgTypes(({
addActionsFromArgTypes({
argTypes: { onClick: { action: 'clicked!' } },
args: { onClick: null },
initialArgs: { onClick: null },
parameters: {},
} as unknown) as StoryContext)
} as unknown as StoryContext)
).toEqual({});
});
it('should override pre-existing args, if undefined', () => {
expect(
addActionsFromArgTypes(({
addActionsFromArgTypes({
argTypes: { onClick: { action: 'clicked!' } },
args: { onClick: undefined },
initialArgs: { onClick: undefined },
parameters: {},
} as unknown) as StoryContext)
} as unknown as StoryContext)
).toEqual({ onClick: expect.any(Function) });
});
it('should do nothing if actions are disabled', () => {
expect(
addActionsFromArgTypes(({
args: {},
addActionsFromArgTypes({
initialArgs: {},
argTypes,
parameters: { actions: { disable: true } },
} as unknown) as StoryContext)
} as unknown as StoryContext)
).toEqual({});
});
});

View File

@ -1,5 +1,5 @@
import { Args } from '@storybook/addons';
import { ArgsEnhancer } from '@storybook/client-api';
import { AnyFramework, ArgsEnhancer } from '@storybook/csf';
import { action } from '../index';
// interface ActionsParameter {
@ -12,9 +12,9 @@ import { action } from '../index';
* matches a regex, such as `^on.*` for react-style `onClick` etc.
*/
export const inferActionsFromArgTypesRegex: ArgsEnhancer = (context) => {
export const inferActionsFromArgTypesRegex: ArgsEnhancer<AnyFramework> = (context) => {
const {
args,
initialArgs,
argTypes,
parameters: { actions },
} = context;
@ -28,7 +28,7 @@ export const inferActionsFromArgTypesRegex: ArgsEnhancer = (context) => {
);
return argTypesMatchingRegex.reduce((acc, [name, argType]) => {
if (typeof args[name] === 'undefined') {
if (typeof initialArgs[name] === 'undefined') {
acc[name] = action(name);
}
return acc;
@ -38,9 +38,9 @@ export const inferActionsFromArgTypesRegex: ArgsEnhancer = (context) => {
/**
* Add action args for list of strings.
*/
export const addActionsFromArgTypes: ArgsEnhancer = (context) => {
export const addActionsFromArgTypes: ArgsEnhancer<AnyFramework> = (context) => {
const {
args,
initialArgs,
argTypes,
parameters: { actions },
} = context;
@ -51,7 +51,7 @@ export const addActionsFromArgTypes: ArgsEnhancer = (context) => {
const argTypesWithAction = Object.entries(argTypes).filter(([name, argType]) => !!argType.action);
return argTypesWithAction.reduce((acc, [name, argType]) => {
if (typeof args[name] === 'undefined') {
if (typeof initialArgs[name] === 'undefined') {
acc[name] = action(typeof argType.action === 'string' ? argType.action : name);
}
return acc;

View File

@ -4,6 +4,40 @@ import { EVENT_ID } from '../constants';
import { ActionDisplay, ActionOptions, HandlerFunction } from '../models';
import { config } from './configureActions';
type SyntheticEvent = any; // import('react').SyntheticEvent;
const findProto = (obj: unknown, callback: (proto: any) => boolean): Function | null => {
const proto = Object.getPrototypeOf(obj);
if (!proto || callback(proto)) return proto;
return findProto(proto, callback);
};
const isReactSyntheticEvent = (e: unknown): e is SyntheticEvent =>
Boolean(
typeof e === 'object' &&
e &&
findProto(e, (proto) => /^Synthetic(?:Base)?Event$/.test(proto.constructor.name)) &&
typeof (e as SyntheticEvent).persist === 'function'
);
const serializeArg = <T>(a: T) => {
if (isReactSyntheticEvent(a)) {
const e: SyntheticEvent = Object.create(
a.constructor.prototype,
Object.getOwnPropertyDescriptors(a)
);
e.persist();
const viewDescriptor = Object.getOwnPropertyDescriptor(e, 'view');
// dont send the entire window object over.
const view: unknown = viewDescriptor?.value;
if (typeof view === 'object' && view?.constructor.name === 'Window') {
Object.defineProperty(e, 'view', {
...viewDescriptor,
value: Object.create(view.constructor.prototype),
});
}
return e;
}
return a;
};
export function action(name: string, options: ActionOptions = {}): HandlerFunction {
const actionOptions = {
...config,
@ -14,7 +48,8 @@ export function action(name: string, options: ActionOptions = {}): HandlerFuncti
const channel = addons.getChannel();
const id = uuidv4();
const minDepth = 5; // anything less is really just storybook internals
const normalizedArgs = args.length > 1 ? args : args[0];
const serializedArgs = args.map(serializeArg);
const normalizedArgs = args.length > 1 ? serializedArgs : serializedArgs[0];
const actionDisplayToEmit: ActionDisplay = {
id,
@ -22,7 +57,7 @@ export function action(name: string, options: ActionOptions = {}): HandlerFuncti
data: { name, args: normalizedArgs },
options: {
...actionOptions,
depth: minDepth + (actionOptions.depth || 3),
maxDepth: minDepth + (actionOptions.depth || 3),
allowFunction: actionOptions.allowFunction || false,
},
};

View File

@ -1,10 +1,9 @@
// Based on http://backbonejs.org/docs/backbone.html#section-164
import global from 'global';
import { useEffect } from '@storybook/client-api';
import { useEffect, makeDecorator } from '@storybook/addons';
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
import { makeDecorator } from '@storybook/addons';
import { actions } from './actions';
import { PARAM_KEY } from '../constants';

View File

@ -2,9 +2,14 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env", "jest"]
"types": [
"webpack-env",
"jest"
]
},
"include": ["src/**/*"],
"include": [
"src/**/*"
],
"exclude": [
"src/**/*.test.*",
"src/**/tests/**/*",
@ -13,4 +18,4 @@
"src/**/*.mockdata.*",
"src/**/__testfixtures__/**"
]
}
}

View File

@ -4,7 +4,7 @@ Storybook Addon Backgrounds can be used to change background colors inside the p
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
![React Storybook Screenshot](https://raw.githubusercontent.com/storybookjs/storybook/main/docs/static/img/addon-backgrounds.gif)
![React Storybook Screenshot](https://raw.githubusercontent.com/storybookjs/storybook/next/addons/backgrounds/docs/addon-backgrounds.gif)
## Installation

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-backgrounds",
"version": "6.4.0-alpha.28",
"version": "6.5.0-alpha.36",
"description": "Switch backgrounds to view components in different settings",
"keywords": [
"addon",
@ -30,7 +30,7 @@
"types": "dist/ts3.9/index.d.ts",
"typesVersions": {
"<3.8": {
"*": [
"dist/ts3.9/*": [
"dist/ts3.4/*"
]
}
@ -45,12 +45,13 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.4.0-alpha.28",
"@storybook/api": "6.4.0-alpha.28",
"@storybook/client-logger": "6.4.0-alpha.28",
"@storybook/components": "6.4.0-alpha.28",
"@storybook/core-events": "6.4.0-alpha.28",
"@storybook/theming": "6.4.0-alpha.28",
"@storybook/addons": "6.5.0-alpha.36",
"@storybook/api": "6.5.0-alpha.36",
"@storybook/client-logger": "6.5.0-alpha.36",
"@storybook/components": "6.5.0-alpha.36",
"@storybook/core-events": "6.5.0-alpha.36",
"@storybook/csf": "0.0.2--canary.87bc651.0",
"@storybook/theming": "6.5.0-alpha.36",
"core-js": "^3.8.2",
"global": "^4.4.0",
"memoizerific": "^1.11.3",
@ -76,7 +77,7 @@
"publishConfig": {
"access": "public"
},
"gitHead": "921d1b75b7bf5876088fd6c5870122474df28190",
"gitHead": "7332caf9f83fe7ab1bb1e80fb52747fb4cf4cdf1",
"sbmodern": "dist/modern/index.js",
"storybook": {
"displayName": "Backgrounds",

View File

@ -1,4 +1,5 @@
import { StoryFn as StoryFunction, StoryContext, useMemo, useEffect } from '@storybook/addons';
import { useMemo, useEffect } from '@storybook/addons';
import { AnyFramework, PartialStoryFn as StoryFunction, StoryContext } from '@storybook/csf';
import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants';
import {
@ -8,7 +9,10 @@ import {
isReduceMotionEnabled,
} from '../helpers';
export const withBackground = (StoryFn: StoryFunction, context: StoryContext) => {
export const withBackground = (
StoryFn: StoryFunction<AnyFramework>,
context: StoryContext<AnyFramework>
) => {
const { globals, parameters } = context;
const globalsBackgroundColor = globals[BACKGROUNDS_PARAM_KEY]?.value;
const backgroundsConfig = parameters[BACKGROUNDS_PARAM_KEY];

View File

@ -1,6 +1,7 @@
import dedent from 'ts-dedent';
import deprecate from 'util-deprecate';
import { StoryFn as StoryFunction, StoryContext, useMemo, useEffect } from '@storybook/addons';
import { useMemo, useEffect } from '@storybook/addons';
import { AnyFramework, PartialStoryFn as StoryFunction, StoryContext } from '@storybook/csf';
import { clearStyles, addGridStyle } from '../helpers';
import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants';
@ -15,7 +16,10 @@ const deprecatedCellSizeWarning = deprecate(
`
);
export const withGrid = (StoryFn: StoryFunction, context: StoryContext) => {
export const withGrid = (
StoryFn: StoryFunction<AnyFramework>,
context: StoryContext<AnyFramework>
) => {
const { globals, parameters } = context;
const gridParameters = parameters[BACKGROUNDS_PARAM_KEY].grid;
const isActive = globals[BACKGROUNDS_PARAM_KEY]?.grid === true && gridParameters.disable !== true;

View File

@ -2,7 +2,9 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env"]
"types": [
"webpack-env"
]
},
"include": [
"src/**/*"
@ -15,4 +17,4 @@
"src/**/*.mockdata.*",
"src/**/__testfixtures__/**"
]
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-controls",
"version": "6.4.0-alpha.28",
"version": "6.5.0-alpha.36",
"description": "Interact with component inputs dynamically in the Storybook UI",
"keywords": [
"addon",
@ -30,7 +30,7 @@
"types": "dist/ts3.9/index.d.ts",
"typesVersions": {
"<3.8": {
"*": [
"dist/ts3.9/*": [
"dist/ts3.4/*"
]
}
@ -45,13 +45,17 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.4.0-alpha.28",
"@storybook/api": "6.4.0-alpha.28",
"@storybook/client-api": "6.4.0-alpha.28",
"@storybook/components": "6.4.0-alpha.28",
"@storybook/node-logger": "6.4.0-alpha.28",
"@storybook/theming": "6.4.0-alpha.28",
"@storybook/addons": "6.5.0-alpha.36",
"@storybook/api": "6.5.0-alpha.36",
"@storybook/client-logger": "6.5.0-alpha.36",
"@storybook/components": "6.5.0-alpha.36",
"@storybook/core-common": "6.5.0-alpha.36",
"@storybook/csf": "0.0.2--canary.87bc651.0",
"@storybook/node-logger": "6.5.0-alpha.36",
"@storybook/store": "6.5.0-alpha.36",
"@storybook/theming": "6.5.0-alpha.36",
"core-js": "^3.8.2",
"lodash": "^4.17.21",
"ts-dedent": "^2.0.0"
},
"peerDependencies": {
@ -69,7 +73,7 @@
"publishConfig": {
"access": "public"
},
"gitHead": "921d1b75b7bf5876088fd6c5870122474df28190",
"gitHead": "7332caf9f83fe7ab1bb1e80fb52747fb4cf4cdf1",
"sbmodern": "dist/modern/register.js",
"storybook": {
"displayName": "Controls",

View File

@ -1,7 +1,7 @@
const { ensureDocsBeforeControls } = require('./dist/cjs/preset/ensureDocsBeforeControls');
function managerEntries(entry = [], options) {
ensureDocsBeforeControls(options.configDir);
// eslint-disable-next-line global-require
const { checkDocsLoaded } = require('./dist/cjs/preset/checkDocsLoaded');
checkDocsLoaded(options.configDir);
return [...entry, require.resolve('./dist/esm/register')];
}

View File

@ -0,0 +1,19 @@
import { checkAddonOrder } from '@storybook/core-common';
import path from 'path';
export const checkDocsLoaded = (configDir: string) => {
checkAddonOrder({
before: {
name: '@storybook/addon-docs',
inEssentials: true,
},
after: {
name: '@storybook/addon-controls',
inEssentials: true,
},
configFile: path.isAbsolute(configDir)
? path.join(configDir, 'main')
: path.join(process.cwd(), configDir, 'main'),
getConfig: (configFile) => import(configFile),
});
};

View File

@ -1,27 +0,0 @@
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']],
[['@storybook/addon-docs', 'foo/node_modules/@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

@ -1,48 +0,0 @@
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.includes(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' to be listed before '@storybook/addon-controls' (or '@storybook/addon-essentials'). Check your main.js?
If addon-docs or addon-essentials is included by another addon/preset you can safely ignore this warning.
https://github.com/storybookjs/storybook/issues/11442
`);
}
} catch (err) {
logger.warn(`Unable to find main.js: ${mainFile}`);
}
};

View File

@ -2,9 +2,15 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env", "jest", "node"]
"types": [
"webpack-env",
"jest",
"node"
]
},
"include": ["src/**/*"],
"include": [
"src/**/*"
],
"exclude": [
"src/**/*.test.*",
"src/**/tests/**/*",
@ -13,4 +19,4 @@
"src/**/*.mockdata.*",
"src/**/__testfixtures__/**"
]
}
}

View File

@ -77,19 +77,10 @@ For more information on `MDX`, see the [`MDX` reference](https://github.com/stor
## Framework support
Storybook Docs supports all view layers that Storybook supports except for React Native (currently). There are some framework-specific features as well, such as props tables and inline story rendering. This chart captures the current state of support:
Storybook Docs supports all view layers that Storybook supports except for React Native (currently). There are some framework-specific features as well, such as props tables and inline story rendering. The following page captures the current state of support:
[Framework Support](https://storybook.js.org/docs/react/api/frameworks-feature-support)
| | React | Vue | Angular | Ember | Web Components | Marko | HTML | Svelte | Preact | Riot | Mithril | Marko |
| ----------------- | :---: | :-: | :-----: | :---: | :------------: | :---: | :--: | :----: | :----: | :--: | :-----: | :---: |
| MDX stories | + | + | + | + | + | WIP | + | + | + | + | + | + |
| CSF stories | + | + | + | + | + | WIP | + | + | + | + | + | + |
| StoriesOf stories | + | + | + | + | + | WIP | + | + | + | + | + | + |
| Source | + | + | + | + | + | WIP | + | + | + | + | + | + |
| Notes / Info | + | + | + | + | + | WIP | + | + | + | + | + | + |
| Props table | + | + | + | + | + | WIP | | | | | | |
| Props controls | + | + | + | | | WIP | | | | | | |
| Description | + | + | + | + | + | WIP | | | | | | |
| Inline stories | + | + | + | | + | WIP | + | | | | | |
**Note:** `#` = WIP support

View File

@ -210,12 +210,6 @@ And for `MDX` you can modify it as an attribute on the `Story` element:
Storybook Docs renders all Angular stories inside IFrames by default. But it is possible to use an inline rendering:
To get this, you'll first need to install Angular elements:
```sh
yarn add -D @angular/elements @webcomponents/custom-elements
```
Then update `.storybook/preview.js`:
```js

1
addons/docs/angular/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export * from '../dist/ts3.9/frameworks/angular/index.d';

View File

@ -63,7 +63,8 @@ basic.parameters = {
```md
import { Meta, Story } from '@storybook/addon-docs';
import \* as stories from './Button.stories.js';
import * as stories from './Button.stories.js';
import { Button } from './Button';
import { SomeComponent } from 'path/to/SomeComponent';
<Meta title="Demo/Button" component={Button} />

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-docs",
"version": "6.4.0-alpha.28",
"version": "6.5.0-alpha.36",
"description": "Document component usage and properties in Markdown",
"keywords": [
"addon",
@ -29,7 +29,7 @@
"types": "dist/ts3.9/index.d.ts",
"typesVersions": {
"<3.8": {
"*": [
"dist/ts3.9/*": [
"dist/ts3.4/*"
]
}
@ -40,6 +40,7 @@
"common/**/*",
"ember/**/*",
"html/**/*",
"svelte/**/*",
"postinstall/**/*",
"react/**/*",
"vue/**/*",
@ -63,20 +64,21 @@
"@mdx-js/loader": "^1.6.22",
"@mdx-js/mdx": "^1.6.22",
"@mdx-js/react": "^1.6.22",
"@storybook/addons": "6.4.0-alpha.28",
"@storybook/api": "6.4.0-alpha.28",
"@storybook/builder-webpack4": "6.4.0-alpha.28",
"@storybook/client-api": "6.4.0-alpha.28",
"@storybook/client-logger": "6.4.0-alpha.28",
"@storybook/components": "6.4.0-alpha.28",
"@storybook/core": "6.4.0-alpha.28",
"@storybook/core-events": "6.4.0-alpha.28",
"@storybook/csf": "0.0.1",
"@storybook/csf-tools": "6.4.0-alpha.28",
"@storybook/node-logger": "6.4.0-alpha.28",
"@storybook/postinstall": "6.4.0-alpha.28",
"@storybook/source-loader": "6.4.0-alpha.28",
"@storybook/theming": "6.4.0-alpha.28",
"@storybook/addons": "6.5.0-alpha.36",
"@storybook/api": "6.5.0-alpha.36",
"@storybook/builder-webpack4": "6.5.0-alpha.36",
"@storybook/client-logger": "6.5.0-alpha.36",
"@storybook/components": "6.5.0-alpha.36",
"@storybook/core": "6.5.0-alpha.36",
"@storybook/core-events": "6.5.0-alpha.36",
"@storybook/csf": "0.0.2--canary.87bc651.0",
"@storybook/csf-tools": "6.5.0-alpha.36",
"@storybook/node-logger": "6.5.0-alpha.36",
"@storybook/postinstall": "6.5.0-alpha.36",
"@storybook/preview-web": "6.5.0-alpha.36",
"@storybook/source-loader": "6.5.0-alpha.36",
"@storybook/store": "6.5.0-alpha.36",
"@storybook/theming": "6.5.0-alpha.36",
"acorn": "^7.4.1",
"acorn-jsx": "^5.3.1",
"acorn-walk": "^7.2.0",
@ -88,12 +90,12 @@
"html-tags": "^3.1.0",
"js-string-escape": "^1.0.1",
"loader-utils": "^2.0.0",
"lodash": "^4.17.20",
"lodash": "^4.17.21",
"nanoid": "^3.1.23",
"p-limit": "^3.1.0",
"prettier": "^2.2.1",
"prettier": ">=2.2.1 <=2.3.0",
"prop-types": "^15.7.2",
"react-element-to-jsx-string": "^14.3.2",
"react-element-to-jsx-string": "^14.3.4",
"regenerator-runtime": "^0.13.7",
"remark-external-links": "^8.0.0",
"remark-slug": "^6.0.0",
@ -103,12 +105,13 @@
"devDependencies": {
"@angular/core": "^11.2.14",
"@babel/core": "^7.12.10",
"@emotion/core": "^10.1.1",
"@emotion/core": "^10.3.1",
"@emotion/styled": "^10.0.27",
"@storybook/angular": "6.4.0-alpha.28",
"@storybook/react": "6.4.0-alpha.28",
"@storybook/vue": "6.4.0-alpha.28",
"@storybook/web-components": "6.4.0-alpha.28",
"@storybook/angular": "6.5.0-alpha.36",
"@storybook/html": "6.5.0-alpha.36",
"@storybook/react": "6.5.0-alpha.36",
"@storybook/vue": "6.5.0-alpha.36",
"@storybook/web-components": "6.5.0-alpha.36",
"@types/cross-spawn": "^6.0.2",
"@types/doctrine": "^0.0.3",
"@types/enzyme": "^3.10.8",
@ -124,12 +127,12 @@
"fs-extra": "^9.0.1",
"jest": "^26.6.3",
"jest-specific-snapshot": "^4.0.0",
"lit-element": "^3.0.0-rc.2",
"lit-html": "^2.0.0-rc.3",
"lit-element": "^3.0.2",
"lit-html": "^2.0.2",
"require-from-string": "^2.0.2",
"rxjs": "^6.6.3",
"styled-components": "^5.2.1",
"terser-webpack-plugin": "^5.0.3",
"sveltedoc-parser": "4.1.0",
"tmp": "^0.2.1",
"tslib": "^2.1.0",
"vue": "^2.6.10",
@ -138,12 +141,14 @@
"zone.js": "^0.11.3"
},
"peerDependencies": {
"@storybook/angular": "6.4.0-alpha.28",
"@storybook/vue": "6.4.0-alpha.28",
"@storybook/vue3": "6.4.0-alpha.28",
"@storybook/web-components": "6.4.0-alpha.28",
"lit": "^2.0.0-rc.1",
"lit-html": "^1.4.1 || ^2.0.0-rc.3",
"@storybook/angular": "6.5.0-alpha.36",
"@storybook/html": "6.5.0-alpha.36",
"@storybook/react": "6.5.0-alpha.36",
"@storybook/vue": "6.5.0-alpha.36",
"@storybook/vue3": "6.5.0-alpha.36",
"@storybook/web-components": "6.5.0-alpha.36",
"lit": "^2.0.0",
"lit-html": "^1.4.1 || ^2.0.0",
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0",
"svelte": "^3.31.2",
@ -155,6 +160,12 @@
"@storybook/angular": {
"optional": true
},
"@storybook/html": {
"optional": true
},
"@storybook/react": {
"optional": true
},
"@storybook/vue": {
"optional": true
},
@ -192,7 +203,7 @@
"publishConfig": {
"access": "public"
},
"gitHead": "921d1b75b7bf5876088fd6c5870122474df28190",
"gitHead": "7332caf9f83fe7ab1bb1e80fb52747fb4cf4cdf1",
"sbmodern": "dist/modern/index.js",
"storybook": {
"displayName": "Docs",

View File

@ -1 +1,2 @@
/* eslint-disable import/extensions */
require('./dist/esm/register.js');

View File

@ -1,24 +1,23 @@
/* eslint-disable no-underscore-dangle */
import React, { FC, useContext, useEffect, useState, useCallback } from 'react';
import mapValues from 'lodash/mapValues';
import {
ArgsTable as PureArgsTable,
ArgsTableProps as PureArgsTableProps,
ArgsTableError,
ArgTypes,
SortType,
TabbedArgsTable,
} from '@storybook/components';
import { Args } from '@storybook/addons';
import { StoryStore, filterArgTypes } from '@storybook/client-api';
import type { PropDescriptor } from '@storybook/client-api';
import { addons } from '@storybook/addons';
import { filterArgTypes, PropDescriptor } from '@storybook/store';
import Events from '@storybook/core-events';
import { StrictArgTypes, Args } from '@storybook/csf';
import { DocsContext, DocsContextProps } from './DocsContext';
import { Component, CURRENT_SELECTION, PRIMARY_STORY } from './types';
import { getComponentName, getDocsStories } from './utils';
import { getComponentName } from './utils';
import { ArgTypesExtractor } from '../lib/docgen/types';
import { lookupStoryId } from './Story';
import { useStory } from './useStory';
interface BaseProps {
include?: PropDescriptor;
@ -45,29 +44,33 @@ type ArgsTableProps = BaseProps | OfProps | ComponentsProps | StoryProps;
const useArgs = (
storyId: string,
storyStore: StoryStore
context: DocsContextProps
): [Args, (args: Args) => void, (argNames?: string[]) => void] => {
const story = storyStore.fromId(storyId);
const channel = addons.getChannel();
const story = context.storyById(storyId);
if (!story) {
throw new Error(`Unknown story: ${storyId}`);
}
const { args: initialArgs } = story;
const [args, setArgs] = useState(initialArgs);
const storyContext = context.getStoryContext(story);
const [args, setArgs] = useState(storyContext.args);
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);
channel.on(Events.STORY_ARGS_UPDATED, cb);
return () => channel.off(Events.STORY_ARGS_UPDATED, cb);
}, [storyId]);
const updateArgs = useCallback((newArgs) => storyStore.updateStoryArgs(storyId, newArgs), [
storyId,
]);
const updateArgs = useCallback(
(updatedArgs) => channel.emit(Events.UPDATE_STORY_ARGS, { storyId, updatedArgs }),
[storyId]
);
const resetArgs = useCallback(
(argNames?: string[]) => storyStore.resetStoryArgs(storyId, argNames),
(argNames?: string[]) => channel.emit(Events.RESET_STORY_ARGS, { storyId, argNames }),
[storyId]
);
return [args, updateArgs, resetArgs];
@ -75,12 +78,12 @@ const useArgs = (
export const extractComponentArgTypes = (
component: Component,
{ parameters }: DocsContextProps,
{ id, storyById }: DocsContextProps,
include?: PropDescriptor,
exclude?: PropDescriptor
): ArgTypes => {
const params = parameters || {};
const { extractArgTypes }: { extractArgTypes: ArgTypesExtractor } = params.docs || {};
): StrictArgTypes => {
const { parameters } = storyById(id);
const { extractArgTypes }: { extractArgTypes: ArgTypesExtractor } = parameters.docs || {};
if (!extractArgTypes) {
throw new Error(ArgsTableError.ARGS_UNSUPPORTED);
}
@ -94,11 +97,13 @@ const isShortcut = (value?: string) => {
return value && [CURRENT_SELECTION, PRIMARY_STORY].includes(value);
};
export const getComponent = (props: ArgsTableProps = {}, context: DocsContextProps): Component => {
export const getComponent = (
props: ArgsTableProps = {},
{ id, storyById }: DocsContextProps
): Component => {
const { of } = props as OfProps;
const { story } = props as StoryProps;
const { parameters = {} } = context;
const { component } = parameters;
const { component } = storyById(id);
if (isShortcut(of) || isShortcut(story)) {
return component || null;
}
@ -127,47 +132,49 @@ export const StoryTable: FC<
StoryProps & { component: Component; subcomponents: Record<string, Component> }
> = (props) => {
const context = useContext(DocsContext);
const { id: currentId, componentStories } = context;
const {
id: currentId,
parameters: { argTypes },
storyStore,
} = context;
const { story, component, subcomponents, showComponent, include, exclude, sort } = props;
let storyArgTypes;
story: storyName,
component,
subcomponents,
showComponent,
include,
exclude,
sort,
} = props;
try {
let storyId;
switch (story) {
switch (storyName) {
case CURRENT_SELECTION: {
storyId = currentId;
storyArgTypes = argTypes;
break;
}
case PRIMARY_STORY: {
const primaryStory = getDocsStories(context)[0];
const primaryStory = componentStories()[0];
storyId = primaryStory.id;
storyArgTypes = primaryStory.parameters.argTypes;
break;
}
default: {
storyId = lookupStoryId(story, context);
const data = storyStore.fromId(storyId);
storyArgTypes = data.parameters.argTypes;
storyId = lookupStoryId(storyName, context);
}
}
storyArgTypes = filterArgTypes(storyArgTypes, include, exclude);
const story = useStory(storyId, context);
// eslint-disable-next-line prefer-const
let [args, updateArgs, resetArgs] = useArgs(storyId, context);
if (!story) return <PureArgsTable isLoading updateArgs={updateArgs} resetArgs={resetArgs} />;
const argTypes = filterArgTypes(story.argTypes, include, exclude);
const mainLabel = getComponentName(component) || 'Story';
// eslint-disable-next-line prefer-const
let [args, updateArgs, resetArgs] = useArgs(storyId, storyStore);
let tabs = { [mainLabel]: { rows: storyArgTypes, args, updateArgs, resetArgs } } as Record<
let tabs = { [mainLabel]: { rows: argTypes, 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);
const storyHasArgsWithControls = argTypes && Object.values(argTypes).find((v) => !!v?.control);
if (!storyHasArgsWithControls) {
updateArgs = null;
@ -203,15 +210,19 @@ export const ComponentsTable: FC<ComponentsProps> = (props) => {
export const ArgsTable: FC<ArgsTableProps> = (props) => {
const context = useContext(DocsContext);
const { parameters: { subcomponents, controls } = {} } = context;
const { id, storyById } = context;
const {
parameters: { controls },
subcomponents,
} = storyById(id);
const { include, exclude, components, sort: sortProp } = props as ComponentsProps;
const { story } = props as StoryProps;
const { story: storyName } = props as StoryProps;
const sort = sortProp || controls?.sort;
const main = getComponent(props, context);
if (story) {
if (storyName) {
return <StoryTable {...(props as StoryProps)} component={main} {...{ subcomponents, sort }} />;
}

View File

@ -1,14 +1,16 @@
import React, { FC, ReactElement, ReactNode, ReactNodeArray, useContext } from 'react';
import { MDXProvider } from '@mdx-js/react';
import { toId, storyNameFromExport } from '@storybook/csf';
import { toId, storyNameFromExport, AnyFramework } from '@storybook/csf';
import {
resetComponents,
Preview as PurePreview,
PreviewProps as PurePreviewProps,
PreviewSkeleton,
} from '@storybook/components';
import { DocsContext, DocsContextProps } from './DocsContext';
import { SourceContext, SourceContextProps } from './SourceContainer';
import { getSourceProps, SourceState } from './Source';
import { useStories } from './useStory';
export { SourceState };
@ -19,47 +21,59 @@ type CanvasProps = PurePreviewProps & {
const getPreviewProps = (
{ withSource, mdxSource, children, ...props }: CanvasProps & { children?: ReactNode },
docsContext: DocsContextProps,
docsContext: DocsContextProps<AnyFramework>,
sourceContext: SourceContextProps
): PurePreviewProps => {
const { mdxComponentMeta, mdxStoryNameToKey } = docsContext;
) => {
const { mdxComponentAnnotations, mdxStoryNameToKey } = docsContext;
let sourceState = withSource;
let isLoading = false;
if (sourceState === SourceState.NONE) {
return props;
return { isLoading, previewProps: props };
}
if (mdxSource) {
return {
...props,
withSource: getSourceProps({ code: decodeURI(mdxSource) }, docsContext, sourceContext),
isLoading,
previewProps: {
...props,
withSource: getSourceProps({ code: decodeURI(mdxSource) }, docsContext, sourceContext),
},
};
}
const childArray: ReactNodeArray = Array.isArray(children) ? children : [children];
const stories = childArray.filter(
const storyChildren = childArray.filter(
(c: ReactElement) => c.props && (c.props.id || c.props.name)
) as ReactElement[];
const targetIds = stories.map(
const targetIds = storyChildren.map(
(s) =>
s.props.id ||
toId(
mdxComponentMeta.id || mdxComponentMeta.title,
mdxComponentAnnotations.id || mdxComponentAnnotations.title,
storyNameFromExport(mdxStoryNameToKey[s.props.name])
)
);
const sourceProps = getSourceProps({ ids: targetIds }, docsContext, sourceContext);
if (!sourceState) sourceState = sourceProps.state;
const stories = useStories(targetIds, docsContext);
isLoading = stories.some((s) => !s);
return {
...props, // pass through columns etc.
withSource: sourceProps,
isExpanded: sourceState === SourceState.OPEN,
isLoading,
previewProps: {
...props, // pass through columns etc.
withSource: sourceProps,
isExpanded: sourceState === SourceState.OPEN,
},
};
};
export const Canvas: FC<CanvasProps> = (props) => {
const docsContext = useContext(DocsContext);
const sourceContext = useContext(SourceContext);
const previewProps = getPreviewProps(props, docsContext, sourceContext);
const { isLoading, previewProps } = getPreviewProps(props, docsContext, sourceContext);
const { children } = props;
if (isLoading) return <PreviewSkeleton />;
return (
<MDXProvider components={resetComponents}>
<PurePreview {...previewProps}>{children}</PurePreview>

View File

@ -31,12 +31,13 @@ const noDescription = (component?: Component): string | null => null;
export const getDescriptionProps = (
{ of, type, markdown, children }: DescriptionProps,
{ parameters }: DocsContextProps
{ id, storyById }: DocsContextProps<any>
): PureDescriptionProps => {
const { component, parameters } = storyById(id);
if (children || markdown) {
return { markdown: children || markdown };
}
const { component, notes, info, docs } = parameters;
const { notes, info, docs } = parameters;
const { extractComponentDescription = noDescription, description } = docs || {};
const target = of === CURRENT_SELECTION ? component : of;
@ -63,7 +64,7 @@ ${extractComponentDescription(target) || ''}
case DescriptionType.DOCGEN:
case DescriptionType.AUTO:
default:
return { markdown: extractComponentDescription(target, parameters) };
return { markdown: extractComponentDescription(target, { component, ...parameters }) };
}
};

View File

@ -5,6 +5,7 @@ import dedent from 'ts-dedent';
import { MDXProvider } from '@mdx-js/react';
import { ThemeProvider, ensure as ensureTheme } from '@storybook/theming';
import { DocsWrapper, DocsContent, components as htmlComponents } from '@storybook/components';
import { AnyFramework } from '@storybook/csf';
import { DocsContextProps, DocsContext } from './DocsContext';
import { anchorBlockIdFromId } from './Anchor';
import { storyBlockIdFromId } from './Story';
@ -14,8 +15,8 @@ import { scrollToElement } from './utils';
const { document, window: globalWindow } = global;
export interface DocsContainerProps {
context: DocsContextProps;
export interface DocsContainerProps<TFramework extends AnyFramework = AnyFramework> {
context: DocsContextProps<TFramework>;
}
const defaultComponents = {
@ -35,8 +36,10 @@ const warnOptionsTheme = deprecate(
);
export const DocsContainer: FunctionComponent<DocsContainerProps> = ({ context, children }) => {
const { id: storyId = null, parameters = {} } = context || {};
const { options = {}, docs = {} } = parameters;
const { id: storyId, storyById } = context;
const {
parameters: { options = {}, docs = {} },
} = storyById(storyId);
let themeVars = docs.theme;
if (!themeVars && options.theme) {
warnOptionsTheme();

View File

@ -1,22 +1,10 @@
import { Context, createContext } from 'react';
import { window as globalWindow } from 'global';
export interface DocsContextProps {
id?: string;
kind?: string;
name?: string;
import { DocsContextProps } from '@storybook/preview-web';
import { AnyFramework } from '@storybook/csf';
/**
* mdxStoryNameToKey is an MDX-compiler-generated mapping of an MDX story's
* display name to its story key for ID generation. It's used internally by the `<Story>`
* and `Preview` doc blocks.
*/
mdxStoryNameToKey?: Record<string, string>;
mdxComponentMeta?: any;
parameters?: any;
storyStore?: any;
forceRender?: () => void;
}
export type { DocsContextProps };
// We add DocsContext to window. The reason is that in case DocsContext.ts is
// imported multiple times (maybe once directly, and another time from a minified bundle)
@ -29,4 +17,4 @@ if (globalWindow.__DOCS_CONTEXT__ === undefined) {
globalWindow.__DOCS_CONTEXT__.displayName = 'DocsContext';
}
export const DocsContext: Context<DocsContextProps> = globalWindow.__DOCS_CONTEXT__;
export const DocsContext: Context<DocsContextProps<AnyFramework>> = globalWindow.__DOCS_CONTEXT__;

View File

@ -2,9 +2,8 @@ import { extractTitle } from './Title';
describe('defaultTitleSlot', () => {
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');
expect(extractTitle({ title: 'a/b/c' } as any)).toBe('c');
expect(extractTitle({ title: 'a|b' } as any)).toBe('a|b');
expect(extractTitle({ title: 'a/b/c.d' } as any)).toBe('c.d');
});
});

View File

@ -41,7 +41,7 @@ export const DocsStory: FunctionComponent<DocsStoryProps> = ({
{subheading && <Subheading>{subheading}</Subheading>}
{description && <Description markdown={description} />}
<Canvas withToolbar={withToolbar}>
<Story id={id} />
<Story id={id} parameters={parameters} />
</Canvas>
</Anchor>
);

View File

@ -1,17 +1,15 @@
import React, { FC, useContext } from 'react';
import global from 'global';
import { Args, BaseAnnotations, BaseMeta } from '@storybook/addons';
import { BaseAnnotations } from '@storybook/csf';
import { Anchor } from './Anchor';
import { DocsContext, DocsContextProps } from './DocsContext';
import { getDocsStories } from './utils';
import { Component } from './types';
const { document } = global;
type MetaProps = BaseMeta<Component> & BaseAnnotations<Args, any>;
type MetaProps = BaseAnnotations;
function getFirstStoryId(docsContext: DocsContextProps): string {
const stories = getDocsStories(docsContext);
const stories = docsContext.componentStories();
return stories.length > 0 ? stories[0].id : null;
}

View File

@ -1,18 +1,19 @@
import React, { useContext, FC } from 'react';
import { Story } from '@storybook/store';
import { DocsContext } from './DocsContext';
import { DocsStory } from './DocsStory';
import { getDocsStories } from './utils';
interface PrimaryProps {
name?: string;
}
export const Primary: FC<PrimaryProps> = ({ name }) => {
const context = useContext(DocsContext);
const componentStories = getDocsStories(context);
const { componentStories: getComponentStories } = useContext(DocsContext);
const componentStories = getComponentStories();
let story;
if (componentStories) {
story = name ? componentStories.find((s) => s.name === name) : componentStories[0];
story = name ? componentStories.find((s: Story<any>) => s.name === name) : componentStories[0];
}
return story ? <DocsStory {...story} expanded={false} withToolbar /> : null;
};

View File

@ -5,8 +5,7 @@ import {
SourceProps as PureSourceProps,
} from '@storybook/components';
import { StoryId } from '@storybook/api';
import { logger } from '@storybook/client-logger';
import { StoryContext } from '@storybook/addons';
import { Story } from '@storybook/store';
import { DocsContext, DocsContextProps } from './DocsContext';
import { SourceContext, SourceContextProps } from './SourceContainer';
@ -14,6 +13,7 @@ import { CURRENT_SELECTION } from './types';
import { SourceType } from '../shared';
import { enhanceSource } from './enhanceSource';
import { useStories } from './useStory';
export enum SourceState {
OPEN = 'open',
@ -43,28 +43,8 @@ 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 getSourceState = (storyIds: string[], docsContext: DocsContextProps) => {
const states = storyIds
.map((storyId) => {
const storyContext = getStoryContext(storyId, docsContext);
if (!storyContext) return null;
return storyContext.parameters.docs?.source?.state;
})
.filter(Boolean);
const getSourceState = (stories: Story[]) => {
const states = stories.map((story) => story.parameters.docs?.source?.state).filter(Boolean);
if (states.length === 0) return SourceState.CLOSED;
// FIXME: handling multiple stories is a pain
return states[0];
@ -77,34 +57,34 @@ const getStorySource = (storyId: StoryId, sourceContext: SourceContextProps): st
return sources?.[storyId] || '';
};
const getSnippet = (snippet: string, storyContext?: StoryContext): string => {
if (!storyContext) {
const getSnippet = (snippet: string, story?: Story<any>): string => {
if (!story) {
return snippet;
}
const { parameters } = storyContext;
const { parameters } = story;
// 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) {
if (userCode !== undefined) {
return userCode;
}
// if user has explicitly set this as dynamic, use snippet
if (type === SourceType.DYNAMIC) {
return parameters.docs?.transformSource?.(snippet, storyContext) || snippet;
return parameters.docs?.transformSource?.(snippet, story) || 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;
return parameters.docs?.transformSource?.(snippet, story) || snippet;
}
// otherwise, use the source code logic
const enhanced = enhanceSource(storyContext) || parameters;
const enhanced = enhanceSource(story) || parameters;
return enhanced?.docs?.source?.code || '';
};
@ -112,10 +92,11 @@ type SourceStateProps = { state: SourceState };
export const getSourceProps = (
props: SourceProps,
docsContext: DocsContextProps,
docsContext: DocsContextProps<any>,
sourceContext: SourceContextProps
): PureSourceProps & SourceStateProps => {
const { id: currentId, parameters = {} } = docsContext;
const { id: currentId, storyById } = docsContext;
const { parameters } = storyById(currentId);
const codeProps = props as CodeProps;
const singleProps = props as SingleSourceProps;
@ -123,21 +104,27 @@ export const getSourceProps = (
let source = codeProps.code; // prefer user-specified code
const targetId =
singleProps.id === CURRENT_SELECTION || !singleProps.id ? currentId : singleProps.id;
const targetIds = multiProps.ids || [targetId];
const targetIds = multiProps.ids || [singleProps.id || currentId];
const storyIds = targetIds.map((targetId) =>
targetId === CURRENT_SELECTION ? currentId : targetId
);
const stories = useStories(storyIds, docsContext);
if (!stories.every(Boolean)) {
return { error: SourceError.SOURCE_UNAVAILABLE, state: SourceState.NONE };
}
if (!source) {
source = targetIds
.map((storyId) => {
source = storyIds
.map((storyId, idx) => {
const storySource = getStorySource(storyId, sourceContext);
const storyContext = getStoryContext(storyId, docsContext);
return getSnippet(storySource, storyContext);
const storyObj = stories[idx] as Story;
return getSnippet(storySource, storyObj);
})
.join('\n\n');
}
const state = getSourceState(targetIds, docsContext);
const state = getSourceState(stories as Story[]);
const { docs: docsParameters = {} } = parameters;
const { source: sourceParameters = {} } = docsParameters;

View File

@ -18,24 +18,21 @@ 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);
}
const handleSnippetRendered = (id: StoryId, newItem: SourceItem) => {
if (newItem !== sources[id]) {
setSources((current) => {
const newSources = { ...current, [id]: newItem };
if (!deepEqual(current, newSources)) {
return newSources;
}
return current;
});
}
};
channel.on(SNIPPET_RENDERED, handleSnippetRendered);
return () => channel.off(SNIPPET_RENDERED, handleSnippetRendered);
});

View File

@ -2,7 +2,6 @@ import React, { useContext, FunctionComponent } from 'react';
import { DocsContext } from './DocsContext';
import { DocsStory } from './DocsStory';
import { Heading } from './Heading';
import { getDocsStories } from './utils';
import { DocsStoryProps } from './types';
interface StoriesProps {
@ -11,10 +10,10 @@ interface StoriesProps {
}
export const Stories: FunctionComponent<StoriesProps> = ({ title, includePrimary = false }) => {
const context = useContext(DocsContext);
const componentStories = getDocsStories(context);
const { componentStories } = useContext(DocsContext);
let stories: DocsStoryProps[] = componentStories;
let stories: DocsStoryProps[] = componentStories();
stories = stories.filter((story) => !story.parameters?.docs?.disable);
if (!includePrimary) stories = stories.slice(1);
if (!stories || stories.length === 0) {

View File

@ -1,17 +1,30 @@
import React, { FunctionComponent, ReactNode, ElementType, ComponentProps } from 'react';
import React, {
FunctionComponent,
ReactNode,
ElementType,
ComponentProps,
useContext,
useRef,
useEffect,
useState,
} from 'react';
import { MDXProvider } from '@mdx-js/react';
import { resetComponents, Story as PureStory } from '@storybook/components';
import { toId, storyNameFromExport } from '@storybook/csf';
import { Args, BaseAnnotations } from '@storybook/addons';
import { CURRENT_SELECTION } from './types';
import global from 'global';
import { resetComponents, Story as PureStory, StorySkeleton } from '@storybook/components';
import { StoryId, toId, storyNameFromExport, StoryAnnotations, AnyFramework } from '@storybook/csf';
import { Story as StoryType } from '@storybook/store';
import { addons } from '@storybook/addons';
import Events from '@storybook/core-events';
import { CURRENT_SELECTION } from './types';
import { DocsContext, DocsContextProps } from './DocsContext';
import { useStory } from './useStory';
export const storyBlockIdFromId = (storyId: string) => `story--${storyId}`;
type PureStoryProps = ComponentProps<typeof PureStory>;
type CommonProps = BaseAnnotations<Args, any> & {
type CommonProps = StoryAnnotations & {
height?: string;
inline?: boolean;
};
@ -34,22 +47,27 @@ export type StoryProps = (StoryDefProps | StoryRefProps | StoryImportProps) & Co
export const lookupStoryId = (
storyName: string,
{ mdxStoryNameToKey, mdxComponentMeta }: DocsContextProps
{ mdxStoryNameToKey, mdxComponentAnnotations }: DocsContextProps
) =>
toId(
mdxComponentMeta.id || mdxComponentMeta.title,
mdxComponentAnnotations.id || mdxComponentAnnotations.title,
storyNameFromExport(mdxStoryNameToKey[storyName])
);
export const getStoryProps = (props: StoryProps, context: DocsContextProps): PureStoryProps => {
export const getStoryId = (props: StoryProps, context: DocsContextProps): StoryId => {
const { id } = props as StoryRefProps;
const { name } = props as StoryDefProps;
const inputId = id === CURRENT_SELECTION ? context.id : id;
const previewId = inputId || lookupStoryId(name, context);
const data = context.storyStore.fromId(previewId) || {};
return inputId || lookupStoryId(name, context);
};
const { height, inline } = props;
const { storyFn = undefined, name: storyName = undefined, parameters = {} } = data;
export const getStoryProps = <TFramework extends AnyFramework>(
{ height, inline }: StoryProps,
story: StoryType<TFramework>,
context: DocsContextProps<TFramework>,
onStoryFnCalled: () => void
): PureStoryProps => {
const { name: storyName, parameters } = story;
const { docs = {} } = parameters;
if (docs.disable) {
@ -65,33 +83,137 @@ export const getStoryProps = (props: StoryProps, context: DocsContextProps): Pur
);
}
const boundStoryFn = () => {
const storyResult = story.unboundStoryFn({
...context.getStoryContext(story),
loaded: {},
abortSignal: undefined,
canvasElement: undefined,
});
// We need to wait until the bound story function has actually been called before we
// consider the story rendered. Certain frameworks (i.e. angular) don't actually render
// the component in the very first react render cycle, and so we can't just wait until the
// `PureStory` component has been rendered to consider the underlying story "rendered".
onStoryFnCalled();
return storyResult;
};
return {
parameters,
inline: storyIsInline,
id: previewId,
storyFn: prepareForInline && storyFn ? () => prepareForInline(storyFn, data) : storyFn,
id: story.id,
height: height || (storyIsInline ? undefined : iframeHeight),
title: storyName,
...(storyIsInline && {
parameters,
storyFn: () => prepareForInline(boundStoryFn, context.getStoryContext(story)),
}),
};
};
const Story: FunctionComponent<StoryProps> = (props) => (
<DocsContext.Consumer>
{(context) => {
const storyProps = getStoryProps(props, context);
if (!storyProps) {
return null;
}
function makeGate(): [Promise<void>, () => void] {
let open;
const gate = new Promise<void>((r) => {
open = r;
});
return [gate, open];
}
const Story: FunctionComponent<StoryProps> = (props) => {
const context = useContext(DocsContext);
const channel = addons.getChannel();
const storyRef = useRef();
const storyId = getStoryId(props, context);
const story = useStory(storyId, context);
const [showLoader, setShowLoader] = useState(true);
useEffect(() => {
let cleanup: () => void;
if (story && storyRef.current) {
const { componentId, id, title, name } = story;
const renderContext = {
componentId,
title,
kind: title,
id,
name,
story: name,
// TODO what to do when these fail?
showMain: () => {},
showError: () => {},
showException: () => {},
};
cleanup = context.renderStoryToElement({
story,
renderContext,
element: storyRef.current as HTMLElement,
viewMode: 'docs',
});
setShowLoader(false);
}
return () => cleanup && cleanup();
}, [story]);
const [storyFnRan, onStoryFnRan] = makeGate();
const [rendered, onRendered] = makeGate();
useEffect(onRendered);
if (!story) {
return <StorySkeleton />;
}
const storyProps = getStoryProps(props, story, context, onStoryFnRan);
if (!storyProps) {
return null;
}
if (storyProps.inline) {
// If we are rendering a old-style inline Story via `PureStory` below, we want to emit
// the `STORY_RENDERED` event when it renders. The modern mode below calls out to
// `Preview.renderStoryToDom()` which itself emits the event.
if (!global?.FEATURES?.modernInlineRender) {
// We need to wait for two things before we can consider the story rendered:
// (a) React's `useEffect` hook needs to fire. This is needed for React stories, as
// decorators of the form `<A><B/></A>` will not actually execute `B` in the first
// call to the story function.
// (b) The story function needs to actually have been called.
// Certain frameworks (i.e.angular) don't actually render the component in the very first
// React render cycle, so we need to wait for the framework to actually do that
Promise.all([storyFnRan, rendered]).then(() => {
channel.emit(Events.STORY_RENDERED, storyId);
});
} else {
// We do this so React doesn't complain when we replace the span in a secondary render
const htmlContents = `<span></span>`;
// FIXME: height/style/etc. lifted from PureStory
const { height } = storyProps;
return (
<div id={storyBlockIdFromId(storyProps.id)}>
<div id={storyBlockIdFromId(story.id)}>
<MDXProvider components={resetComponents}>
<PureStory {...storyProps} />
{height ? (
<style>{`#story--${story.id} { min-height: ${height}; transform: translateZ(0); overflow: auto }`}</style>
) : null}
{showLoader && <StorySkeleton />}
<div
ref={storyRef}
data-name={story.name}
dangerouslySetInnerHTML={{ __html: htmlContents }}
/>
</MDXProvider>
</div>
);
}}
</DocsContext.Consumer>
);
}
}
return (
<div id={storyBlockIdFromId(story.id)}>
<MDXProvider components={resetComponents}>
<PureStory {...storyProps} />
</MDXProvider>
</div>
);
};
Story.defaultProps = {
children: null,

View File

@ -7,8 +7,8 @@ interface SubtitleProps {
}
export const Subtitle: FunctionComponent<SubtitleProps> = ({ children }) => {
const context = useContext(DocsContext);
const { parameters } = context;
const { id, storyById } = useContext(DocsContext);
const { parameters } = storyById(id);
let text: JSX.Element | string = children;
if (!text) {
text = parameters?.componentSubtitle;

View File

@ -8,9 +8,9 @@ interface TitleProps {
const STORY_KIND_PATH_SEPARATOR = /\s*\/\s*/;
export const extractTitle = ({ kind }: DocsContextProps) => {
const groups = kind.trim().split(STORY_KIND_PATH_SEPARATOR);
return (groups && groups[groups.length - 1]) || kind;
export const extractTitle = ({ title }: DocsContextProps) => {
const groups = title.trim().split(STORY_KIND_PATH_SEPARATOR);
return (groups && groups[groups.length - 1]) || title;
};
export const Title: FunctionComponent<TitleProps> = ({ children }) => {

View File

@ -1,5 +1,5 @@
import { combineParameters } from '@storybook/client-api';
import { StoryContext, Parameters } from '@storybook/addons';
import { Parameters } from '@storybook/addons';
import { Story, combineParameters } from '@storybook/store';
// ============================================================
// START @storybook/source-loader/extract-source
@ -76,8 +76,8 @@ const extract = (targetId: string, { source, locationsMap }: StorySource) => {
return extractSource(location, lines);
};
export const enhanceSource = (context: StoryContext): Parameters => {
const { id, parameters } = context;
export const enhanceSource = (story: Story<any>): Parameters => {
const { id, parameters } = story;
const { storySource, docs = {} } = parameters;
const { transformSource } = docs;
@ -87,7 +87,7 @@ export const enhanceSource = (context: StoryContext): Parameters => {
}
const input = extract(id, storySource);
const code = transformSource ? transformSource(input, context) : input;
const code = transformSource ? transformSource(input, story) : input;
return { docs: combineParameters(docs, { source: { code } }) };
};

View File

@ -0,0 +1,44 @@
import { useState, useEffect } from 'react';
import { StoryId, AnyFramework } from '@storybook/csf';
import { Story } from '@storybook/store';
import { DocsContextProps } from './DocsContext';
export function useStory<TFramework extends AnyFramework = AnyFramework>(
storyId: StoryId,
context: DocsContextProps<TFramework>
): Story<TFramework> | void {
const stories = useStories([storyId], context);
return stories && stories[0];
}
export function useStories<TFramework extends AnyFramework = AnyFramework>(
storyIds: StoryId[],
context: DocsContextProps<TFramework>
): (Story<TFramework> | void)[] {
const initialStoriesById = context.componentStories().reduce((acc, story) => {
acc[story.id] = story;
return acc;
}, {} as Record<StoryId, Story<TFramework>>);
const [storiesById, setStories] = useState(initialStoriesById as typeof initialStoriesById);
useEffect(() => {
Promise.all(
storyIds.map(async (storyId) => {
// loadStory will be called every single time useStory is called
// because useEffect does not use storyIds as an input. This is because
// HMR can change the story even when the storyId hasn't changed. However, it
// will be a no-op once the story has loaded. Furthermore, the `story` will
// have an exact equality when the story hasn't changed, so it won't trigger
// any unnecessary re-renders
const story = await context.loadStory(storyId);
setStories((current) =>
current[storyId] === story ? current : { ...current, [storyId]: story }
);
})
);
});
return storyIds.map((storyId) => storiesById[storyId]);
}

View File

@ -1,18 +1,5 @@
/* eslint-disable no-underscore-dangle */
import { DocsContextProps } from './DocsContext';
import { StoryData, Component } from './types';
export const getDocsStories = (context: DocsContextProps): StoryData[] => {
const { storyStore, kind } = context;
if (!storyStore) {
return [];
}
return storyStore
.getStoriesForKind(kind)
.filter((s: any) => !(s.parameters && s.parameters.docs && s.parameters.docs.disable));
};
import { Component } from './types';
const titleCase = (str: string): string =>
str

View File

@ -8,6 +8,9 @@ Object {
"name": "_inputValue",
"table": Object {
"category": "properties",
"defaultValue": Object {
"summary": "some value",
},
"type": Object {
"required": true,
"summary": "string",
@ -24,6 +27,9 @@ Private value.",
"name": "_value",
"table": Object {
"category": "properties",
"defaultValue": Object {
"summary": "Private hello",
},
"type": Object {
"required": true,
"summary": "string",
@ -35,10 +41,14 @@ Private value.",
},
"accent": Object {
"defaultValue": undefined,
"description": "Specify the accent-type of the button",
"description": "
Specify the accent-type of the button",
"name": "accent",
"table": Object {
"category": "inputs",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": true,
"summary": "ButtonAccent",
@ -50,10 +60,14 @@ Private value.",
},
"appearance": Object {
"defaultValue": "secondary",
"description": "Appearance style of the button.",
"description": "
Appearance style of the button.",
"name": "appearance",
"table": Object {
"category": "inputs",
"defaultValue": Object {
"summary": "secondary",
},
"type": Object {
"required": true,
"summary": "\\"primary\\" | \\"secondary\\"",
@ -73,6 +87,9 @@ Private value.",
"name": "buttonRef",
"table": Object {
"category": "view child",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": true,
"summary": "ElementRef",
@ -92,6 +109,9 @@ An internal calculation method which adds \`x\` and \`y\` together.
"name": "calc",
"table": Object {
"category": "methods",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": false,
"summary": "(x: number, y: string | number) => number",
@ -107,6 +127,9 @@ An internal calculation method which adds \`x\` and \`y\` together.
"name": "focus",
"table": Object {
"category": "properties",
"defaultValue": Object {
"summary": false,
},
"type": Object {
"required": true,
"summary": "",
@ -118,10 +141,14 @@ An internal calculation method which adds \`x\` and \`y\` together.
},
"inputValue": Object {
"defaultValue": undefined,
"description": "Setter for \`inputValue\` that is also an \`@Input\`.",
"description": "
Setter for \`inputValue\` that is also an \`@Input\`.",
"name": "inputValue",
"table": Object {
"category": "inputs",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": true,
"summary": "string",
@ -138,6 +165,9 @@ Public value.",
"name": "internalProperty",
"table": Object {
"category": "properties",
"defaultValue": Object {
"summary": "Public hello",
},
"type": Object {
"required": true,
"summary": "string",
@ -149,10 +179,14 @@ Public value.",
},
"isDisabled": Object {
"defaultValue": false,
"description": "Sets the button to a disabled state.",
"description": "
Sets the button to a disabled state.",
"name": "isDisabled",
"table": Object {
"category": "inputs",
"defaultValue": Object {
"summary": false,
},
"type": Object {
"required": true,
"summary": "boolean",
@ -168,6 +202,9 @@ Public value.",
"name": "item",
"table": Object {
"category": "inputs",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": true,
"summary": "[]",
@ -179,10 +216,17 @@ Public value.",
},
"label": Object {
"defaultValue": undefined,
"description": "The inner text of the button.",
"description": "
The inner text of the button.
",
"name": "label",
"table": Object {
"category": "inputs",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": true,
"summary": "string",
@ -204,6 +248,9 @@ Will also block the emission of the event if \`isDisabled\` is true.
"name": "onClick",
"table": Object {
"category": "outputs",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": true,
"summary": "EventEmitter",
@ -219,6 +266,9 @@ Will also block the emission of the event if \`isDisabled\` is true.
"name": "onClickListener",
"table": Object {
"category": "methods",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": false,
"summary": "(btn: ) => void",
@ -238,6 +288,9 @@ A private method.
"name": "privateMethod",
"table": Object {
"category": "methods",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": false,
"summary": "(password: string) => void",
@ -253,6 +306,9 @@ A private method.
"name": "processedItem",
"table": Object {
"category": "properties",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": true,
"summary": "T[]",
@ -272,6 +328,9 @@ A protected method.
"name": "protectedMethod",
"table": Object {
"category": "methods",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": false,
"summary": "(id?: number) => void",
@ -288,6 +347,9 @@ A public method using an interface.",
"name": "publicMethod",
"table": Object {
"category": "methods",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": false,
"summary": "(things: ISomeInterface) => void",
@ -303,6 +365,9 @@ A public method using an interface.",
"name": "showKeyAlias",
"table": Object {
"category": "inputs",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": true,
"summary": "",
@ -314,10 +379,14 @@ A public method using an interface.",
},
"size": Object {
"defaultValue": "medium",
"description": "Size of the button.",
"description": "
Size of the button.",
"name": "size",
"table": Object {
"category": "inputs",
"defaultValue": Object {
"summary": "medium",
},
"type": Object {
"required": true,
"summary": "ButtonSize",
@ -329,10 +398,14 @@ A public method using an interface.",
},
"someDataObject": Object {
"defaultValue": undefined,
"description": "Specifies some arbitrary object",
"description": "
Specifies some arbitrary object",
"name": "someDataObject",
"table": Object {
"category": "inputs",
"defaultValue": Object {
"summary": undefined,
},
"type": Object {
"required": true,
"summary": "ISomeInterface",
@ -344,10 +417,17 @@ A public method using an interface.",
},
"somethingYouShouldNotUse": Object {
"defaultValue": false,
"description": "Some input you shouldn't use.",
"description": "
Some input you shouldn't use.
",
"name": "somethingYouShouldNotUse",
"table": Object {
"category": "inputs",
"defaultValue": Object {
"summary": false,
},
"type": Object {
"required": true,
"summary": "boolean",

View File

@ -163,7 +163,7 @@ like <strong>bold</strong>, <em>italic</em>, and <code>inline code</code>.</p>
"name": "click",
},
],
"id": "component-InputComponent-568feeafa68e593b062061c27c4625a9",
"id": "component-InputComponent-fd2eff3e4da750f1c06d4928670993b3",
"inputs": Array [],
"inputsClass": Array [
Object {
@ -223,18 +223,18 @@ like <strong>bold</strong>, <em>italic</em>, and <code>inline code</code>.</p>
"jsdoctags": Array [
Object {
"comment": "",
"end": 1525,
"end": 1590,
"flags": 4227072,
"kind": 317,
"modifierFlagsCache": 0,
"pos": 1512,
"pos": 1576,
"tagName": Object {
"end": 1521,
"end": 1585,
"escapedText": "required",
"flags": 4227072,
"kind": 78,
"modifierFlagsCache": 0,
"pos": 1513,
"pos": 1577,
"transformFlags": 0,
},
"transformFlags": 0,
@ -282,18 +282,18 @@ like <strong>bold</strong>, <em>italic</em>, and <code>inline code</code>.</p>
"jsdoctags": Array [
Object {
"comment": "",
"end": 1802,
"end": 1882,
"flags": 4227072,
"kind": 321,
"modifierFlagsCache": 0,
"pos": 1787,
"pos": 1866,
"tagName": Object {
"end": 1798,
"end": 1877,
"escapedText": "deprecated",
"flags": 4227072,
"kind": 78,
"modifierFlagsCache": 0,
"pos": 1788,
"pos": 1867,
"transformFlags": 0,
},
"transformFlags": 0,
@ -332,21 +332,21 @@ like <strong>bold</strong>, <em>italic</em>, and <code>inline code</code>.</p>
"deprecated": false,
"deprecationMessage": "",
"name": Object {
"end": 3518,
"end": 3678,
"escapedText": "x",
"flags": 4227072,
"kind": 78,
"modifierFlagsCache": 0,
"pos": 3517,
"pos": 3677,
"transformFlags": 0,
},
"tagName": Object {
"end": 3516,
"end": 3676,
"escapedText": "param",
"flags": 4227072,
"kind": 78,
"modifierFlagsCache": 0,
"pos": 3511,
"pos": 3671,
"transformFlags": 0,
},
"type": "number",
@ -357,21 +357,21 @@ like <strong>bold</strong>, <em>italic</em>, and <code>inline code</code>.</p>
"deprecated": false,
"deprecationMessage": "",
"name": Object {
"end": 3563,
"end": 3724,
"escapedText": "y",
"flags": 4227072,
"kind": 78,
"modifierFlagsCache": 0,
"pos": 3562,
"pos": 3723,
"transformFlags": 0,
},
"tagName": Object {
"end": 3561,
"end": 3722,
"escapedText": "param",
"flags": 4227072,
"kind": 78,
"modifierFlagsCache": 0,
"pos": 3556,
"pos": 3717,
"transformFlags": 0,
},
"type": "string | number",
@ -445,21 +445,21 @@ An internal calculation method which adds \`x\` and \`y\` together.
"deprecated": false,
"deprecationMessage": "",
"name": Object {
"end": 4079,
"end": 4263,
"escapedText": "password",
"flags": 4227072,
"kind": 78,
"modifierFlagsCache": 0,
"pos": 4071,
"pos": 4255,
"transformFlags": 0,
},
"tagName": Object {
"end": 4070,
"end": 4254,
"escapedText": "param",
"flags": 4227072,
"kind": 78,
"modifierFlagsCache": 0,
"pos": 4065,
"pos": 4249,
"transformFlags": 0,
},
"type": "string",
@ -500,22 +500,22 @@ A private method.
"deprecated": false,
"deprecationMessage": "",
"name": Object {
"end": 3938,
"end": 4113,
"escapedText": "id",
"flags": 4227072,
"kind": 78,
"modifierFlagsCache": 0,
"pos": 3936,
"pos": 4111,
"transformFlags": 0,
},
"optional": true,
"tagName": Object {
"end": 3935,
"end": 4110,
"escapedText": "param",
"flags": 4227072,
"kind": 78,
"modifierFlagsCache": 0,
"pos": 3930,
"pos": 4105,
"transformFlags": 0,
},
"type": "number",
@ -945,7 +945,7 @@ export class InputComponent<T> {
"deprecated": false,
"deprecationMessage": "",
"file": "addons/docs/src/frameworks/angular/__testfixtures__/doc-button/input.ts",
"id": "interface-ISomeInterface-568feeafa68e593b062061c27c4625a9",
"id": "interface-ISomeInterface-fd2eff3e4da750f1c06d4928670993b3",
"indexSignatures": Array [],
"kind": 163,
"methods": Array [],

View File

@ -3,5 +3,7 @@
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts"]
}
"include": [
"./*.ts"
]
}

View File

@ -6,6 +6,8 @@ import { sync as spawnSync } from 'cross-spawn';
import { findComponentByName, extractArgTypesFromData } from './compodoc';
const { SNAPSHOT_OS } = global;
// File hierarchy: __testfixtures__ / some-test-case / input.*
const inputRegExp = /^input\..*$/;
@ -35,14 +37,15 @@ describe('angular component properties', () => {
const testDir = path.join(fixturesDir, testEntry.name);
const testFile = fs.readdirSync(testDir).find((fileName) => inputRegExp.test(fileName));
if (testFile) {
// eslint-disable-next-line jest/valid-title
it(testEntry.name, () => {
const inputPath = path.join(testDir, testFile);
// snapshot the output of compodoc
const compodocOutput = runCompodoc(inputPath);
const compodocJson = JSON.parse(compodocOutput);
expect(compodocJson).toMatchSpecificSnapshot(path.join(testDir, 'compodoc.snapshot'));
expect(compodocJson).toMatchSpecificSnapshot(
path.join(testDir, `compodoc-${SNAPSHOT_OS}.snapshot`)
);
// snapshot the output of addon-docs angular-properties
const componentData = findComponentByName('InputComponent', compodocJson);

View File

@ -13,6 +13,7 @@ import {
Pipe,
Property,
Directive,
JsDocTag,
} from './types';
export const isMethod = (methodOrProp: Method | Property): methodOrProp is Method => {
@ -42,7 +43,7 @@ export const checkValidCompodocJson = (compodocJson: CompodocJson) => {
const hasDecorator = (item: Property, decoratorName: string) =>
item.decorators && item.decorators.find((x: any) => x.name === decoratorName);
const mapPropertyToSection = (key: string, item: Property) => {
const mapPropertyToSection = (item: Property) => {
if (hasDecorator(item, 'ViewChild')) {
return 'view child';
}
@ -72,7 +73,7 @@ const mapItemToSection = (key: string, item: Method | Property): string => {
if (isMethod(item)) {
throw new Error("Cannot be of type Method if key === 'propertiesClass'");
}
return mapPropertyToSection(key, item);
return mapPropertyToSection(item);
default:
throw new Error(`Unknown key: ${key}`);
}
@ -119,7 +120,7 @@ const extractTypeFromValue = (defaultValue: any) => {
const extractEnumValues = (compodocType: any) => {
const compodocJson = getCompodocJson();
const enumType = compodocJson?.miscellaneous.enumerations.find((x) => x.name === compodocType);
const enumType = compodocJson?.miscellaneous?.enumerations?.find((x) => x.name === compodocType);
if (enumType?.childs.every((x) => x.value)) {
return enumType.childs.map((x) => x.value);
@ -154,10 +155,59 @@ export const extractType = (property: Property, defaultValue: any) => {
}
};
const castDefaultValue = (property: Property, defaultValue: any) => {
const compodocType = property.type;
// All these checks are necessary as compodoc does not always set the type ie. @HostBinding have empty types.
// null and undefined also have 'any' type
if (['boolean', 'number', 'string', 'EventEmitter'].includes(compodocType)) {
switch (compodocType) {
case 'boolean':
return defaultValue === 'true';
case 'number':
return Number(defaultValue);
case 'EventEmitter':
return undefined;
default:
return defaultValue;
}
} else {
switch (defaultValue) {
case 'true':
return true;
case 'false':
return false;
case 'null':
return null;
case 'undefined':
return undefined;
default:
return defaultValue;
}
}
};
const extractDefaultValueFromComments = (property: Property, value: any) => {
let commentValue = value;
property.jsdoctags.forEach((tag: JsDocTag) => {
if (['default', 'defaultvalue'].includes(tag.tagName.escapedText)) {
// @ts-ignore
const dom = new window.DOMParser().parseFromString(tag.comment, 'text/html');
commentValue = dom.body.textContent;
}
});
return commentValue;
};
const extractDefaultValue = (property: Property) => {
try {
// eslint-disable-next-line no-eval
const value = eval(property.defaultValue);
let value: string | boolean = property.defaultValue?.replace(/^'(.*)'$/, '$1');
value = castDefaultValue(property, value);
if (value == null && property.jsdoctags?.length > 0) {
value = extractDefaultValueFromComments(property, value);
}
return value;
} catch (err) {
logger.debug(`Error extracting ${property.name}: ${property.defaultValue}`);
@ -167,7 +217,7 @@ const extractDefaultValue = (property: Property) => {
const resolveTypealias = (compodocType: string): string => {
const compodocJson = getCompodocJson();
const typeAlias = compodocJson?.miscellaneous.typealiases.find((x) => x.name === compodocType);
const typeAlias = compodocJson?.miscellaneous?.typealiases?.find((x) => x.name === compodocType);
return typeAlias ? resolveTypealias(typeAlias.rawtype) : compodocType;
};
@ -189,11 +239,13 @@ export const extractArgTypesFromData = (componentData: Class | Directive | Injec
data.forEach((item: Method | Property) => {
const section = mapItemToSection(key, item);
const defaultValue = isMethod(item) ? undefined : extractDefaultValue(item as Property);
const type =
isMethod(item) || (section !== 'inputs' && section !== 'properties')
? { name: 'void' }
: extractType(item as Property, defaultValue);
const action = section === 'outputs' ? { action: item.name } : {};
const argType = {
name: item.name,
description: item.rawdescription || item.description,
@ -206,6 +258,7 @@ export const extractArgTypesFromData = (componentData: Class | Directive | Injec
summary: isMethod(item) ? displaySignature(item) : item.type,
required: isMethod(item) ? false : !item.optional,
},
defaultValue: { summary: defaultValue },
},
};

View File

@ -2,31 +2,42 @@ import React from 'react';
import pLimit from 'p-limit';
import { nanoid } from 'nanoid';
import { IStory, StoryContext } from '@storybook/angular';
import { AngularFramework, StoryContext } from '@storybook/angular';
import { rendererFactory } from '@storybook/angular/renderer';
import { StoryFn } from '@storybook/addons';
import { PartialStoryFn } from '@storybook/csf';
const limit = pLimit(1);
/**
* Uses the angular renderer to generate a story. Uses p-limit to run synchronously
*/
export const prepareForInline = (storyFn: StoryFn<IStory>, { id, parameters }: StoryContext) => {
return React.createElement('div', {
ref: async (node?: HTMLDivElement): Promise<void> => {
if (!node) {
return null;
}
export const prepareForInline = (
storyFn: PartialStoryFn<AngularFramework>,
{ id, parameters, component }: StoryContext
) => {
const el = React.useRef();
return limit(async () => {
const renderer = await rendererFactory.getRendererInstance(`${id}-${nanoid(10)}`, node);
await renderer.render({
forced: false,
parameters,
storyFnAngular: storyFn(),
targetDOMNode: node,
});
React.useEffect(() => {
(async () => {
limit(async () => {
const renderer = await rendererFactory.getRendererInstance(
`${id}-${nanoid(10)}`.toLowerCase(),
el.current
);
if (renderer) {
await renderer.render({
forced: false,
component,
parameters,
storyFnAngular: storyFn(),
targetDOMNode: el.current,
});
}
});
},
})();
});
return React.createElement('div', {
ref: el,
});
};

View File

@ -1,8 +1,7 @@
import { addons, StoryContext, StoryFn } from '@storybook/addons';
import { IStory } from '@storybook/angular';
import { addons, useEffect } from '@storybook/addons';
import { PartialStoryFn } from '@storybook/csf';
import { StoryContext, AngularFramework } from '@storybook/angular';
import { computesTemplateSourceFromComponent } from '@storybook/angular/renderer';
import prettierHtml from 'prettier/parser-html';
import prettier from 'prettier/standalone';
import { SNIPPET_RENDERED, SourceType } from '../../shared';
export const skipSourceRender = (context: StoryContext) => {
@ -17,46 +16,66 @@ export const skipSourceRender = (context: StoryContext) => {
return sourceParams?.code || sourceParams?.type === SourceType.CODE;
};
const prettyUp = (source: string) => {
return prettier.format(source, {
parser: 'angular',
plugins: [prettierHtml],
htmlWhitespaceSensitivity: 'ignore',
});
let prettyUpInternal: (source: string) => string | undefined;
const makePrettyUp = async () => {
if (prettyUpInternal) {
return prettyUpInternal;
}
const prettierHtml = await import('prettier/parser-html');
const prettier = await import('prettier/standalone');
prettyUpInternal = (source: string) => {
return prettier.format(source, {
parser: 'angular',
plugins: [prettierHtml],
htmlWhitespaceSensitivity: 'ignore',
});
};
return prettyUpInternal;
};
/**
* Svelte source decorator.
* Angular source decorator.
* @param storyFn Fn
* @param context StoryContext
*/
export const sourceDecorator = (storyFn: StoryFn<IStory>, context: StoryContext) => {
export const sourceDecorator = (
storyFn: PartialStoryFn<AngularFramework>,
context: StoryContext
) => {
const story = storyFn();
if (skipSourceRender(context)) {
return story;
}
const channel = addons.getChannel();
const { props, template } = story;
const { props, template, userDefinedTemplate } = story;
const {
parameters: { component, argTypes },
} = context;
const { component, argTypes } = context;
if (component) {
const source = computesTemplateSourceFromComponent(component, props, argTypes);
let toEmit: string;
const prettyUpPromise = makePrettyUp();
// We might have a story with a Directive or Service defined as the component
// In these cases there might exist a template, even if we aren't able to create source from component
if (source || template) {
channel.emit(SNIPPET_RENDERED, context.id, prettyUp(source || template));
useEffect(() => {
prettyUpPromise.then((prettyUp) => {
if (toEmit) channel.emit(SNIPPET_RENDERED, context.id, prettyUp(toEmit));
});
});
prettyUpPromise.then((prettyUp) => {
if (component && !userDefinedTemplate) {
const source = computesTemplateSourceFromComponent(component, props, argTypes);
// We might have a story with a Directive or Service defined as the component
// In these cases there might exist a template, even if we aren't able to create source from component
if (source || template) {
toEmit = prettyUp(source || template);
}
} else if (template) {
toEmit = prettyUp(template);
}
return story;
}
if (template) {
channel.emit(SNIPPET_RENDERED, context.id, prettyUp(template));
return story;
}
});
return story;
};

View File

@ -7,6 +7,13 @@ export interface Method {
rawdescription?: string;
}
export interface JsDocTag {
comment?: string;
tagName?: {
escapedText?: string;
};
}
export interface Property {
name: string;
decorators?: Decorator[];
@ -15,6 +22,7 @@ export interface Property {
defaultValue?: string;
description?: string;
rawdescription?: string;
jsdoctags?: JsDocTag[];
}
export interface Class {

View File

@ -1,11 +1,10 @@
import { DocsContainer, DocsPage } from '../../blocks';
import { enhanceArgTypes } from './enhanceArgTypes';
export const parameters = {
docs: {
inlineStories: false,
container: DocsContainer,
page: DocsPage,
getContainer: async () => (await import('../../blocks')).DocsContainer,
getPage: async () => (await import('../../blocks')).DocsPage,
iframeHeight: 100,
},
};

View File

@ -1,4 +1,5 @@
import { ArgType, ArgTypes } from '@storybook/api';
import { ArgTypes } from '@storybook/api';
import { StrictInputType } from '@storybook/csf';
import { enhanceArgTypes } from './enhanceArgTypes';
expect.addSnapshotSerializer({
@ -12,30 +13,28 @@ const enhance = ({
extractedArgTypes,
isArgsStory = true,
}: {
argType?: ArgType;
argType?: StrictInputType;
arg?: any;
extractedArgTypes?: ArgTypes;
isArgsStory?: boolean;
}) => {
const context = {
id: 'foo--bar',
componentId: 'foo',
title: 'foo',
kind: 'foo',
id: 'foo--bar',
name: 'bar',
story: 'bar',
component: 'dummy',
parameters: {
component: 'dummy',
__isArgsStory: isArgsStory,
docs: {
extractArgTypes: extractedArgTypes && (() => extractedArgTypes),
},
argTypes: argType && {
input: argType,
},
args: {
input: arg,
},
},
args: {},
argTypes: {},
argTypes: argType && { input: argType },
initialArgs: { input: arg },
args: { input: arg },
globals: {},
};
return enhanceArgTypes(context);
@ -46,7 +45,7 @@ describe('enhanceArgTypes', () => {
it('should no-op', () => {
expect(
enhance({
argType: { foo: 'unmodified', type: { name: 'number' } },
argType: { name: 'input', foo: 'unmodified', type: { name: 'number' } },
isArgsStory: false,
}).input
).toMatchInlineSnapshot(`
@ -66,7 +65,7 @@ describe('enhanceArgTypes', () => {
it('number', () => {
expect(
enhance({
argType: { type: { name: 'number' } },
argType: { name: 'input', type: { name: 'number' } },
}).input
).toMatchInlineSnapshot(`
{
@ -99,7 +98,7 @@ describe('enhanceArgTypes', () => {
it('range', () => {
expect(
enhance({
argType: { control: { type: 'range', min: 0, max: 100 } },
argType: { name: 'input', control: { type: 'range', min: 0, max: 100 } },
}).input
).toMatchInlineSnapshot(`
{
@ -115,7 +114,7 @@ describe('enhanceArgTypes', () => {
it('options', () => {
expect(
enhance({
argType: { control: { type: 'radio', options: [1, 2] } },
argType: { name: 'input', control: { type: 'radio', options: [1, 2] } },
}).input
).toMatchInlineSnapshot(`
{
@ -137,7 +136,7 @@ describe('enhanceArgTypes', () => {
it('user-specified argTypes take precedence over extracted argTypes', () => {
expect(
enhance({
argType: { type: { name: 'number' } },
argType: { name: 'input', type: { name: 'number' } },
extractedArgTypes: { input: { type: { name: 'string' } } },
}).input
).toMatchInlineSnapshot(`
@ -153,7 +152,7 @@ describe('enhanceArgTypes', () => {
it('user-specified argTypes take precedence over inferred argTypes', () => {
expect(
enhance({
argType: { type: { name: 'number' } },
argType: { name: 'input', type: { name: 'number' } },
arg: 'hello',
}).input
).toMatchInlineSnapshot(`
@ -184,7 +183,7 @@ describe('enhanceArgTypes', () => {
it('user-specified controls take precedence over inferred controls', () => {
expect(
enhance({
argType: { defaultValue: 5, control: { type: 'range', step: 50 } },
argType: { name: 'input', defaultValue: 5, control: { type: 'range', step: 50 } },
arg: 3,
extractedArgTypes: { input: { name: 'input' } },
}).input
@ -223,7 +222,7 @@ describe('enhanceArgTypes', () => {
it('includes extracted argTypes when user-specified argTypes match', () => {
expect(
enhance({
argType: { type: { name: 'number' } },
argType: { name: 'input', type: { name: 'number' } },
extractedArgTypes: { input: { name: 'input' }, foo: { type: { name: 'number' } } },
})
).toMatchInlineSnapshot(`
@ -246,7 +245,7 @@ describe('enhanceArgTypes', () => {
it('excludes extracted argTypes when user-specified argTypes do not match', () => {
expect(
enhance({
argType: { type: { name: 'number' } },
argType: { name: 'input', type: { name: 'number' } },
extractedArgTypes: { foo: { type: { name: 'number' } } },
})
).toMatchInlineSnapshot(`

View File

@ -1,17 +1,20 @@
import mapValues from 'lodash/mapValues';
import { ArgTypesEnhancer, combineParameters } from '@storybook/client-api';
import { normalizeArgTypes } from './normalizeArgTypes';
import { AnyFramework, StoryContextForEnhancers } from '@storybook/csf';
import { combineParameters } from '@storybook/store';
export const enhanceArgTypes: ArgTypesEnhancer = (context) => {
const { component, argTypes: userArgTypes = {}, docs = {} } = context.parameters;
export const enhanceArgTypes = <TFramework extends AnyFramework>(
context: StoryContextForEnhancers<TFramework>
) => {
const {
component,
argTypes: userArgTypes,
parameters: { docs = {} },
} = context;
const { extractArgTypes } = docs;
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;
? combineParameters(extractedArgTypes, userArgTypes)
: userArgTypes;
return withExtractedTypes;
};

View File

@ -1,18 +0,0 @@
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

@ -4,10 +4,7 @@ import remarkExternalLinks from 'remark-external-links';
// @ts-ignore
import { createCompiler } from '@storybook/csf-tools/mdx';
const resolvedBabelLoader = require.resolve('babel-loader', {
paths: [require.resolve('@storybook/builder-webpack4')], // FIXME!!!
});
import type { BuilderConfig, Options } from '@storybook/core-common';
// 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
@ -34,8 +31,26 @@ function createBabelOptions({ babelOptions, mdxBabelOptions, configureJSX }: Bab
};
}
export function webpack(webpackConfig: any = {}, options: any = {}) {
export async function webpack(
webpackConfig: any = {},
options: Options &
BabelParams & { sourceLoaderOptions: any; transcludeMarkdown: boolean } & Parameters<
typeof createCompiler
>[0]
) {
const { builder = 'webpack4' } = await options.presets.apply<{
builder: BuilderConfig;
}>('core', {} as any);
const builderName = typeof builder === 'string' ? builder : builder.name;
const resolvedBabelLoader = require.resolve('babel-loader', {
paths: builderName.match(/(webpack4|webpack5)/)
? [require.resolve(`@storybook/builder-${builder}`)]
: [builderName],
});
const { module = {} } = webpackConfig;
// it will reuse babel options that are already in use in storybook
// also, these babel options are chained with other presets.
const {
@ -65,7 +80,7 @@ export function webpack(webpackConfig: any = {}, options: any = {}) {
let rules = module.rules || [];
if (transcludeMarkdown) {
rules = [
...rules.filter((rule: any) => rule.test.toString() !== '/\\.md$/'),
...rules.filter((rule: any) => rule.test?.toString() !== '/\\.md$/'),
{
test: /\.md$/,
use: [

View File

@ -1,7 +1,7 @@
import React from 'react';
import { StoryFn } from '@storybook/addons';
import { PartialStoryFn } from '@storybook/csf';
export function prepareForInline(storyFn: StoryFn<string>) {
export function prepareForInline(storyFn: PartialStoryFn<any>) {
const html = storyFn();
if (typeof html === 'string') {
// eslint-disable-next-line react/no-danger

View File

@ -1,15 +1,18 @@
import { addons, StoryContext } from '@storybook/addons';
import { addons, StoryContext, useEffect } from '@storybook/addons';
import { sourceDecorator } from './sourceDecorator';
import { SNIPPET_RENDERED } from '../../shared';
jest.mock('@storybook/addons');
const mockedAddons = addons as jest.Mocked<typeof addons>;
const mockedUseEffect = useEffect as jest.Mocked<typeof useEffect>;
expect.addSnapshotSerializer({
print: (val: any) => val,
test: (val) => typeof val === 'string',
});
const tick = () => new Promise((r) => setTimeout(r, 0));
const makeContext = (name: string, parameters: any, args: any, extra?: object): StoryContext => ({
id: `html-test--${name}`,
kind: 'js-text',
@ -25,15 +28,17 @@ describe('sourceDecorator', () => {
let mockChannel: { on: jest.Mock; emit?: jest.Mock };
beforeEach(() => {
mockedAddons.getChannel.mockReset();
mockedUseEffect.mockImplementation((cb) => setTimeout(cb, 0));
mockChannel = { on: jest.fn(), emit: jest.fn() };
mockedAddons.getChannel.mockReturnValue(mockChannel as any);
});
it('should render dynamically for args stories', () => {
it('should render dynamically for args stories', async () => {
const storyFn = (args: any) => `<div>args story</div>`;
const context = makeContext('args', { __isArgsStory: true }, {});
sourceDecorator(storyFn, context);
await tick();
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'html-test--args',
@ -41,7 +46,7 @@ describe('sourceDecorator', () => {
);
});
it('should dedent source by default', () => {
it('should dedent source by default', async () => {
const storyFn = (args: any) => `
<div>
args story
@ -49,6 +54,7 @@ describe('sourceDecorator', () => {
`;
const context = makeContext('args', { __isArgsStory: true }, {});
sourceDecorator(storyFn, context);
await tick();
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'html-test--args',
@ -56,14 +62,15 @@ describe('sourceDecorator', () => {
);
});
it('should skip dynamic rendering for no-args stories', () => {
it('should skip dynamic rendering for no-args stories', async () => {
const storyFn = () => `<div>classic story</div>`;
const context = makeContext('classic', {}, {});
sourceDecorator(storyFn, context);
await tick();
expect(mockChannel.emit).not.toHaveBeenCalled();
});
it('should use the originalStoryFn if excludeDecorators is set', () => {
it('should use the originalStoryFn if excludeDecorators is set', async () => {
const storyFn = (args: any) => `<div>args story</div>`;
const decoratedStoryFn = (args: any) => `
<div style="padding: 25px; border: 3px solid red;">${storyFn(args)}</div>
@ -82,6 +89,7 @@ describe('sourceDecorator', () => {
{ originalStoryFn: storyFn }
);
sourceDecorator(decoratedStoryFn, context);
await tick();
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'html-test--args',
@ -89,12 +97,13 @@ describe('sourceDecorator', () => {
);
});
it('allows the snippet output to be modified by transformSource', () => {
it('allows the snippet output to be modified by transformSource', async () => {
const storyFn = (args: any) => `<div>args story</div>`;
const transformSource = (dom: string) => `<p>${dom}</p>`;
const docs = { transformSource };
const context = makeContext('args', { __isArgsStory: true, docs }, {});
sourceDecorator(storyFn, context);
await tick();
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'html-test--args',

View File

@ -1,9 +1,12 @@
/* global window */
import { addons, StoryContext, StoryFn } from '@storybook/addons';
import { addons, useEffect } from '@storybook/addons';
import { ArgsStoryFn, PartialStoryFn, StoryContext } from '@storybook/csf';
import dedent from 'ts-dedent';
import { HtmlFramework } from '@storybook/html';
import { SNIPPET_RENDERED, SourceType } from '../../shared';
function skipSourceRender(context: StoryContext) {
function skipSourceRender(context: StoryContext<HtmlFramework>) {
const sourceParams = context?.parameters.docs?.source;
const isArgsStory = context?.parameters.__isArgsStory;
@ -23,22 +26,35 @@ function defaultTransformSource(source: string) {
return dedent(source);
}
function applyTransformSource(source: string, context: StoryContext): string {
function applyTransformSource(source: string, context: StoryContext<HtmlFramework>): string {
const docs = context.parameters.docs ?? {};
const transformSource = docs.transformSource ?? defaultTransformSource;
return transformSource(source, context);
}
export function sourceDecorator(storyFn: StoryFn, context: StoryContext) {
export function sourceDecorator(
storyFn: PartialStoryFn<HtmlFramework>,
context: StoryContext<HtmlFramework>
) {
const story = context?.parameters.docs?.source?.excludeDecorators
? context.originalStoryFn(context.args)
? (context.originalStoryFn as ArgsStoryFn<HtmlFramework>)(context.args, context)
: storyFn();
if (typeof story === 'string' && !skipSourceRender(context)) {
const source = applyTransformSource(story, context);
let source: string;
if (!skipSourceRender(context)) {
if (typeof story === 'string') {
source = story;
}
// eslint-disable-next-line no-undef
else if (story instanceof Element) {
source = story.outerHTML;
}
addons.getChannel().emit(SNIPPET_RENDERED, context.id, source);
if (source) source = applyTransformSource(source, context);
}
useEffect(() => {
if (source) addons.getChannel().emit(SNIPPET_RENDERED, context.id, source);
});
return story;
}

View File

@ -1,4 +1,6 @@
import { StoryFn } from '@storybook/addons';
import { PartialStoryFn } from '@storybook/csf';
import { ReactFramework } from '@storybook/react';
import { extractArgTypes } from './extractArgTypes';
import { extractComponentDescription } from '../../lib/docgen';
import { jsxDecorator } from './jsxDecorator';
@ -7,7 +9,7 @@ export const parameters = {
docs: {
inlineStories: true,
// NOTE: that the result is a react element. Hooks support is provided by the outer code.
prepareForInline: (storyFn: StoryFn) => storyFn(),
prepareForInline: (storyFn: PartialStoryFn<ReactFramework>) => storyFn(),
extractArgTypes,
extractComponentDescription,
},

View File

@ -1,4 +1,4 @@
import { ArgTypes } from '@storybook/api';
import { StrictArgTypes } from '@storybook/csf';
import { PropDef, ArgTypesExtractor } from '../../lib/docgen';
import { extractProps } from './extractProps';
@ -6,7 +6,7 @@ export const extractArgTypes: ArgTypesExtractor = (component) => {
if (component) {
const { rows } = extractProps(component);
if (rows) {
return rows.reduce((acc: ArgTypes, row: PropDef) => {
return rows.reduce((acc: StrictArgTypes, row: PropDef) => {
const {
name,
description,

View File

@ -1,12 +1,13 @@
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
import React from 'react';
import PropTypes from 'prop-types';
import { addons, StoryContext } from '@storybook/addons';
import { addons, StoryContext, useEffect } 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>;
const mockedUseEffect = useEffect as jest.Mocked<typeof useEffect>;
expect.addSnapshotSerializer({
print: (val: any) => val,
@ -168,15 +169,17 @@ describe('jsxDecorator', () => {
let mockChannel: { on: jest.Mock; emit?: jest.Mock };
beforeEach(() => {
mockedAddons.getChannel.mockReset();
mockedUseEffect.mockImplementation((cb) => setTimeout(cb, 0));
mockChannel = { on: jest.fn(), emit: jest.fn() };
mockedAddons.getChannel.mockReturnValue(mockChannel as any);
});
it('should render dynamically for args stories', () => {
it('should render dynamically for args stories', async () => {
const storyFn = (args: any) => <div>args story</div>;
const context = makeContext('args', { __isArgsStory: true }, {});
jsxDecorator(storyFn, context);
await new Promise((r) => setTimeout(r, 0));
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'jsx-test--args',
@ -184,7 +187,7 @@ describe('jsxDecorator', () => {
);
});
it('should not render decorators when provided excludeDecorators parameter', () => {
it('should not render decorators when provided excludeDecorators parameter', async () => {
const storyFn = (args: any) => <div>args story</div>;
const decoratedStoryFn = (args: any) => (
<div style={{ padding: 25, border: '3px solid red' }}>{storyFn(args)}</div>
@ -203,6 +206,8 @@ describe('jsxDecorator', () => {
{ originalStoryFn: storyFn }
);
jsxDecorator(decoratedStoryFn, context);
await new Promise((r) => setTimeout(r, 0));
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'jsx-test--args',
@ -210,20 +215,24 @@ describe('jsxDecorator', () => {
);
});
it('should skip dynamic rendering for no-args stories', () => {
it('should skip dynamic rendering for no-args stories', async () => {
const storyFn = () => <div>classic story</div>;
const context = makeContext('classic', {}, {});
jsxDecorator(storyFn, context);
await new Promise((r) => setTimeout(r, 0));
expect(mockChannel.emit).not.toHaveBeenCalled();
});
// This is deprecated, but still test it
it('allows the snippet output to be modified by onBeforeRender', () => {
it('allows the snippet output to be modified by onBeforeRender', async () => {
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);
await new Promise((r) => setTimeout(r, 0));
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'jsx-test--args',
@ -231,12 +240,14 @@ describe('jsxDecorator', () => {
);
});
it('allows the snippet output to be modified by transformSource', () => {
it('allows the snippet output to be modified by transformSource', async () => {
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);
await new Promise((r) => setTimeout(r, 0));
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,
'jsx-test--args',
@ -253,7 +264,7 @@ describe('jsxDecorator', () => {
expect(transformSource).toHaveBeenCalledWith('<div>\n args story\n</div>', context);
});
it('renders MDX properly', () => {
it('renders MDX properly', async () => {
// FIXME: generate this from actual MDX
const mdxElement = {
type: { displayName: 'MDXCreateElement' },
@ -265,6 +276,7 @@ describe('jsxDecorator', () => {
};
jsxDecorator(() => mdxElement, makeContext('mdx-args', { __isArgsStory: true }, {}));
await new Promise((r) => setTimeout(r, 0));
expect(mockChannel.emit).toHaveBeenCalledWith(
SNIPPET_RENDERED,

View File

@ -3,8 +3,10 @@ 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 { addons, useEffect } from '@storybook/addons';
import { StoryContext, ArgsStoryFn, PartialStoryFn } from '@storybook/csf';
import { logger } from '@storybook/client-logger';
import { ReactFramework } from '@storybook/react';
import { SourceType, SNIPPET_RENDERED } from '../../shared';
import { getDocgenSection } from '../../lib/docgen';
@ -22,7 +24,7 @@ type JSXOptions = Options & {
/** 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;
transformSource?(dom: string, context?: StoryContext<ReactFramework>): string;
};
/** Run the user supplied onBeforeRender function if it exists */
@ -44,7 +46,11 @@ const applyBeforeRender = (domString: string, options: JSXOptions) => {
};
/** Run the user supplied transformSource function if it exists */
const applyTransformSource = (domString: string, options: JSXOptions, context?: StoryContext) => {
const applyTransformSource = (
domString: string,
options: JSXOptions,
context?: StoryContext<ReactFramework>
) => {
if (typeof options.transformSource !== 'function') {
return domString;
}
@ -138,7 +144,7 @@ const defaultOpts = {
showDefaultProps: false,
};
export const skipJsxRender = (context: StoryContext) => {
export const skipJsxRender = (context: StoryContext<ReactFramework>) => {
const sourceParams = context?.parameters.docs?.source;
const isArgsStory = context?.parameters.__isArgsStory;
@ -165,17 +171,26 @@ const mdxToJsx = (node: any) => {
return createElement(originalType, rest, ...jsxChildren);
};
export const jsxDecorator = (storyFn: any, context: StoryContext) => {
export const jsxDecorator = (
storyFn: PartialStoryFn<ReactFramework>,
context: StoryContext<ReactFramework>
) => {
const channel = addons.getChannel();
const skip = skipJsxRender(context);
const story = storyFn();
let jsx = '';
useEffect(() => {
if (!skip) channel.emit(SNIPPET_RENDERED, (context || {}).id, jsx);
});
// 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)) {
if (skip) {
return story;
}
const channel = addons.getChannel();
const options = {
...defaultOpts,
...(context?.parameters.jsx || {}),
@ -183,18 +198,15 @@ export const jsxDecorator = (storyFn: any, context: StoryContext) => {
// Exclude decorators from source code snippet by default
const storyJsx = context?.parameters.docs?.source?.excludeDecorators
? context.originalStoryFn(context.args)
? (context.originalStoryFn as ArgsStoryFn<ReactFramework>)(context.args, context)
: story;
const sourceJsx = mdxToJsx(storyJsx);
let jsx = '';
const rendered = renderJsx(sourceJsx, options);
if (rendered) {
jsx = applyTransformSource(rendered, options, context);
}
channel.emit(SNIPPET_RENDERED, (context || {}).id, jsx);
return story;
};

View File

@ -207,7 +207,7 @@ function parseExpression(expression: any): ParsingResult<InspectionInferedType>
}
export function parse(value: string): ParsingResult<InspectionInferedType> {
const ast = (acornParser.parse(`(${value})`) as unknown) as estree.Program;
const ast = acornParser.parse(`(${value})`) as unknown as estree.Program;
let parsingResult: ParsingResult<InspectionUnknown> = {
inferredType: { type: InspectionType.UNKNOWN },

View File

@ -107,8 +107,7 @@ describe('enhancePropTypesProp', () => {
const component = createTestComponent({
type: {
name: 'custom',
raw:
'{\n text: PropTypes.string.isRequired,\n value1: PropTypes.string.isRequired,\n value2: PropTypes.string.isRequired,\n value3: PropTypes.string.isRequired,\n value4: PropTypes.string.isRequired,\n}',
raw: '{\n text: PropTypes.string.isRequired,\n value1: PropTypes.string.isRequired,\n value2: PropTypes.string.isRequired,\n value3: PropTypes.string.isRequired,\n value4: PropTypes.string.isRequired,\n}',
},
});
@ -144,8 +143,7 @@ describe('enhancePropTypesProp', () => {
const component = createTestComponent({
type: {
name: 'custom',
raw:
'function InlinedFunctionalComponent() {\n return <div>Inlined FunctionalComponent!</div>;\n}',
raw: 'function InlinedFunctionalComponent() {\n return <div>Inlined FunctionalComponent!</div>;\n}',
},
});
@ -164,8 +162,7 @@ describe('enhancePropTypesProp', () => {
const component = createTestComponent({
type: {
name: 'custom',
raw:
'<div>Hello world from Montreal, Quebec, Canada!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!</div>',
raw: '<div>Hello world from Montreal, Quebec, Canada!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!</div>',
},
});
@ -203,8 +200,7 @@ describe('enhancePropTypesProp', () => {
const component = createTestComponent({
type: {
name: 'custom',
raw:
'Symbol("A very very very very very very lonnnngggggggggggggggggggggggggggggggggggg symbol")',
raw: 'Symbol("A very very very very very very lonnnngggggggggggggggggggggggggggggggggggg symbol")',
},
});
@ -608,8 +604,7 @@ describe('enhancePropTypesProp', () => {
name: 'objectOf',
value: {
name: 'custom',
raw:
'{\n foo: PropTypes.string,\n bar: PropTypes.string,\n another: PropTypes.string,\n anotherAnother: PropTypes.string,\n}',
raw: '{\n foo: PropTypes.string,\n bar: PropTypes.string,\n another: PropTypes.string,\n anotherAnother: PropTypes.string,\n}',
},
},
});
@ -815,8 +810,7 @@ describe('enhancePropTypesProp', () => {
name: 'arrayOf',
value: {
name: 'custom',
raw:
'{\n text: PropTypes.string.isRequired,\n value: PropTypes.string.isRequired,\n another: PropTypes.string.isRequired,\n another2: PropTypes.string.isRequired,\n another3: PropTypes.string.isRequired,\n another4: PropTypes.string.isRequired,\n}',
raw: '{\n text: PropTypes.string.isRequired,\n value: PropTypes.string.isRequired,\n another: PropTypes.string.isRequired,\n another2: PropTypes.string.isRequired,\n another3: PropTypes.string.isRequired,\n another4: PropTypes.string.isRequired,\n}',
},
},
});

View File

@ -3,15 +3,15 @@ import mapValues from 'lodash/mapValues';
import { storiesOf, StoryContext } from '@storybook/react';
import { ArgsTable } from '@storybook/components';
import { Args } from '@storybook/api';
import { inferControls } from '@storybook/client-api';
import { inferControls } from '@storybook/store';
import { extractArgTypes } from './extractArgTypes';
import { Component } from '../../blocks';
const argsTableProps = (component: Component) => {
const argTypes = extractArgTypes(component);
const parameters = { __isArgsStory: true, argTypes };
const rows = inferControls(({ parameters } as unknown) as StoryContext);
const parameters = { __isArgsStory: true };
const rows = inferControls({ argTypes, parameters } as unknown as StoryContext<any>);
return { rows };
};

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