mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 07:21:17 +08:00
Merge branch 'next' into pr/15822
This commit is contained in:
commit
3a3561e3c6
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
28
.eslintrc.js
28
.eslintrc.js
@ -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',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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
1
.github/stale.yml
vendored
@ -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
28
.github/workflows/generate-repros.yml
vendored
Normal 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
|
99
.github/workflows/handle-release-branches.yml
vendored
Normal file
99
.github/workflows/handle-release-branches.yml
vendored
Normal 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
27
.github/workflows/linear-export.yml
vendored
Normal 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 }}
|
27
.github/workflows/tests-unit.yml
vendored
27
.github/workflows/tests-unit.yml
vendored
@ -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
3
.gitignore
vendored
@ -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.*
|
||||
|
32
.teamcity/settings.kts
vendored
32
.teamcity/settings.kts
vendored
@ -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
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
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
631
.yarn/releases/yarn-sources.cjs
generated
vendored
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
@ -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>)
|
1293
CHANGELOG.md
1293
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
|
||||
|
516
MIGRATION.md
516
MIGRATION.md
@ -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';
|
||||
|
42
README.md
42
README.md
@ -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/*) | [](app/react) |
|
||||
| [Vue](app/vue) | [v6.3.x](https://storybookjs.netlify.com/vue-kitchen-sink/) | [](app/vue) |
|
||||
| [Angular](app/angular) | [v6.3.x](https://storybookjs.netlify.com/angular-cli/) | [](app/angular) |
|
||||
| [Web components](app/web-components) | [v6.3.x](https://storybookjs.netlify.com/web-components-kitchen-sink/) | [](app/web-components) |
|
||||
| [React](app/react) | [v6.4.x](https://storybookjs.netlify.com/official-storybook/?path=/story/*) | [](app/react) |
|
||||
| [Vue](app/vue) | [v6.4.x](https://storybookjs.netlify.com/vue-kitchen-sink/) | [](app/vue) |
|
||||
| [Angular](app/angular) | [v6.4.x](https://storybookjs.netlify.com/angular-cli/) | [](app/angular) |
|
||||
| [Web components](app/web-components) | [v6.4.x](https://storybookjs.netlify.com/web-components-kitchen-sink/) | [](app/web-components) |
|
||||
| [React Native](https://github.com/storybookjs/react-native) | - | [](app/react-native) |
|
||||
| [HTML](app/html) | [v6.3.x](https://storybookjs.netlify.com/html-kitchen-sink/) | [](app/html) |
|
||||
| [Ember](app/ember) | [v6.3.x](https://storybookjs.netlify.com/ember-cli/) | [](app/ember) |
|
||||
| [Svelte](app/svelte) | [v6.3.x](https://storybookjs.netlify.com/svelte-kitchen-sink/) | [](app/svelte) |
|
||||
| [Preact](app/preact) | [v6.3.x](https://storybookjs.netlify.com/preact-kitchen-sink/) | [](app/preact) |
|
||||
| [Marionette.js](app/marionette) | - | [](app/marionette) |
|
||||
| [Mithril](app/mithril) | [v6.3.x](https://storybookjs.netlify.com/mithril-kitchen-sink/) | [](app/mithril) |
|
||||
| [Marko](app/marko) | [v6.3.x](https://storybookjs.netlify.com/marko-cli/) | [](app/marko) |
|
||||
| [Riot](app/riot) | [v6.3.x](https://storybookjs.netlify.com/riot-kitchen-sink/) | [](app/riot) |
|
||||
| [Rax](app/rax) | [v6.3.x](https://storybookjs.netlify.com/rax-kitchen-sink/) | [](app/rax) |
|
||||
| [Android, iOS, Flutter](https://github.com/storybookjs/native) | [v6.3.x](https://storybookjs.github.io/native/@storybook/native-flutter-example/index.html) | [](https://github.com/storybookjs/native) |
|
||||
| [HTML](app/html) | [v6.4.x](https://storybookjs.netlify.com/html-kitchen-sink/) | [](app/html) |
|
||||
| [Ember](app/ember) | [v6.4.x](https://storybookjs.netlify.com/ember-cli/) | [](app/ember) |
|
||||
| [Svelte](app/svelte) | [v6.4.x](https://storybookjs.netlify.com/svelte-kitchen-sink/) | [](app/svelte) |
|
||||
| [Preact](app/preact) | [v6.4.x](https://storybookjs.netlify.com/preact-kitchen-sink/) | [](app/preact) |
|
||||
| [Marionette.js](https://github.com/storybookjs/marionette) | - | [](app/marionette) |
|
||||
| [Mithril](https://github.com/storybookjs/mithril) | [v6.4.x](https://storybookjs.netlify.com/mithril-kitchen-sink/) | [](app/mithril) |
|
||||
| [Marko](https://github.com/storybookjs/marko) | [v6.4.x](https://storybookjs.netlify.com/marko-cli/) | [](app/marko) |
|
||||
| [Riot](https://github.com/storybookjs/riot) | [v6.4.x](https://storybookjs.netlify.com/riot-kitchen-sink/) | [](app/riot) |
|
||||
| [Rax](https://github.com/storybookjs/rax) | [v6.4.x](https://storybookjs.netlify.com/rax-kitchen-sink/) | [](app/rax) |
|
||||
| [Android, iOS, Flutter](https://github.com/storybookjs/native) | [v6.4.x](https://storybookjs.github.io/native/@storybook/native-flutter-example/index.html) | [](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.
|
||||
|
||||
|
13
SECURITY.md
13
SECURITY.md
@ -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
29
__mocks__/fs-extra.js
Normal 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;
|
@ -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)
|
||||
|
||||

|
||||
|
||||
@ -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, it’ll help you an
|
||||
```js
|
||||
MyStory.parameters = {
|
||||
a11y: {
|
||||
options: {
|
||||
config: {
|
||||
rules: [
|
||||
{
|
||||
// Allow `autocomplete="nope"` on form elements,
|
||||
|
@ -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",
|
||||
|
@ -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: {},
|
||||
|
@ -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 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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(
|
||||
() => [
|
||||
{
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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__/**"
|
||||
]
|
||||
}
|
||||
}
|
@ -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` |
|
||||
|
@ -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)
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 80 KiB |
@ -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",
|
||||
|
@ -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>;
|
||||
|
@ -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({});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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__/**"
|
||||
]
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||

|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
|
BIN
addons/backgrounds/docs/addon-backgrounds.gif
Normal file
BIN
addons/backgrounds/docs/addon-backgrounds.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 465 KiB |
@ -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",
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
|
@ -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__/**"
|
||||
]
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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')];
|
||||
}
|
||||
|
||||
|
19
addons/controls/src/preset/checkDocsLoaded.ts
Normal file
19
addons/controls/src/preset/checkDocsLoaded.ts
Normal 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),
|
||||
});
|
||||
};
|
@ -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();
|
||||
});
|
||||
});
|
@ -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}`);
|
||||
}
|
||||
};
|
@ -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__/**"
|
||||
]
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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
1
addons/docs/angular/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export * from '../dist/ts3.9/frameworks/angular/index.d';
|
@ -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} />
|
||||
|
@ -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",
|
||||
|
@ -1 +1,2 @@
|
||||
/* eslint-disable import/extensions */
|
||||
require('./dist/esm/register.js');
|
||||
|
@ -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 }} />;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 }) };
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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__;
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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 }) => {
|
||||
|
@ -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 } }) };
|
||||
};
|
||||
|
44
addons/docs/src/blocks/useStory.ts
Normal file
44
addons/docs/src/blocks/useStory.ts
Normal 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]);
|
||||
}
|
@ -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
|
||||
|
@ -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",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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 [],
|
@ -3,5 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["./*.ts"]
|
||||
}
|
||||
"include": [
|
||||
"./*.ts"
|
||||
]
|
||||
}
|
@ -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);
|
||||
|
@ -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 },
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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(`
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
});
|
@ -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: [
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 },
|
||||
|
@ -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}',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user