Merge branch 'next' into pr/9531

This commit is contained in:
Michael Shilman 2020-05-04 12:40:43 +08:00
commit 8a882e9eaa
1760 changed files with 44085 additions and 42186 deletions

View File

@ -92,6 +92,10 @@ module.exports = {
plugins: [
'emotion',
'babel-plugin-macros',
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-shorthand-properties',
'@babel/plugin-transform-block-scoping',
'@babel/plugin-transform-destructuring',
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-export-default-from',

View File

@ -3,7 +3,9 @@ import { execSync } from 'child_process';
execSync('npm install lodash');
const { flatten, intersection, isEmpty } = require('lodash');
const flatten = require('lodash/flatten');
const intersection = require('lodash/intersection');
const isEmpty = require('lodash/isEmpty');
const pkg = require('../../package.json'); // eslint-disable-line import/newline-after-import
const prLogConfig = pkg['pr-log'];

View File

@ -1,32 +1,29 @@
version: 2.1
aliases:
- &defaults
working_directory: /tmp/storybook
docker:
- image: circleci/node:10
- image: circleci/node:10-browsers
jobs:
build:
install:
<<: *defaults
steps:
- checkout
- restore_cache:
name: Restore core dependencies cache
keys:
- core-dependencies-v4-{{ checksum "yarn.lock" }}
- core-dependencies-v5-{{ checksum "yarn.lock" }}
- run:
name: Install dependencies
command: yarn install
- run:
name: Check that yarn.lock is not corrupted
command: yarn repo-dirty-check
- run:
name: Bootstrap
command: yarn bootstrap --core
- save_cache:
name: Cache core dependencies
key: core-dependencies-v4-{{ checksum "yarn.lock" }}
key: core-dependencies-v5-{{ checksum "yarn.lock" }}
paths:
- ~/.cache
- node_modules
@ -40,64 +37,34 @@ jobs:
- dev-kits
- app
- lib
chromatic:
build:
<<: *defaults
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Run chromatic on the pre-built official example
command: yarn chromatic --storybook-build-dir="built-storybooks/official-storybook" --exit-zero-on-changes --app-code="ab7m45tp9p"
name: Bootstrap
command: yarn bootstrap --core
- persist_to_workspace:
root: .
paths:
- examples
- addons
- dev-kits
- app
- lib
chromatic:
<<: *defaults
parallelism: 10
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Run chromatic on the pre-built angular example
command: yarn chromatic --storybook-build-dir="built-storybooks/angular-cli" --app-code="tl92yzsj6w"
- run:
name: Run chromatic on the pre-built cra-kitchen-sink example
command: yarn chromatic --storybook-build-dir="built-storybooks/cra-kitchen-sink" --app-code="tg55gajmdt"
- run:
name: Run chromatic on the pre-built cra-react15 example
command: yarn chromatic --storybook-build-dir="built-storybooks/cra-react15" --app-code="gxk7iqej3wt"
- run:
name: Run chromatic on the pre-built cra-ts-essentials example
command: yarn chromatic --storybook-build-dir="built-storybooks/cra-ts-essentials" --app-code="b311ypk6of"
- run:
name: Run chromatic on the pre-built cra-ts-kitchen-sink example
command: yarn chromatic --storybook-build-dir="built-storybooks/cra-ts-kitchen-sink" --app-code="19whyj1tlac"
- run:
name: Run chromatic on the pre-built dev-kits example
command: yarn chromatic --storybook-build-dir="built-storybooks/dev-kits" --app-code="7yykp9ifdxx"
- run:
name: Run chromatic on the pre-built ember-cli example
command: yarn chromatic --storybook-build-dir="built-storybooks/ember-cli" --app-code="19z23qxndju"
- run:
name: Run chromatic on the pre-built html-kitchen-sink example
command: yarn chromatic --storybook-build-dir="built-storybooks/html-kitchen-sink" --app-code="e8zolxoyg8o"
- run:
name: Run chromatic on the pre-built marko-cli example
command: yarn chromatic --storybook-build-dir="built-storybooks/marko-cli" --app-code="qaegx64axu"
- run:
name: Run chromatic on the pre-built mithril-kitchen-sink example
command: yarn chromatic --storybook-build-dir="built-storybooks/mithril-kitchen-sink" --app-code="8adgm46jzk8"
- run:
name: Run chromatic on the pre-built preact-kitchen-sink example
command: yarn chromatic --storybook-build-dir="built-storybooks/preact-kitchen-sink" --app-code="ls0ikhnwqt"
- run:
name: Run chromatic on the pre-built rax-kitchen-sink example
command: yarn chromatic --storybook-build-dir="built-storybooks/rax-kitchen-sink" --app-code="4co6vptx8qo"
- run:
name: Run chromatic on the pre-built riot-kitchen-sink example
command: yarn chromatic --storybook-build-dir="built-storybooks/riot-kitchen-sink" --app-code="g2dp3lnr34a"
- run:
name: Run chromatic on the pre-built svelte-kitchen-sink example
command: yarn chromatic --storybook-build-dir="built-storybooks/svelte-kitchen-sink" --app-code="8ob73wgl995"
- run:
name: Run chromatic on the pre-built vue-kitchen-sink example
command: yarn chromatic --storybook-build-dir="built-storybooks/vue-kitchen-sink" --app-code="cyxj0e38bqj"
- run:
name: Run chromatic on the pre-built web-components-kitchen-sink example
command: yarn chromatic --storybook-build-dir="built-storybooks/web-components-kitchen-sink" --app-code="npm5gsofwkf"
name: examples
command: |
yarn run-chromatics
packtracker:
<<: *defaults
steps:
@ -111,7 +78,7 @@ jobs:
yarn packtracker
examples:
<<: *defaults
parallelism: 4
parallelism: 10
steps:
- checkout
- attach_workspace:
@ -139,7 +106,7 @@ jobs:
command: yarn cypress install
- save_cache:
name: Cache core dependencies
key: core-dependencies-v4-{{ checksum "yarn.lock" }}
key: core-dependencies-v5-{{ checksum "yarn.lock" }}
paths:
- ~/.cache
- node_modules
@ -228,7 +195,7 @@ jobs:
- restore_cache:
name: Restore core dependencies cache
keys:
- core-dependencies-v4-{{ checksum "yarn.lock" }}
- core-dependencies-v5-{{ checksum "yarn.lock" }}
- run:
name: Install dependencies
command: yarn bootstrap --install
@ -242,7 +209,7 @@ jobs:
- restore_cache:
name: Restore docs dependencies cache
keys:
- docs-dependencies-v2-{{ checksum "docs/yarn.lock" }}
- docs-dependencies-v3-{{ checksum "docs/yarn.lock" }}
- run:
name: Install dependencies
command: |
@ -255,7 +222,7 @@ jobs:
yarn build
- save_cache:
name: Cache docs dependencies
key: docs-dependencies-v2-{{ checksum "docs/yarn.lock" }}
key: docs-dependencies-v3-{{ checksum "docs/yarn.lock" }}
paths:
- ~/.cache
lint:
@ -292,7 +259,10 @@ jobs:
workflows:
test:
jobs:
- build
- install
- build:
requires:
- install
- lint:
requires:
- build

View File

@ -19,6 +19,7 @@ examples/cra-ts-kitchen-sink/public/*
examples/cra-ts-essentials/*.json
examples/cra-ts-essentials/public/*
examples/rax-kitchen-sink/src/document/*
ember-output
.yarn
!.remarkrc.js
!.babelrc.js

View File

@ -1,46 +1,11 @@
module.exports = {
root: true,
extends: ['@storybook/eslint-config-storybook'],
rules: {
'import/extensions': [
'error',
'never',
{ ignorePackages: true, md: 'always', svg: 'always', json: 'always', tag: 'always' },
],
'import/no-unresolved': ['error', { ignore: ['@storybook'] }],
'react/state-in-constructor': 'off',
'react/static-property-placement': 'off',
'react/jsx-props-no-spreading': 'off',
'react/jsx-fragments': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/no-object-literal-type-assertion': 'off',
'@typescript-eslint/no-empty-function': 'off',
'react/sort-comp': [
'error',
{
order: [
'staticLifecycle',
'static-methods',
'instance-variables',
'lifecycle',
'/^on.+$/',
'/^(get|set)(?!(DerivedStateFromProps|SnapshotBeforeUpdate$)).+$/',
'instance-methods',
'instance-variables',
'everything-else',
'render',
],
groups: {
staticLifecycle: ['displayName', 'propTypes', 'defaultProps', 'getDerivedStateFromProps'],
},
},
],
'max-classes-per-file': 'off',
},
overrides: [
{
files: [
'**/__tests__/**',
'**/__testfixtures__/**',
'**/*.test.*',
'**/*.stories.*',
'**/storyshots/**/stories/**',
@ -52,6 +17,14 @@ module.exports = {
'import/no-extraneous-dependencies': 'off',
},
},
{
files: ['**/__testfixtures__/**'],
rules: {
'react/forbid-prop-types': 'off',
'react/no-unused-prop-types': 'off',
'react/require-default-props': 'off',
},
},
{ files: '**/.storybook/config.js', rules: { 'global-require': 'off' } },
{
files: ['**/*.stories.*'],

1
.github/CODEOWNERS vendored
View File

@ -4,7 +4,6 @@
/addons/a11y/ @jbovenschen @codebyalex
/addons/actions/ @rhalff
/addons/backgrounds/ @ndelangen
/addons/centered/ @kazupon
/addons/events/ @z4o4z @ndelangen
/addons/graphql/ @mnmtanish
/addons/info/ @theinterned @z4o4z @UsulPro @dangreenisrael

View File

@ -1,7 +1,6 @@
'addon: a11y': ["addons/a11y/**"]
'addon: actions': ["addons/actions/**"]
'addon: backgrounds': ["addons/backgrounds/**"]
'addon: centered': ["addons/centered/**"]
'addon: events ': ["addons/events/**"]
'addon: graphql ': ["addons/graphql/**"]
'addon: info': ["addons/info/**"]

View File

@ -4,14 +4,13 @@
'app: marko': ['nm123github']
'app: preact': ['BartWaardenburg']
'app: rax': ['SoloJiang']
'app: svelte': ['rixo', 'cam-stitt', 'plumpNation']
'app: svelte': ['rixo', 'plumpNation']
'app: vue': ['backbone87', 'elevatebart', 'pksunkara', 'Aaron-Pool', 'pocka']
'app: web-components': ['daKmoR']
'api: addons': ['ndelangen']
'addon: a11y': ['CodeByAlex', 'Armanio', 'jsomsanith']
'addon: contexts': ['leoyli']
'addon: docs': ['shilman', 'elevatebart', 'jeroenreumkens']
'addon: info': ['shilman', 'elevatebart']
'addon: toolbars': ['shilman']
'addon: docs': ['shilman', 'patricklafrance']
'addon: knobs': ['leoyli', 'Armanio']
'addon: storysource': ['igor-dv', 'libetl']
typescript: ['kroeder', 'gaetanmaisse', 'ndelangen', 'emilio-martinez']

View File

@ -1,6 +1,6 @@
name: CLI tests
on:
on:
push
# push:
# disabled for now:
@ -17,22 +17,44 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Cache node modules
uses: actions/cache@v1
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }}
key: build-v2-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.OS }}-build-${{ env.cache-name }}-
${{ runner.OS }}-build-
${{ runner.OS }}-
build-v2-${{ env.cache-name }}-
build-v2-
- name: install, bootstrap
run: |
yarn bootstrap --core
- name: cli
run: |
yarn test --cli
cli-yarn-2:
name: CLI Fixtures with Yarn 2
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v2
- name: Cache node modules
uses: actions/cache@v1
with:
path: node_modules
key: build-v2-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
build-v2-${{ env.cache-name }}-
build-v2-
- name: install, bootstrap
run: |
yarn bootstrap --core
- name: cli with Yarn 2
run: |
cd lib/cli
yarn test-yarn-2
latest-cra:
name: Latest CRA
runs-on: ubuntu-latest
@ -40,10 +62,10 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: install, bootstrap
run: |
yarn bootstrap --core
- name: latest-cra
run: |
yarn test-latest-cra
yarn test-latest-cra

View File

@ -1,32 +0,0 @@
name: Puppeteer & A11y tests
on: [push]
jobs:
build:
name: Puppeteer & A11y tests
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v1
- name: Cache node modules
uses: actions/cache@v1
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.OS }}-build-${{ env.cache-name }}-
${{ runner.OS }}-build-
${{ runner.OS }}-
- name: install, bootstrap
run: |
yarn bootstrap --core
- name: build storybook
run: |
yarn --cwd examples/official-storybook build-storybook
- name: test
run: |
yarn test --puppeteer

View File

@ -4,23 +4,21 @@ on: [push]
jobs:
build:
name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }}
runs-on: ${{ matrix.os }}
name: Core Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Cache node modules
uses: actions/cache@v1
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }}
key: build-v2-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.OS }}-build-${{ env.cache-name }}-
${{ runner.OS }}-build-
${{ runner.OS }}-
build-v2-${{ env.cache-name }}-
build-v2-
- name: install, bootstrap
run: |
yarn bootstrap --core

1
.gitignore vendored
View File

@ -7,6 +7,7 @@ node_modules
*.sw*
npm-shrinkwrap.json
dist
ts3.5
.tern-port
*.DS_Store
.cache

View File

@ -1 +0,0 @@
.yarn

View File

@ -3,5 +3,6 @@
"tabWidth": 2,
"bracketSpacing": true,
"trailingComma": "es5",
"singleQuote": true
"singleQuote": true,
"arrowParens": "always"
}

View File

@ -1,81 +0,0 @@
package OpenSourceProjects_Storybook.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.buildFeatures.commitStatusPublisher
import jetbrains.buildServer.configs.kotlin.v2017_2.buildSteps.script
import jetbrains.buildServer.configs.kotlin.v2017_2.triggers.vcs
import jetbrains.buildServer.configs.kotlin.v2017_2.triggers.retryBuild
import jetbrains.buildServer.configs.kotlin.v2017_2.triggers.VcsTrigger
object OpenSourceProjects_Storybook_Bootstrap : BuildType({
uuid = "9f9177e7-9ec9-4e2e-aabb-d304fd667712"
id = "OpenSourceProjects_Storybook_Bootstrap"
name = "Bootstrap"
artifactRules = """
addons/*/dist/** => dist.zip/addons
addons/storyshots/*/dist/** => dist.zip/addons/storyshots
app/*/dist/** => dist.zip/app
dev-kits/*/dist/** => dist.zip/dev-kits
lib/*/dist/** => dist.zip/lib
lib/core/dll/** => dist.zip/lib/core/dll
""".trimIndent()
vcs {
root(OpenSourceProjects_Storybook.vcsRoots.OpenSourceProjects_Storybook_HttpsGithubComStorybooksStorybookRefsHeadsMaster)
}
steps {
script {
name = "Bootstrap"
scriptContent = """
#!/bin/sh
set -e -x
yarn
yarn bootstrap --core
""".trimIndent()
dockerImage = "node:%docker.node.version%"
}
}
triggers {
vcs {
quietPeriodMode = VcsTrigger.QuietPeriodMode.USE_DEFAULT
triggerRules = "-:comment=^TeamCity change:**"
branchFilter = """
+:pull/*
+:release/*
+:master
+:next
+:snyk-fix-*
""".trimIndent()
enabled = false
}
retryBuild {
delaySeconds = 60
enabled = false
}
}
features {
commitStatusPublisher {
publisher = github {
githubUrl = "https://api.github.com"
authType = personalToken {
token = "credentialsJSON:5ffe2d7e-531e-4f6f-b1fc-a41bfea26eaa"
}
}
param("github_oauth_user", "Hypnosphi")
}
}
requirements {
doesNotContain("env.OS", "Windows")
}
cleanup {
artifacts(days = 1)
}
})

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '1bda59b5-d08d-4fd8-b317-953e7d79d881' (id = 'OpenSourceProjects_Storybook_Docs')
accordingly, and delete the patch script.
*/
changeBuildType("1bda59b5-d08d-4fd8-b317-953e7d79d881") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '1ea2b5bd-28f6-44f5-8ab3-6c659ce8fbd6' (id = 'OpenSourceProjects_Storybook_SmokeTests')
accordingly, and delete the patch script.
*/
changeBuildType("1ea2b5bd-28f6-44f5-8ab3-6c659ce8fbd6") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '2b9c73e2-0a6e-47ca-95ae-729cac42be2b' (id = 'OpenSourceProjects_Storybook_Build_2')
accordingly, and delete the patch script.
*/
changeBuildType("2b9c73e2-0a6e-47ca-95ae-729cac42be2b") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '42cfbb9a-f35b-4f96-afae-0b508927a737' (id = 'OpenSourceProjects_Storybook_Lint')
accordingly, and delete the patch script.
*/
changeBuildType("42cfbb9a-f35b-4f96-afae-0b508927a737") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '42cfbb9a-f35b-4f96-afae-0b508927a738' (id = 'OpenSourceProjects_Storybook_Lint_Warnings')
accordingly, and delete the patch script.
*/
changeBuildType("42cfbb9a-f35b-4f96-afae-0b508927a738") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-angular' (id = 'OpenSourceProjects_Storybook_Angular')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-angular") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-chromatic' (id = 'OpenSourceProjects_Storybook_Chromatic')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-chromatic") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-cra' (id = 'OpenSourceProjects_Storybook_CRA')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-cra") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-cra_react15' (id = 'OpenSourceProjects_Storybook_CRA_REACT15')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-cra_react15") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-cra_ts' (id = 'OpenSourceProjects_Storybook_CRA_TS')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-cra_ts") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-ember' (id = 'OpenSourceProjects_Storybook_Ember')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-ember") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-html' (id = 'OpenSourceProjects_Storybook_HTML')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-html") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-marko' (id = 'OpenSourceProjects_Storybook_Marko')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-marko") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-mithril' (id = 'OpenSourceProjects_Storybook_Mithril')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-mithril") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-polymer' (id = 'OpenSourceProjects_Storybook_Polymer')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-polymer") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-preact' (id = 'OpenSourceProjects_Storybook_Preact')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-preact") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-riot' (id = 'OpenSourceProjects_Storybook_Riot')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-riot") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-svelte' (id = 'OpenSourceProjects_Storybook_Svelte')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-svelte") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6-vue' (id = 'OpenSourceProjects_Storybook_Vue')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6-vue") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '8cc5f747-4ca7-4f0d-940d-b0c422f501a6' (id = 'OpenSourceProjects_Storybook_Examples')
accordingly, and delete the patch script.
*/
changeBuildType("8cc5f747-4ca7-4f0d-940d-b0c422f501a6") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,22 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '9f9177e7-9ec9-4e2e-aabb-d304fd667711' (id = 'OpenSourceProjects_Storybook_Test')
accordingly, and delete the patch script.
*/
changeBuildType("9f9177e7-9ec9-4e2e-aabb-d304fd667711") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
params {
add {
param("docker.node.version", "10.13")
}
}
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = '9f9177e7-9ec9-4e2e-aabb-d304fd667712' (id = 'OpenSourceProjects_Storybook_Bootstrap')
accordingly, and delete the patch script.
*/
changeBuildType("9f9177e7-9ec9-4e2e-aabb-d304fd667712") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.patches.buildTypes
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the buildType with uuid = 'd4320bd8-6094-4dd6-9bed-e13d6f0d12e2' (id = 'OpenSourceProjects_Storybook_CliTestLatestCra')
accordingly, and delete the patch script.
*/
changeBuildType("d4320bd8-6094-4dd6-9bed-e13d6f0d12e2") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

@ -1,17 +0,0 @@
package OpenSourceProjects_Storybook.patches.projects
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.Project
import jetbrains.buildServer.configs.kotlin.v2017_2.ui.*
/*
This patch script was generated by TeamCity on settings change in UI.
To apply the patch, change the project with uuid = '69382d9b-7791-418a-9ff6-1c83b86ed6b5' (id = 'OpenSourceProjects_Storybook')
accordingly, and delete the patch script.
*/
changeProject("69382d9b-7791-418a-9ff6-1c83b86ed6b5") {
check(archived == false) {
"Unexpected archived: '$archived'"
}
archived = true
}

View File

@ -1,20 +0,0 @@
package OpenSourceProjects_Storybook.vcsRoots
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.vcs.GitVcsRoot
object OpenSourceProjects_Storybook_HttpsGithubComStorybooksStorybookRefsHeadsMaster : GitVcsRoot({
uuid = "cec03c4b-d52c-42a0-8e9e-53bde85d6b33"
id = "OpenSourceProjects_Storybook_HttpsGithubComStorybooksStorybookRefsHeadsMaster"
name = "Main root"
url = "git@github.com:storybookjs/storybook.git"
branch = "refs/heads/next"
branchSpec = """
+:refs/(pull/*)/head
+:refs/heads/*
""".trimIndent()
authMethod = uploadedKey {
userName = "git"
uploadedKey = "Storybook bot"
}
})

View File

@ -1,16 +0,0 @@
package OpenSourceProjects_Storybook.vcsRoots
import jetbrains.buildServer.configs.kotlin.v2017_2.*
import jetbrains.buildServer.configs.kotlin.v2017_2.vcs.GitVcsRoot
object OpenSourceProjects_Storybook_HttpsGithubComStorybooksStorybookRefsHeadsMaster1 : GitVcsRoot({
uuid = "5cacf90a-381a-4c73-9aa3-57f6439b545e"
id = "OpenSourceProjects_Storybook_HttpsGithubComStorybooksStorybookRefsHeadsMaster1"
name = "https://github.com/storybookjs/storybook#refs/heads/master (1)"
url = "git@github.com:storybookjs/storybook.git"
branch = "refs/heads/next"
authMethod = uploadedKey {
userName = "git"
uploadedKey = "Storybook bot"
}
})

107
.teamcity/pom.xml vendored Normal file
View File

@ -0,0 +1,107 @@
<?xml version="1.0"?>
<project>
<modelVersion>4.0.0</modelVersion>
<name>Hosted_Root Config DSL Script</name>
<groupId>Hosted_Root</groupId>
<artifactId>Hosted_Root_dsl</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>configs-dsl-kotlin-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<repositories>
<repository>
<id>jetbrains-all</id>
<url>https://download.jetbrains.com/teamcity-repository</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>teamcity-server</id>
<url>https://storybook.beta.teamcity.com/app/dsl-plugins-repository</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>JetBrains</id>
<url>https://download.jetbrains.com/teamcity-repository</url>
</pluginRepository>
</pluginRepositories>
<build>
<sourceDirectory>${basedir}</sourceDirectory>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<configuration/>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>process-test-sources</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>teamcity-configs-maven-plugin</artifactId>
<version>${teamcity.dsl.version}</version>
<configuration>
<format>kotlin</format>
<dstDir>target/generated-configs</dstDir>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>configs-dsl-kotlin</artifactId>
<version>${teamcity.dsl.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.teamcity</groupId>
<artifactId>configs-dsl-kotlin-plugins</artifactId>
<version>1.0-SNAPSHOT</version>
<type>pom</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-script-runtime</artifactId>
<version>${kotlin.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
<properties>
<teamcity.dsl.version>2019.2.2-SNAPSHOT</teamcity.dsl.version>
</properties>
</project>

614
.teamcity/settings.kts vendored Normal file
View File

@ -0,0 +1,614 @@
import jetbrains.buildServer.configs.kotlin.v2019_2.*
import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.PullRequests
import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.commitStatusPublisher
import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.pullRequests
import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.swabra
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.ScriptBuildStep
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script
import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.BuildFailureOnMetric
import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.failOnMetricChange
import jetbrains.buildServer.configs.kotlin.v2019_2.projectFeatures.buildReportTab
import jetbrains.buildServer.configs.kotlin.v2019_2.projectFeatures.githubConnection
import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.VcsTrigger
import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs
/*
The settings script is an entry point for defining a TeamCity
project hierarchy. The script should contain a single call to the
project() function with a Project instance or an init function as
an argument.
VcsRoots, BuildTypes, Templates, and subprojects can be
registered inside the project using the vcsRoot(), buildType(),
template(), and subProject() methods respectively.
To debug settings scripts in command-line, run the
mvnDebug org.jetbrains.teamcity:teamcity-configs-maven-plugin:generate
command and attach your debugger to the port 8000.
To debug in IntelliJ Idea, open the 'Maven Projects' tool window (View
-> Tool Windows -> Maven Projects), find the generate task node
(Plugins -> teamcity-configs -> teamcity-configs:generate), the
'Debug' option is available in the context menu for the task.
*/
version = "2019.2"
project {
template(Common)
defaultTemplate = Common
buildType(TestWorkflow)
buildType(Build)
buildType(E2E)
buildType(SmokeTests)
buildType(Frontpage)
buildType(Docs)
buildType(Lint)
buildType(Test)
buildType(Coverage)
subProject(ExamplesProject)
buildTypesOrderIds = arrayListOf(
RelativeId("TestWorkflow"),
RelativeId("Build"),
RelativeId("E2E"),
RelativeId("SmokeTests"),
RelativeId("Frontpage"),
RelativeId("Docs"),
RelativeId("Lint"),
RelativeId("Test"),
RelativeId("Coverage")
)
features {
githubConnection {
id = "PROJECT_EXT_6"
displayName = "GitHub.com"
clientId = "800d730c725f771d6d2a"
clientSecret = "credentialsJSON:d1a5af15-1200-46c6-b0f1-f35bd466d909"
}
buildReportTab {
id = "PROJECT_EXT_8"
title = "Official"
startPage = "built-storybooks.tar.gz!official-storybook/index.html"
}
}
}
object Common: Template({
name = "Common"
vcs {
root(DslContext.settingsRoot)
checkoutMode = CheckoutMode.ON_AGENT
checkoutDir = "storybook/%teamcity.build.branch%"
}
features {
commitStatusPublisher {
id = "Commit status publisher"
publisher = github {
githubUrl = "https://api.github.com"
authType = personalToken {
token = "credentialsJSON:5273320e-14be-4317-951e-a54c4dcca35d"
}
}
param("github_oauth_user", "Hypnosphi")
}
swabra {
id = "swabra"
verbose = true
paths = """
-:.cache
-:node_modules
-:**/node_modules
""".trimIndent()
}
pullRequests {
id = "Pull requests"
provider = github {
authType = vcsRoot()
filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY
}
}
}
})
object Build : BuildType({
name = "Build"
steps {
script {
scriptContent = """
#!/bin/bash
set -e -x
yarn install
yarn repo-dirty-check
yarn bootstrap --core
""".trimIndent()
dockerImage = "node:10"
dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux
}
}
artifactRules = """
+:**/dist/** => dist.tar.gz
+:**/dll/** => dist.tar.gz
-:**/node_modules/** => dist.tar.gz
""".trimIndent()
})
object ExamplesProject : Project({
name = "Examples"
template(ExamplesTemplate)
buildType(Examples1)
buildType(Examples2)
buildType(Examples3)
buildType(Examples4)
buildType(Examples5)
buildType(AggregateExamples)
})
object ExamplesTemplate : Template({
name = "Examples Template"
dependencies {
dependency(Build) {
snapshot {
onDependencyFailure = FailureAction.CANCEL
}
artifacts {
artifactRules = "dist.tar.gz!** => ."
}
}
}
steps {
script {
scriptContent = """
#!/bin/bash
set -e -x
yarn install
rm -rf built-storybooks
mkdir -p built-storybooks
yarn build-storybooks
""".trimIndent()
dockerImage = "buildkite/puppeteer"
dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux
}
}
artifactRules = "built-storybooks => built-storybooks.tar.gz"
params {
param("env.CIRCLE_NODE_TOTAL", "5")
}
})
object Examples1 : BuildType({
name = "Examples 1"
templates = listOf(ExamplesTemplate)
params {
param("env.CIRCLE_NODE_INDEX", "0")
}
disableSettings("Commit status publisher")
})
object Examples2 : BuildType({
name = "Examples 2"
templates = listOf(ExamplesTemplate)
params {
param("env.CIRCLE_NODE_INDEX", "1")
}
disableSettings("Commit status publisher")
})
object Examples3 : BuildType({
name = "Examples 3"
templates = listOf(ExamplesTemplate)
params {
param("env.CIRCLE_NODE_INDEX", "2")
}
disableSettings("Commit status publisher")
})
object Examples4 : BuildType({
name = "Examples 4"
templates = listOf(ExamplesTemplate)
params {
param("env.CIRCLE_NODE_INDEX", "3")
}
disableSettings("Commit status publisher")
})
object Examples5 : BuildType({
name = "Examples 5"
templates = listOf(ExamplesTemplate)
params {
param("env.CIRCLE_NODE_INDEX", "4")
}
disableSettings("Commit status publisher")
})
object AggregateExamples : BuildType({
name = "Aggregate Examples"
dependencies {
dependency(Examples1) {
snapshot {
onDependencyFailure = FailureAction.CANCEL
}
artifacts {
artifactRules = "built-storybooks.tar.gz!** => built-storybooks"
}
}
dependency(Examples2) {
snapshot {
onDependencyFailure = FailureAction.CANCEL
}
artifacts {
artifactRules = "built-storybooks.tar.gz!** => built-storybooks"
}
}
dependency(Examples3) {
snapshot {
onDependencyFailure = FailureAction.CANCEL
}
artifacts {
artifactRules = "built-storybooks.tar.gz!** => built-storybooks"
}
}
dependency(Examples4) {
snapshot {
onDependencyFailure = FailureAction.CANCEL
}
artifacts {
artifactRules = "built-storybooks.tar.gz!** => built-storybooks"
}
}
dependency(Examples5) {
snapshot {
onDependencyFailure = FailureAction.CANCEL
}
artifacts {
artifactRules = "built-storybooks.tar.gz!** => built-storybooks"
}
}
}
artifactRules = "built-storybooks => built-storybooks.tar.gz"
})
object E2E : BuildType({
name = "E2E"
dependencies {
dependency(AggregateExamples) {
snapshot {
onDependencyFailure = FailureAction.CANCEL
}
artifacts {
artifactRules = "built-storybooks.tar.gz!** => built-storybooks"
}
}
}
steps {
script {
scriptContent = """
#!/bin/bash
set -e -x
yarn install
yarn cypress install
yarn serve-storybooks &
yarn await-serve-storybooks
yarn cypress run --reporter teamcity || :
yarn ts-node --transpile-only cypress/report-teamcity-metadata.ts || :
""".trimIndent()
dockerImage = "cypress/base:10.18.1"
dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux
}
}
artifactRules = """
cypress/screenshots => screenshots.tar.gz
cypress/videos => videos.tar.gz
""".trimIndent()
failureConditions {
failOnMetricChange {
metric = BuildFailureOnMetric.MetricType.TEST_COUNT
units = BuildFailureOnMetric.MetricUnit.DEFAULT_UNIT
comparison = BuildFailureOnMetric.MetricComparison.LESS
compareTo = value()
}
}
})
object SmokeTests : BuildType({
name = "Smoke Tests"
dependencies {
dependency(Build) {
snapshot {
onDependencyFailure = FailureAction.CANCEL
}
artifacts {
artifactRules = "dist.tar.gz!** => ."
}
}
}
steps {
script {
scriptContent = """
#!/bin/bash
set -e -x
yarn install
cd examples/cra-kitchen-sink
yarn storybook --smoke-test --quiet
cd ../cra-ts-kitchen-sink
yarn storybook --smoke-test --quiet
cd ../vue-kitchen-sink
yarn storybook --smoke-test --quiet
cd ../svelte-kitchen-sink
yarn storybook --smoke-test --quiet
cd ../angular-cli
yarn storybook --smoke-test --quiet
cd ../ember-cli
yarn storybook --smoke-test --quiet
cd ../marko-cli
yarn storybook --smoke-test --quiet
cd ../official-storybook
yarn storybook --smoke-test --quiet
cd ../mithril-kitchen-sink
yarn storybook --smoke-test --quiet
cd ../riot-kitchen-sink
yarn storybook --smoke-test --quiet
cd ../preact-kitchen-sink
yarn storybook --smoke-test --quiet
cd ../cra-react15
yarn storybook --smoke-test --quiet
""".trimIndent()
dockerImage = "node:10"
dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux
}
}
})
object Frontpage : BuildType({
name = "Frontpage"
type = Type.DEPLOYMENT
steps {
script {
scriptContent = """
#!/bin/bash
set -e -x
yarn bootstrap --install
node ./scripts/build-frontpage.js
""".trimIndent()
dockerImage = "node:10"
dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux
}
}
triggers {
vcs {
quietPeriodMode = VcsTrigger.QuietPeriodMode.USE_DEFAULT
triggerRules = "-:.teamcity/**"
branchFilter = "+:master"
}
}
})
object Docs : BuildType({
name = "Docs"
type = Type.DEPLOYMENT
steps {
script {
workingDir = "docs"
scriptContent = """
#!/bin/bash
set -e -x
yarn install
yarn build
""".trimIndent()
dockerImage = "node:10"
dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux
}
}
triggers {
vcs {
quietPeriodMode = VcsTrigger.QuietPeriodMode.USE_DEFAULT
triggerRules = "-:.teamcity/**"
branchFilter = """
+:<default>
+:next
+:master
+:pull/*
""".trimIndent()
}
}
})
object Lint : BuildType({
name = "Lint"
dependencies {
dependency(Build) {
snapshot {
onDependencyFailure = FailureAction.CANCEL
}
artifacts {
artifactRules = "dist.tar.gz!** => ."
}
}
}
steps {
script {
scriptContent = """
#!/bin/bash
set -e -x
yarn install
# TODO remove after merging
mkdir temp-eslint-teamcity
cd temp-eslint-teamcity
yarn init -y
yarn add -D eslint-teamcity
cd ..
yarn lint:js --format ./temp-eslint-teamcity/node_modules/eslint-teamcity/index.js .
yarn lint:md .
""".trimIndent()
dockerImage = "node:10"
dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux
}
}
failureConditions {
failOnMetricChange {
metric = BuildFailureOnMetric.MetricType.INSPECTION_ERROR_COUNT
threshold = 0
units = BuildFailureOnMetric.MetricUnit.DEFAULT_UNIT
comparison = BuildFailureOnMetric.MetricComparison.MORE
compareTo = value()
}
}
})
object Test : BuildType({
name = "Test"
dependencies {
dependency(Build) {
snapshot {
onDependencyFailure = FailureAction.CANCEL
}
artifacts {
artifactRules = "dist.tar.gz!** => ."
}
}
}
steps {
script {
scriptContent = """
#!/bin/bash
set -e -x
yarn install
# TODO remove after merging
mkdir temp-jest-teamcity
cd temp-jest-teamcity
yarn init -y
yarn add -D jest-teamcity
cd ..
yarn jest --coverage -w 2 --reporters=${'$'}PWD/temp-jest-teamcity/node_modules/jest-teamcity
""".trimIndent()
dockerImage = "node:10"
dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux
}
}
artifactRules = "coverage => coverage.tar.gz"
})
object Coverage : BuildType({
name = "Coverage"
dependencies {
dependency(Test) {
snapshot {
onDependencyFailure = FailureAction.CANCEL
}
artifacts {
artifactRules = "coverage.tar.gz!** => coverage"
}
}
}
steps {
script {
scriptContent = """
#!/bin/bash
set -e -x
yarn install
yarn coverage
""".trimIndent()
dockerImage = "node:10"
dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux
}
}
})
object TestWorkflow : BuildType({
name = "Test Workflow"
type = Type.COMPOSITE
maxRunningBuilds = 2
dependencies {
snapshot(E2E) {}
snapshot(SmokeTests) {}
snapshot(Lint) {}
snapshot(Coverage) {}
}
triggers {
vcs {
quietPeriodMode = VcsTrigger.QuietPeriodMode.USE_DEFAULT
triggerRules = "-:.teamcity/**"
branchFilter = """
+:<default>
+:next
+:master
+:pull/*
""".trimIndent()
}
}
})

View File

@ -5,21 +5,28 @@
| [a11y](addons/a11y) | + | | + | + | + | + | + | + | + | + | + | + |
| [actions](addons/actions) | + | +\* | + | + | + | + | + | + | + | + | + | + |
| [backgrounds](addons/backgrounds) | + | \* | + | + | + | + | + | + | + | + | + | + |
| [centered](addons/centered) | + | | + | + | + | + | | + | | + | + | + |
| [contexts](addons/contexts) | + | | + | | | | | | | | + | + |
| [events](addons/events) | + | | + | + | + | + | + | | | + | + | + |
| [cssresources](addons/cssresources) | + | | + | + | + | + | + | + | + | + | + | + |
| [design assets](addons/design-assets) | + | | + | + | + | + | + | + | + | + | + | + |
| [graphql](addons/graphql) | + | | | | | | | | | | | |
| [docs](addons/docs) | + | | + | + | + | + | + | + | + | + | + | + |
| [events](addons/events) | + | | + | + | + | + | + | | | + | + | + |
| [google-analytics](addons/google-analytics) | + | + | + | + | + | + | + | + | + | + | + | + |
| [info](addons/info) | + | | | | | | | | | | | |
| [graphql](addons/graphql) | + | | | | | | | | | | | |
| [jest](addons/jest) | + | + | + | + | + | + | + | + | + | + | + | + |
| [knobs](addons/knobs) | + | +\* | + | + | + | + | + | + | + | + | + | + |
| [links](addons/links) | + | + | + | + | + | + | | + | + | + | + | + |
| [notes](addons/notes) | + | +\* | + | + | + | + | | + | + | + | + | + |
| [options](addons/options) | + | + | + | + | + | + | | + | + | + | + | + |
| [cssresources](addons/cssresources) | + | | + | + | + | + | + | + | + | + | + | + |
| [query params](addons/queryparams) | + | | + | + | + | + | + | + | + | + | + | + |
| [storyshots](addons/storyshots) | + | + | + | + | | + | | + | + | | + | + |
| [storysource](addons/storysource) | + | | + | + | + | + | + | + | + | + | + | + |
| [viewport](addons/viewport) | + | | + | + | + | + | + | + | + | + | + | + |
`*` - React Native on device addon (addons/onDevice-\<name>)
`*` - 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/storybook/tree/master/addons/info) | + | | | | | | | | | | | |
| [notes](https://github.com/storybookjs/storybook/tree/master/addons/notes) | + | +\* | + | + | + | + | | + | + | + | + | + |
`*` - React Native on device addon (addons/onDevice-\<name>)

File diff suppressed because it is too large Load Diff

View File

@ -30,6 +30,8 @@ cd storybook
yarn bootstrap
```
> NOTE: on windows you may need to run `yarn` before `yarn bootstrap`!
The bootstrap command might ask which sections of the codebase you want to bootstrap. Unless you're going to work with ReactNative or the Documentation, you can keep the default.
You can also pick directly from CLI:
@ -126,6 +128,18 @@ It can be immensely helpful to get feedback in your editor, if you're using VsCo
This should enable auto-fix for all source files, and give linting warnings and errors within your editor.
### 2d. Run Cypress tests
First make sure the repo is bootstrapped.
Then run `yarn build-storybooks`, this creates a static website from all examples.
Then run `yarn serve-storybooks`, this will run the static site on the port cypress expects.
Then run `yarn add cypress -W --optional`. When this has completed cypress should be installed on your system. If it is already on your system, this step can be skipped.
Then run `yarn cypress open` if you want to see the tests run in the UI, or `yarn cypress run` to run the tests headless.
### Reproductions
#### In the monorepo
@ -140,6 +154,8 @@ git clone https://github.com/storybookjs/storybook.git
cd storybook
yarn bootstrap --core
# NOTE: on windows you may need to run `yarn` before `yarn bootstrap`!
# make changes to try and reproduce the problem, such as adding components + stories
cd examples/cra-kitchen-sink
yarn storybook
@ -207,6 +223,8 @@ Before you submit a new PR, make sure you run `yarn test`. Do not submit a PR if
**As a PR submitter**, you should reference the issue if there is one, include a short description of what you contributed and, if it is a code change, instructions for how to manually test out the change. This is informally enforced by our [PR template](https://github.com/storybookjs/storybook/blob/master/.github/PULL_REQUEST_TEMPLATE.md). If your PR is reviewed as only needing trivial changes (e.g. small typos etc), and you have commit access then you can merge the PR after making those changes.
> NOTE: Although the latest stable version of storybook corresponds to the `master` branch, nearly all Storybook development happens in the `next` branch. If you submit a PR, branch off `next` and target your PR to `next`.
**As a PR reviewer**, you should read through the changes and comment on any potential problems. If you see something cool, a kind word never hurts either! Additionally, you should follow the testing instructions and manually test the changes. If the instructions are missing, unclear, or overly complex, feel free to request better instructions from the submitter. Unless the PR is tagged with the `do not merge` label, if you approve the review and there is no other required discussion or changes, you should also go ahead and merge the PR.
## Issue Triage
@ -263,10 +281,11 @@ If you run into trouble here, make sure your node, npm, and **_yarn_** are on th
1. `cd ~` (optional)
2. `git clone https://github.com/storybookjs/storybook.git` _bonus_: use your own fork for this step
3. `cd storybook`
4. `yarn`
5. `yarn bootstrap --core`
6. `yarn test --core`
7. `yarn dev` _You must have this running for your changes to show up_
4. `yarn bootstrap --core`
5. `yarn test --core`
6. `yarn dev` _You must have this running for your changes to show up_
> NOTE: on windows you may need to run `yarn` before `yarn bootstrap` (between steps 3 and 4).
#### Bootstrapping everything
@ -276,6 +295,17 @@ _This method is slow_
2. Take a break 🍵
3. `yarn test` (to verify everything worked)
#### Building specific packages
If you're working on one or a few packages, for every change that you make, you have to rebuild those packages. To make the process easier, there is a CLI command for that:
- Run `yarn build` to bring you a list of packages to select from. There will be also an option to run in watch mode.
- Run `yarn build <package-name>` to build that package specifically. \
For the package name, use its short version. Example: for `@storybook/addon-docs`, run `yarn build addon-docs`.
- Run `yarn build --all` to build everything.
- Add `--watch` to run automatically in watch more if you are either building a selection of packages by name or building all.
Example: `yarn build core addon-docs --watch` or `yarn build --all --watch`.
### Working with the kitchen sink apps
Within the `examples` folder of the Storybook repo, you will find kitchen sink examples of storybook implementations for the various platforms that storybook supports.

View File

@ -1,80 +1,503 @@
# Migration
<h1>Migration</h1>
- [Migration](#migration)
- [From version 5.2.x to 5.3.x](#from-version-52x-to-53x)
- [To main.js configuration](#to-mainjs-configuration)
- [Create React App preset](#create-react-app-preset)
- [Description doc block](#description-doc-block)
- [React Native Async Storage](#react-native-async-storage)
- [Deprecate displayName parameter](#deprecate-displayname-parameter)
- [Unified docs preset](#unified-docs-preset)
- [Simplified hierarchy separators](#simplified-hierarchy-separators)
- [Addon StoryShots Puppeteer uses external puppeteer](#addon-storyshots-puppeteer-uses-external-puppeteer)
- [From version 5.1.x to 5.2.x](#from-version-51x-to-52x)
- [Source-loader](#source-loader)
- [Default viewports](#default-viewports)
- [Grid toolbar-feature](#grid-toolbar-feature)
- [Docs mode docgen](#docs-mode-docgen)
- [storySort option](#storysort-option)
- [From version 5.1.x to 5.1.10](#from-version-51x-to-5110)
- [babel.config.js support](#babelconfigjs-support)
- [From version 5.0.x to 5.1.x](#from-version-50x-to-51x)
- [React native server](#react-native-server)
- [Angular 7](#angular-7)
- [CoreJS 3](#corejs-3)
- [From version 5.0.1 to 5.0.2](#from-version-501-to-502)
- [Deprecate webpack extend mode](#deprecate-webpack-extend-mode)
- [From version 4.1.x to 5.0.x](#from-version-41x-to-50x)
- [sortStoriesByKind](#sortstoriesbykind)
- [Webpack config simplification](#webpack-config-simplification)
- [Theming overhaul](#theming-overhaul)
- [Story hierarchy defaults](#story-hierarchy-defaults)
- [Options addon deprecated](#options-addon-deprecated)
- [Individual story decorators](#individual-story-decorators)
- [Addon backgrounds uses parameters](#addon-backgrounds-uses-parameters)
- [Addon cssresources name attribute renamed](#addon-cssresources-name-attribute-renamed)
- [Addon viewport uses parameters](#addon-viewport-uses-parameters)
- [Addon a11y uses parameters, decorator renamed](#addon-a11y-uses-parameters-decorator-renamed)
- [New keyboard shortcuts defaults](#new-keyboard-shortcuts-defaults)
- [New URL structure](#new-url-structure)
- [Rename of the `--secure` cli parameter to `--https`](#rename-of-the---secure-cli-parameter-to---https)
- [Vue integration](#vue-integration)
- [From version 4.0.x to 4.1.x](#from-version-40x-to-41x)
- [Private addon config](#private-addon-config)
- [React 15.x](#react-15x)
- [From version 3.4.x to 4.0.x](#from-version-34x-to-40x)
- [React 16.3+](#react-163)
- [Generic addons](#generic-addons)
- [Knobs select ordering](#knobs-select-ordering)
- [Knobs URL parameters](#knobs-url-parameters)
- [Keyboard shortcuts moved](#keyboard-shortcuts-moved)
- [Removed addWithInfo](#removed-addwithinfo)
- [Removed RN packager](#removed-rn-packager)
- [Removed RN addons](#removed-rn-addons)
- [Storyshots Changes](#storyshots-changes)
- [Webpack 4](#webpack-4)
- [Babel 7](#babel-7)
- [Create-react-app](#create-react-app)
- [Upgrade CRA1 to babel 7](#upgrade-cra1-to-babel-7)
- [Migrate CRA1 while keeping babel 6](#migrate-cra1-while-keeping-babel-6)
- [start-storybook opens browser](#start-storybook-opens-browser)
- [CLI Rename](#cli-rename)
- [Addon story parameters](#addon-story-parameters)
- [From version 3.3.x to 3.4.x](#from-version-33x-to-34x)
- [From version 3.2.x to 3.3.x](#from-version-32x-to-33x)
- [`babel-core` is now a peer dependency (#2494)](#babel-core-is-now-a-peer-dependency-2494)
- [Base webpack config now contains vital plugins (#1775)](#base-webpack-config-now-contains-vital-plugins-1775)
- [Refactored Knobs](#refactored-knobs)
- [From version 3.1.x to 3.2.x](#from-version-31x-to-32x)
- [Moved TypeScript addons definitions](#moved-typescript-addons-definitions)
- [Updated Addons API](#updated-addons-api)
- [From version 3.0.x to 3.1.x](#from-version-30x-to-31x)
- [Moved TypeScript definitions](#moved-typescript-definitions)
- [Deprecated head.html](#deprecated-headhtml)
- [From version 2.x.x to 3.x.x](#from-version-2xx-to-3xx)
- [Webpack upgrade](#webpack-upgrade)
- [Packages renaming](#packages-renaming)
- [Deprecated embedded addons](#deprecated-embedded-addons)
- [From version 5.3.x to 6.0.x](#from-version-53x-to-60x)
- [CRA preset removed](#cra-preset-removed)
- [Args passed as first argument to story](#args-passed-as-first-argument-to-story)
- [Docs theme separated](#docs-theme-separated)
- [DocsPage slots removed](#docspage-slots-removed)
- [React prop tables with Typescript](#react-prop-tables-with-typescript)
- [React.FC interfaces](#reactfc-interfaces)
- [Imported types](#imported-types)
- [Rolling back](#rolling-back)
- [New addon presets](#new-addon-presets)
- [Removed Deprecated APIs](#removed-deprecated-apis)
- [New setStories event](#new-setstories-event)
- [Client API changes](#client-api-changes)
- [Removed Legacy Story APIs](#removed-legacy-story-apis)
- [Can no longer add decorators/parameters after stories](#can-no-longer-add-decoratorsparameters-after-stories)
- [Changed Parameter Handling](#changed-parameter-handling)
- [Simplified Render Context](#simplified-render-context)
- [Story Store immutable outside of configuration](#story-store-immutable-outside-of-configuration)
- [Improved story source handling](#improved-story-source-handling)
- [6.0 Addon API changes](#60-addon-api-changes)
- [Actions Addon uses parameters](#actions-addon-uses-parameters)
- [Removed action decorator APIs](#removed-action-decorator-apis)
- [Removed withA11y decorator](#removed-witha11y-decorator)
- [6.0 Deprecated addons](#60-deprecated-addons)
- [Deprecated addon-info, addon-notes](#deprecated-addon-info-addon-notes)
- [Deprecated addon-contexts](#deprecated-addon-contexts)
- [Removed addon-centered](#removed-addon-centered)
- [From version 5.2.x to 5.3.x](#from-version-52x-to-53x)
- [To main.js configuration](#to-mainjs-configuration)
- [Using main.js](#using-mainjs)
- [Using preview.js](#using-previewjs)
- [Using manager.js](#using-managerjs)
- [Create React App preset](#create-react-app-preset)
- [Description doc block](#description-doc-block)
- [React Native Async Storage](#react-native-async-storage)
- [Deprecate displayName parameter](#deprecate-displayname-parameter)
- [Unified docs preset](#unified-docs-preset)
- [Simplified hierarchy separators](#simplified-hierarchy-separators)
- [Addon StoryShots Puppeteer uses external puppeteer](#addon-storyshots-puppeteer-uses-external-puppeteer)
- [From version 5.1.x to 5.2.x](#from-version-51x-to-52x)
- [Source-loader](#source-loader)
- [Default viewports](#default-viewports)
- [Grid toolbar-feature](#grid-toolbar-feature)
- [Docs mode docgen](#docs-mode-docgen)
- [storySort option](#storysort-option)
- [From version 5.1.x to 5.1.10](#from-version-51x-to-5110)
- [babel.config.js support](#babelconfigjs-support)
- [From version 5.0.x to 5.1.x](#from-version-50x-to-51x)
- [React native server](#react-native-server)
- [Angular 7](#angular-7)
- [CoreJS 3](#corejs-3)
- [From version 5.0.1 to 5.0.2](#from-version-501-to-502)
- [Deprecate webpack extend mode](#deprecate-webpack-extend-mode)
- [From version 4.1.x to 5.0.x](#from-version-41x-to-50x)
- [sortStoriesByKind](#sortstoriesbykind)
- [Webpack config simplification](#webpack-config-simplification)
- [Theming overhaul](#theming-overhaul)
- [Story hierarchy defaults](#story-hierarchy-defaults)
- [Options addon deprecated](#options-addon-deprecated)
- [Individual story decorators](#individual-story-decorators)
- [Addon backgrounds uses parameters](#addon-backgrounds-uses-parameters)
- [Addon cssresources name attribute renamed](#addon-cssresources-name-attribute-renamed)
- [Addon viewport uses parameters](#addon-viewport-uses-parameters)
- [Addon a11y uses parameters, decorator renamed](#addon-a11y-uses-parameters-decorator-renamed)
- [Addon centered decorator deprecated](#addon-centered-decorator-deprecated)
- [New keyboard shortcuts defaults](#new-keyboard-shortcuts-defaults)
- [New URL structure](#new-url-structure)
- [Rename of the `--secure` cli parameter to `--https`](#rename-of-the---secure-cli-parameter-to---https)
- [Vue integration](#vue-integration)
- [From version 4.0.x to 4.1.x](#from-version-40x-to-41x)
- [Private addon config](#private-addon-config)
- [React 15.x](#react-15x)
- [From version 3.4.x to 4.0.x](#from-version-34x-to-40x)
- [React 16.3+](#react-163)
- [Generic addons](#generic-addons)
- [Knobs select ordering](#knobs-select-ordering)
- [Knobs URL parameters](#knobs-url-parameters)
- [Keyboard shortcuts moved](#keyboard-shortcuts-moved)
- [Removed addWithInfo](#removed-addwithinfo)
- [Removed RN packager](#removed-rn-packager)
- [Removed RN addons](#removed-rn-addons)
- [Storyshots Changes](#storyshots-changes)
- [Webpack 4](#webpack-4)
- [Babel 7](#babel-7)
- [Create-react-app](#create-react-app)
- [Upgrade CRA1 to babel 7](#upgrade-cra1-to-babel-7)
- [Migrate CRA1 while keeping babel 6](#migrate-cra1-while-keeping-babel-6)
- [start-storybook opens browser](#start-storybook-opens-browser)
- [CLI Rename](#cli-rename)
- [Addon story parameters](#addon-story-parameters)
- [From version 3.3.x to 3.4.x](#from-version-33x-to-34x)
- [From version 3.2.x to 3.3.x](#from-version-32x-to-33x)
- [`babel-core` is now a peer dependency (#2494)](#babel-core-is-now-a-peer-dependency-2494)
- [Base webpack config now contains vital plugins (#1775)](#base-webpack-config-now-contains-vital-plugins-1775)
- [Refactored Knobs](#refactored-knobs)
- [From version 3.1.x to 3.2.x](#from-version-31x-to-32x)
- [Moved TypeScript addons definitions](#moved-typescript-addons-definitions)
- [Updated Addons API](#updated-addons-api)
- [From version 3.0.x to 3.1.x](#from-version-30x-to-31x)
- [Moved TypeScript definitions](#moved-typescript-definitions)
- [Deprecated head.html](#deprecated-headhtml)
- [From version 2.x.x to 3.x.x](#from-version-2xx-to-3xx)
- [Webpack upgrade](#webpack-upgrade)
- [Packages renaming](#packages-renaming)
- [Deprecated embedded addons](#deprecated-embedded-addons)
## From version 5.3.x to 6.0.x
### CRA preset removed
The built-in create-react-app preset, which was [previously deprecated](#create-react-app-preset), has been fully removed.
If you're using CRA and migrating from an earlier Storybook version, please install [`@storybook/preset-create-react-app`](https://github.com/storybookjs/presets/tree/master/packages/preset-create-react-app) if you haven't already.
### Args passed as first argument to story
Starting in 6.0, the first argument to a story function is an [Args object](https://github.com/storybookjs/storybook/blob/next/docs/src/pages/formats/component-story-format/index.md#args-story-inputs). In 5.3 and earlier, the first argument was a [StoryContext](https://github.com/storybookjs/storybook/blob/next/lib/addons/src/types.ts#L49-L61), and that context is now passed as the second argument by default.
This breaking change only affects you if your stories actually use the context, which is not common. If you have any stories that use the context, you can either (1) update your stories, or (2) set a flag to opt-out of new behavior.
Consider the following story that uses the context:
```js
export const Dummy = ({ parameters }) => <div>{JSON.stringify(parameters)}</div>;
```
Here's an updated story for 6.0 that ignores the args object:
```js
export const Dummy = (_args, { parameters }) => <div>{JSON.stringify(parameters)}</div>;
```
Alternatively, if you want to opt out of the new behavior, you can add the following to your `.storybook/preview.js` config:
```js
export const parameters = {
passArgsFirst: false,
};
```
### Docs theme separated
In 6.0, you should theme Storybook Docs with the `docs.theme` parameter.
In 5.x, the Storybook UI and Storybook Docs were themed using the same theme object. However, in 5.3 we introduced a new API, `addons.setConfig`, which improved UI theming but broke Docs theming. Rather than trying to keep the two unified, we introduced a separate theming mechanism for docs, `docs.theme`. [Read about Docs theming here](https://github.com/storybookjs/storybook/blob/next/addons/docs/docs/theming.md#storybook-theming).
### DocsPage slots removed
In SB5.2, we introduced the concept of [DocsPage slots](https://github.com/storybookjs/storybook/blob/0de8575eab73bfd5c5c7ba5fe33e53a49b92db3a/addons/docs/docs/docspage.md#docspage-slots) for customizing the DocsPage.
In 5.3, we introduced `docs.x` story parameters like `docs.prepareForInline` which get filled in by frameworks and can also be overwritten by users, which is a more natural/convenient way to make global customizations.
We also introduced introduced [Custom DocsPage](https://github.com/storybookjs/storybook/blob/next/addons/docs/docs/docspage.md#replacing-docspage), which makes it possible to add/remove/update DocBlocks on the page.
These mechanisms are superior to slots, so we've removed slots in 6.0. For each slot, we provide a migration path here:
| Slot | Slot function | Replacement |
| ----------- | ----------------- | -------------------------------------------- |
| Title | `titleSlot` | Custom DocsPage |
| Subtitle | `subtitleSlot` | Custom DocsPage |
| Description | `descriptionSlot` | `docs.extractComponentDescription` parameter |
| Primary | `primarySlot` | Custom DocsPage |
| Props | `propsSlot` | `docs.extractProps` parameter |
| Stories | `storiesSlot` | Custom DocsPage |
### React prop tables with Typescript
Starting in 6.0 we are changing our recommended setup for extracting prop tables in `addon-docs` for React projects using TypeScript.
In earlier versions, we recommended `react-docgen-typescript-loader` (`RDTL`) and bundled it with `@storybook/preset-create-react-app` and `@storybook/preset-typescript` for this reason. We now recommend `babel-plugin-react-docgen`, which is already bundled as part of `@storybook/react`.
As a consequence we've removed `RDTL` from the presets, which is a breaking change. We made this change because `react-docgen` now supports TypeScript natively, and fewer dependencies simplifies things for everybody.
The Babel-based `react-docgen` version is the default in:
- `@storybook/preset-create-react-app` @ `^2.1.0`
- `@storybook/preset-typescript` @ `^3.0.0`
> NOTE: If you're using `preset-create-react-app` you don't need `preset-typescript`!
We will be updating this section with migration information as we collect information from our users, and fixing issues as they come up throughout the 6.0 prerelease process. We are cataloging known issues [here](https://github.com/storybookjs/storybook/blob/next/addons/docs/docs/props-tables.md#known-limitations).
#### React.FC interfaces
The biggest known issue is https://github.com/reactjs/react-docgen/issues/387, which means that the following common pattern **DOESN'T WORK**:
```tsx
import React, { FC } from 'react';
interface IProps { ... };
const MyComponent: FC<IProps> = ({ ... }) => ...
```
The following workaround is needed:
```tsx
const MyComponent: FC<IProps> = ({ ... }: IProps) => ...
```
Please upvote https://github.com/reactjs/react-docgen/issues/387 if this is affecting your productivity, or better yet, submit a fix!
#### Imported types
Another major issue is support for imported types.
```tsx
import React, { FC } from 'react';
import SomeType from './someFile';
type NewType = SomeType & { foo: string };
const MyComponent: FC<NewType> = ...
```
This isn't an issue with `RDTL` so unfortunately it gets worse with `react-docgen`.
There's an open PR for this https://github.com/reactjs/react-docgen/pull/352 which you can upvote if it affects you.
#### Rolling back
In the meantime, if you're not ready to make the move you have two options:
1. Pin your to a specific preset version: `preset-create-react-app@1.5.2` or `preset-typescript@1.2.2`
2. OR: Manually configure your setup to add back `react-docgen-typescript-loader`, add the following to your `.storybook/main.js`:
```js
module.exports = {
webpack: async (config, { configType }) => ({
...config,
module: {
...config.module,
rules: [
...config.module.rules,
{
test: /\.tsx?$/,
loader: require.resolve('react-docgen-typescript-loader'),
options: {}, // your options here
},
],
},
}),
};
```
### New addon presets
In Storybook 5.3 we introduced a declarative [main.js configuration](#to-mainjs-configuration), which is now the recommended way to configure Storybook. Part of the change is a simplified syntax for registering addons, which in 6.0 automatically registers many addons _using a preset_, which is a slightly different behavior than in earlier versions.
This breaking change currently applies to: `addon-a11y`, `addon-actions`, `addon-knobs`, `addon-links`, `addon-queryparams`.
Consider the following `main.js` config for the accessibility addon, `addon-knobs`:
```js
module.exports = {
stories: ['../**/*.stories.js'],
addons: ['@storybook/addon-knobs'],
};
```
In earlier versions of Storybook, this would automatically call `@storybook/addon-knobs/register`, which adds the the knobs panel to the Storybook UI. As a user you would also add a decorator:
```js
import { withKnobs } from '../index';
addDecorator(withKnobs);
```
Now in 6.0, `addon-knobs` comes with a preset, `@storybook/addon-knobs/preset`, that does this automatically for you. This change simplifies configuration, since now you don't need to add that decorator.
If you wish to disable this new behavior, you can modify your `main.js` to force it to use the `register` logic rather than the `preset`:
```js
module.exports = {
stories: ['../**/*.stories.js'],
addons: ['@storybook/addon-knobs/register'],
};
```
If you wish to selectively disable `knobs` checks for a subset of stories, you can control this with story parameters:
```js
export const MyNonCheckedStory = () => <SomeComponent />;
MyNonCheckedStory.story = {
parameters: {
knobs: { disable: true },
},
};
```
### Removed Deprecated APIs
In 6.0 we removed a number of APIs that were previously deprecated.
See the migration guides for further details:
- [Addon a11y uses parameters, decorator renamed](#addon-a11y-uses-parameters-decorator-renamed)
- [Addon backgrounds uses parameters](#addon-backgrounds-uses-parameters)
- [Source-loader](#source-loader)
- [Unified docs preset](#unified-docs-preset)
- [Addon centered decorator deprecated](#addon-centered-decorator-deprecated)
### New setStories event
The `setStories`/`SET_STORIES` event has changed and now denormalizes global and kind-level parameters. The new format of the event data is:
```js
{
globalParameters: { p: 'q' },
kindParameters: { kind: { p: 'q' } },
stories: /* as before but with only story-level parameters */
}
```
If you want the full denormalized parameters for a story, you can do something like:
```js
import { combineParameters } from '@storybook/api';
const story = data.stories[storyId];
const parameters = combineParameters(
data.globalParameters,
data.kindParameters[story.kind],
story.parameters
);
```
### Client API changes
#### Removed Legacy Story APIs
In 6.0 we removed a set of APIs from the underlying `StoryStore` (which wasn't publicly accessible):
- `getStories`, `getStoryFileName`, `getStoryAndParameters`, `getStory`, `getStoryWithContext`, `hasStoryKind`, `hasStory`, `dumpStoryBook`, `size`, `clean`
Although these were private APIs, if you were using them, you could probably use the newer APIs (which are still private): `getStoriesForKind`, `getRawStory`, `removeStoryKind`, `remove`.
#### Can no longer add decorators/parameters after stories
You can no longer add decorators and parameters globally after you added your first story, and you can no longer add decorators and parameters to a kind after you've added your first story to it.
It's unclear and confusing what would happened if you did. If you want to disable a decorator for certain stories, use a parameter to do so:
```js
export StoryOne = ...;
StoryOne.story = { parameters: { addon: { disable: true } } };
```
If you want to use a parameter for a subset of stories in a kind, simply use a variable to do so:
```js
const commonParameters = { x: { y: 'z' } };
export StoryOne = ...;
StoryOne.story = { parameters: { ...commonParameters, other: 'things' } };
```
#### Changed Parameter Handling
There have been a few rationalizations of parameter handling in 6.0 to make things more predictable and fit better with the intention of parameters:
_All parameters are now merged recursively to arbitrary depth._
In 5.3 we sometimes merged parameters all the way down and sometimes did not depending on where you added them. It was confusing. If you were relying on this behaviour, let us know.
_Array parameters are no longer "merged"._
If you override an array parameter, the override will be the end product. If you want the old behaviour (appending a new value to an array parameter), export the original and use array spread. This will give you maximum flexibility:
```js
import { allBackgrounds } from './util/allBackgrounds';
export StoryOne = ...;
StoryOne.story = { parameters: { backgrounds: [...allBackgrounds, '#zyx' ] } };
```
_You cannot set parameters from decorators_
Parameters are intended to be statically set at story load time. So setting them via a decorator doesn't quite make sense. If you were using this to control the rendering of a story, chances are using the new `args` feature is a more idiomatic way to do this.
_You can only set storySort globally_
If you want to change the ordering of stories, use `export const parameters = { options: { storySort: ... } }` in `preview.js`.
### Simplified Render Context
The `RenderContext` that is passed to framework rendering layers in order to render a story has been simplified, dropping a few members that were not used by frameworks to render stories. In particular, the following have been removed:
- `selectedKind`/`selectedStory` -- replaced by `kind`/`name`
- `configApi`
- `storyStore`
- `channel`
- `clientApi`
### Story Store immutable outside of configuration
You can no longer change the contents of the StoryStore outside of a `configure()` call. This is to ensure that any changes are properly published to the manager. If you want to add stories "out of band" you can call `store.startConfiguring()` and `store.finishConfiguring()` to ensure that your changes are published.
### Improved story source handling
The story source code handling has been improved in both `addon-storysource` and `addon-docs`.
In 5.x some users used an undocumented _internal_ API, `mdxSource` to customize source snippetization in `addon-docs`. This has been removed in 6.0.
The preferred way to customize source snippets for stories is now:
```js
export const Example = () => <Button />;
Example.story = {
parameters: {
storySource: {
source: 'custom source',
},
},
};
```
The MDX analog:
```jsx
<Story name="Example" parameters={{ storySource: { source: 'custom source' } }}>
<Button />
</Story>
```
### 6.0 Addon API changes
#### Actions Addon uses parameters
Leveraging the new preset `@storybook/addon-actions` uses parameters to pass action options. If you previously had:
```js
import { withactions } from `@storybook/addon-actions`;
export StoryOne = ...;
StoryOne.story = {
decorators: [withActions('mouseover', 'click .btn')],
}
```
You should replace it with:
```js
export StoryOne = ...;
StoryOne.story = {
parameters: { actions: ['mouseover', 'click .btn'] },
}
```
#### Removed action decorator APIs
In 6.0 we removed the actions addon decorate API. Actions handles can be configured globaly, for a collection of stories or per story via parameters. The ability to manipulate the data arguments of an event is only relevant in a few frameworks and is not a common enough usecase to be worth the complexity of supporting.
#### Removed withA11y decorator
In 6.0 we removed the `withA11y` decorator. The code that runs accessibility checks is now directly injected in the preview.
Remove the addon-a11y decorator.
To configure a11y now, you have to specify configuration using `addParameters`.
```js
addParameters({
a11y: {
element: "#root",
config: {},
options: {},
manual: true,
}
};
```
### 6.0 Deprecated addons
We've deprecated the following addons in 6.0: `addon-info`, `addon-notes`, `addon-contexts`, `addon-centered`.
#### Deprecated addon-info, addon-notes
The info/notes addons have been replaced by [addon-docs](https://github.com/storybookjs/storybook/tree/next/addons/docs). We've documented a migration in the [docs recipes](https://github.com/storybookjs/storybook/blob/next/addons/docs/docs/recipes.md#migrating-from-notesinfo-addons).
Both addons are still widely used, and their source code is still available in the [deprecated-addons repo](https://github.com/storybookjs/deprecated-addons). We're looking for maintainers for both addons. If you're interested, please get in touch on [our Discord](https://discordapp.com/invite/UUt2PJb).
#### Deprecated addon-contexts
The contexts addon has been replaced by [addon-toolbars](https://github.com/storybookjs/storybook/blob/next/addons/toolbars), which is simpler, more ergonomic, and compatible with all Storybook frameworks.
The addon's source code is still available in the [deprecated-addons repo](https://github.com/storybookjs/deprecated-addons). If you're interested in maintaining it, please get in touch on [our Discord](https://discordapp.com/invite/UUt2PJb).
#### Removed addon-centered
In 6.0 we removed the centered addon. Centering is now core feature of storybook, so w no longer need an addon.
Remove the addon-centered decorator and instead add a `layout` parameter:
```js
export const MyStory = () => <div>my story</div>;
MyStory.story = {
parameters: { layout: 'centered' },
};
```
Other possible values are: `padded` (default) and `fullscreen`.
## From version 5.2.x to 5.3.x
@ -152,6 +575,8 @@ addons.setConfig({
This makes storybook load and use the theme in the manager directly.
This allows for richer theming in the future, and has a much better performance!
> If you're using addon-docs, you should probably not do this. Docs uses the theme as well, but this change makes the theme inaccessible to addon-docs. We'll address this in 6.0.0.
### Create React App preset
You can now move to the new preset for [Create React App](https://create-react-app.dev/). The in-built preset for Create React App will be disabled in Storybook 6.0.
@ -176,14 +601,14 @@ To avoid that now you have to manually pass asyncStorage to React Native Storybo
Solution:
- Use `require('@react-native-community/async-storage').AsyncStorage` for React Native v0.59 and above.
- Use `require('@react-native-community/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').AsyncStorage || require('react-native').AsyncStorage || null
asyncStorage: require('@react-native-community/async-storage').default || require('react-native').AsyncStorage || null
});
```
@ -212,7 +637,7 @@ yarn sb migrate upgrade-hierarchy-separators --glob="*.stories.js"
If you were using `|` and wish to keep the "root" behavior, use the `showRoots: true` option to re-enable roots:
```js
addParameters({
addParameters({
options: {
showRoots: true,
},
@ -224,13 +649,14 @@ NOTE: it is no longer possible to have some stories with roots and others withou
### Addon StoryShots Puppeteer uses external puppeteer
To give you more control on the Chrome version used when running StoryShots Puppeteer, `puppeteer` is no more included in the addon dependencies. So you can now pick the version of `puppeteer` you want and set it in your project.
If you want the latest version available just run:
```sh
yarn add puppeteer --dev
OR
npm install puppeteer --save-dev
```
```
## From version 5.1.x to 5.2.x
@ -409,7 +835,7 @@ var sortedModules = modules.slice().sort((a, b) => {
});
// execute them
sortedModules.forEach(key => {
sortedModules.forEach((key) => {
context(key);
});
```
@ -642,6 +1068,26 @@ Furthermore, the decorator `checkA11y` has been deprecated and renamed to `withA
See the [a11y addon README](https://github.com/storybookjs/storybook/blob/master/addons/a11y/README.md) for more information.
### Addon centered decorator deprecated
If you previously had:
```js
import centered from '@storybook/addon-centered';
```
You should replace it with the React or Vue version as appropriate
```js
import centered from '@storybook/addon-centered/react';
```
or
```js
import centered from '@storybook/addon-centered/vue';
```
### New keyboard shortcuts defaults
Storybook's keyboard shortcuts are updated in 5.0, but they are configurable via the menu so if you want to set them back you can:
@ -953,7 +1399,7 @@ Here's an example of using Notes and Info in 3.2 with the new API.
storiesOf('composition', module).add(
'new addons api',
withInfo('see Notes panel for composition info')(
withNotes({ text: 'Composition: Info(Notes())' })(context => (
withNotes({ text: 'Composition: Info(Notes())' })((context) => (
<MyComponent name={context.story} />
))
)

View File

@ -109,21 +109,21 @@ For additional help, join us [in our Discord](https://discord.gg/sMFvFsG) or [Sl
### Supported Frameworks
| Framework | Demo | |
| -------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [React](app/react) | [v5.1.0](https://storybooks-official.netlify.com) | [![React](https://img.shields.io/npm/dm/@storybook/react.svg)](app/react) |
| [React Native](app/react-native) | - | [![React Native](https://img.shields.io/npm/dm/@storybook/react-native.svg)](app/react-native) |
| [Vue](app/vue) | [v5.1.0](https://storybooks-vue.netlify.com/) | [![Vue](https://img.shields.io/npm/dm/@storybook/vue.svg)](app/vue) |
| [Angular](app/angular) | [v5.1.0](https://storybooks-angular.netlify.com/) | [![Angular](https://img.shields.io/npm/dm/@storybook/angular.svg)](app/angular) |
| [Marionette.js](app/marionette) | - | [![Marionette.js](https://img.shields.io/npm/dm/@storybook/marionette.svg)](app/marionette) |
| [Mithril](app/mithril) | [v5.1.0](https://storybooks-mithril.netlify.com/) | [![Mithril](https://img.shields.io/npm/dm/@storybook/mithril.svg)](app/mithril) |
| [Marko](app/marko) | [v5.1.0](https://storybooks-marko.netlify.com/) | [![Marko](https://img.shields.io/npm/dm/@storybook/marko.svg)](app/marko) |
| [HTML](app/html) | [v5.1.0](https://storybooks-html.netlify.com/) | [![HTML](https://img.shields.io/npm/dm/@storybook/html.svg)](app/html) |
| [Svelte](app/svelte) | [v5.1.0](https://storybooks-svelte.netlify.com/) | [![Svelte](https://img.shields.io/npm/dm/@storybook/svelte.svg)](app/svelte) |
| [Riot](app/riot) | [v5.1.0](https://storybooks-riot.netlify.com/) | [![Riot](https://img.shields.io/npm/dm/@storybook/riot.svg)](app/riot) |
| [Ember](app/ember) | [v5.1.0](https://storybooks-ember.netlify.com/) | [![Ember](https://img.shields.io/npm/dm/@storybook/ember.svg)](app/ember) |
| [Preact](app/preact) | [v5.1.0](https://storybooks-preact.netlify.com/) | [![Preact](https://img.shields.io/npm/dm/@storybook/preact.svg)](app/preact) |
| [Rax](app/rax) | [v5.1.0](https://storybooks-rax.netlify.com/) | [![Rax](https://img.shields.io/npm/dm/@storybook/rax.svg)](app/rax) |
| Framework | Demo | |
| -------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [React](app/react) | [v5.3.0](https://storybookjs.netlify.com/official-storybook/?path=/story/*) | [![React](https://img.shields.io/npm/dm/@storybook/react.svg)](app/react) |
| [React Native](app/react-native) | - | [![React Native](https://img.shields.io/npm/dm/@storybook/react-native.svg)](app/react-native) |
| [Vue](app/vue) | [v5.3.0](https://storybookjs.netlify.com/vue-kitchen-sink/) | [![Vue](https://img.shields.io/npm/dm/@storybook/vue.svg)](app/vue) |
| [Angular](app/angular) | [v5.3.0](https://storybookjs.netlify.com/angular-cli/) | [![Angular](https://img.shields.io/npm/dm/@storybook/angular.svg)](app/angular) |
| [Marionette.js](app/marionette) | - | [![Marionette.js](https://img.shields.io/npm/dm/@storybook/marionette.svg)](app/marionette) |
| [Mithril](app/mithril) | [v5.3.0](https://storybookjs.netlify.com/mithril-kitchen-sink/) | [![Mithril](https://img.shields.io/npm/dm/@storybook/mithril.svg)](app/mithril) |
| [Marko](app/marko) | [v5.3.0](https://storybookjs.netlify.com/marko-cli/) | [![Marko](https://img.shields.io/npm/dm/@storybook/marko.svg)](app/marko) |
| [HTML](app/html) | [v5.3.0](https://storybookjs.netlify.com/html-kitchen-sink/) | [![HTML](https://img.shields.io/npm/dm/@storybook/html.svg)](app/html) |
| [Svelte](app/svelte) | [v5.3.0](https://storybookjs.netlify.com/svelte-kitchen-sink/) | [![Svelte](https://img.shields.io/npm/dm/@storybook/svelte.svg)](app/svelte) |
| [Riot](app/riot) | [v5.3.0](https://storybookjs.netlify.com/riot-kitchen-sink/) | [![Riot](https://img.shields.io/npm/dm/@storybook/riot.svg)](app/riot) |
| [Ember](app/ember) | [v5.3.0](https://storybookjs.netlify.com/ember-cli/) | [![Ember](https://img.shields.io/npm/dm/@storybook/ember.svg)](app/ember) |
| [Preact](app/preact) | [v5.3.0](https://storybookjs.netlify.com/preact-kitchen-sink/) | [![Preact](https://img.shields.io/npm/dm/@storybook/preact.svg)](app/preact) |
| [Rax](app/rax) | [v5.3.0](https://storybookjs.netlify.com/rax-kitchen-sink/) | [![Rax](https://img.shields.io/npm/dm/@storybook/rax.svg)](app/rax) |
### Sub Projects
@ -137,25 +137,34 @@ For additional help, join us [in our Discord](https://discord.gg/sMFvFsG) or [Sl
| [a11y](addons/a11y/) | Test components for user accessibility in Storybook |
| [actions](addons/actions/) | Log actions as users interact with components in the Storybook UI |
| [backgrounds](addons/backgrounds/) | Let users choose backgrounds in the Storybook UI |
| [centered](addons/centered/) | Center the alignment of your components within the Storybook UI |
| [contexts](addons/contexts/) | Interactively inject component contexts for stories in the Storybook UI |
| [cssresources](addons/cssresources/) | Dynamically add/remove css resources to the component iframe |
| [design assets](addons/design-assets/) | View images, videos, weblinks alongside your story |
| [docs](addons/docs/) | Add high quality documentation to your components |
| [events](addons/events/) | Interactively fire events to components that respond to EventEmitter |
| [graphql](addons/graphql/) | Query a GraphQL server within Storybook stories |
| [google-analytics](addons/google-analytics) | Reports google analytics on stories |
| [info](addons/info/) | Annotate stories with extra component usage information |
| [graphql](addons/graphql/) | Query a GraphQL server within Storybook stories |
| [jest](addons/jest/) | View the results of components' unit tests in Storybook |
| [knobs](addons/knobs/) | Interactively edit component prop data in the Storybook UI |
| [links](addons/links/) | Create links between stories |
| [notes](addons/notes/) | Annotate Storybook stories with notes |
| [options](addons/options/) | Customize the Storybook UI in code |
| [query params](addons/queryparams/) | Mock query params |
| [storyshots](addons/storyshots/) | Snapshot testing for components in Storybook |
| [storysource](addons/storysource/) | View the code of your stories within the Storybook UI |
| [viewport](addons/viewport/) | Change display sizes and layouts for responsive components using Storybook |
See [Addon / Framework Support Table](ADDONS_SUPPORT.md)
### Deprecated Addons
| Addons | |
| ------------------------------------------- | -------------------------------------------------------------------------- |
| [info](https://github.com/storybookjs/storybook/tree/master/addons/info) | Annotate stories with extra component usage information |
| [notes](https://github.com/storybookjs/storybook/tree/master/addons/notes) | Annotate Storybook stories with notes |
In order to continue improving your experience, we have to eventually deprecate certain addons in favor of new, better tools.
If you're using info/notes, we highly recommend you to migrate to [docs](addons/docs/) instead, and [here is a guide](addons/docs/docs/recipes.md#migrating-from-notesinfo-addons) to help you.
## Badges & Presentation materials
We have a badge! Link it to your live Storybook example.

View File

@ -1,88 +0,0 @@
# Roadmap
## Table of contents
* [New features](#new-features)
+ [Responsive + multi-device viewports preview.](#responsive--multi-device-viewports-preview)
+ [Automatic story detection](#automatic-story-detection)
+ [Theme ability and override core UI components](#theme-ability-and-override-core-ui-components)
+ [Add a playground addon](#add-a-playground-addon)
+ [See multiple (or all) stories in 1 preview.](#see-multiple--or-all--stories-in-1-preview)
* [Supporting other frameworks and libraries](#supporting-other-frameworks-and-libraries)
+ [Aurelia](#aurelia)
* [Breaking changes](#breaking-changes)
+ [Addon API](#addon-api)
+ [API for adding stories](#api-for-adding-stories)
* [Documentation](#documentation)
+ [Better design](#better-design)
+ [Record videos and write blog post on how to use, tweak & develop storybook](#record-videos-and-write-blog-post-on-how-to-use--tweak---develop-storybook)
## New features
Doing these will be backwards compatible.
### Responsive + multi-device viewports preview.
If you're smart about it you can already view the preview on multiple devices and windows. It's an iframe after-all.
But story selection and addon-settings are not synced.
We want to make this much much simpler and a core feature of storybook.
### Automatic story detection
Some tools are doing automatic file detection, jest for example.
We think such a feature is highly needed. A lot of users are already hacking this themselves using webpack specific features.
### Themeability and override core UI components
Storybook is often used inside product companies and agencies. We want to help them have a sense of quality and immersion.
We're interested in full customizability of our UI, though addons and options.
### Add a playground addon
Many other styleguide-type projects have what's called a playground, where developers can change the code rendering the component inside the app.
Storybook has a very tight connection with your editor, and it has a knobs addon.
But we still see value in an addon that will allow the workflow of a playground.
### See multiple (or all) stories in 1 preview.
Storybook's philosophy is about describing small bits in a variety of states.
However, some components are best understood when viewed in multiple varieties in 1 view.
It's quite common to see users write a single story, with wrapper components and multiple instances of the component the story is about.
We plan to add a second mode to storybook that will allow you to see all stories in 1 preview.
That way you can write your stories how they are best, and preview them how you like.
## Supporting other frameworks and libraries
We believe in the power of react, and think it's the right choice for a lot of projects.
But it's up to you and your team to decide your stack.
Unfortunately, if you choose anything not from the list of [supported frameworks](README.md#supported-frameworks) you can not use storybook.
We want you to be able to use storybook with the framework / library of your choice.
### Aurelia
We're reaching out to the Aurelia maintainers to cooperate on this.
## Breaking changes
### Addon API
Our addon api is limited and will eventually have to be improved to accommodate better more optimized and modern addons.
### API for adding stories
Currently, it's getting hard to set up a story that has data/options for multiple addons.
We want to support this but will likely mean we will have to change the `add` method's API.
## Documentation
### Better design
We have a new logo, so the next step is an overhaul of our documentation site.
### Record videos and write blog posts on how to use, tweak & develop storybook
- writing addons,
- choosing the right addons.
- how to start developing on our codebase.
- how to use storybook itself and the CLI.

View File

@ -13,11 +13,15 @@ function __setMockFiles(newMockFiles) {
// A custom version of `readdirSync` that reads from the special mocked out
// file list set via __setMockFiles
const readFileSync = (filePath = '') => mockFiles[filePath];
const existsSync = 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.readFileSync = readFileSync;
fs.existsSync = existsSync;
fs.lstatSync = lstatSync;
module.exports = fs;

View File

@ -18,35 +18,35 @@ Add this line to your `main.js` file (create this file inside your storybook con
```js
module.exports = {
addons: ['@storybook/addon-a11y/register']
}
addons: ['@storybook/addon-a11y'],
};
```
import the `withA11y` decorator to check your stories for violations within your components.
```js
import React from 'react';
import { withA11y } from '@storybook/addon-a11y';
export default {
title: 'button',
decorators: [withA11y],
};
export const accessible = () => (
<button>
Accessible button
</button>
);
export const accessible = () => <button>Accessible button</button>;
export const inaccessible = () => (
<button style={{ backgroundColor: 'red', color: 'darkRed', }}>
Inaccessible button
</button>
<button style={{ backgroundColor: 'red', color: 'darkRed' }}>Inaccessible button</button>
);
```
If you wish to selectively disable `a11y` checks for a subset of stories, you can control this with story parameters:
```js
export const MyNonCheckedStory = () => <SomeComponent />;
MyNonCheckedStory.story = {
parameters: {
a11y: { disable: true },
},
};
```
## Parameters
For more customizability use parameters to configure [aXe options](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#api-name-axeconfigure).
@ -56,11 +56,8 @@ You can override these options [at story level too](https://storybook.js.org/doc
import React from 'react';
import { storiesOf, addDecorator, addParameters } from '@storybook/react';
import { withA11y } from '@storybook/addon-a11y';
export default {
title: 'button',
decorators: [withA11y],
parameters: {
a11y: {
// optional selector which element to inspect
@ -69,29 +66,23 @@ export default {
config: {},
// axe-core optionsParameter (https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#options-parameter)
options: {},
// optional flag to prevent the automatic check
// optional flag to prevent the automatic check
manual: true,
},
},
};
export const accessible = () => (
<button>
Accessible button
</button>
);
export const accessible = () => <button>Accessible button</button>;
export const inaccessible = () => (
<button style={{ backgroundColor: 'red', color: 'darkRed', }}>
Inaccessible button
</button>
<button style={{ backgroundColor: 'red', color: 'darkRed' }}>Inaccessible button</button>
);
```
## Roadmap
* Make UI accessible
* Show in story where violations are.
* Add more example tests
* Add tests
* Make CI integration possible
- Make UI accessible
- Show in story where violations are.
- Add more example tests
- Add tests
- Make CI integration possible

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-a11y",
"version": "6.0.0-alpha.1",
"version": "6.0.0-beta.1",
"description": "a11y addon for storybook",
"keywords": [
"a11y",
@ -20,41 +20,53 @@
"directory": "addons/a11y"
},
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts"
"*.d.ts",
"ts3.5/**/*"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.0.0-alpha.1",
"@storybook/api": "6.0.0-alpha.1",
"@storybook/client-logger": "6.0.0-alpha.1",
"@storybook/components": "6.0.0-alpha.1",
"@storybook/core-events": "6.0.0-alpha.1",
"@storybook/theming": "6.0.0-alpha.1",
"axe-core": "^3.3.2",
"@storybook/addons": "6.0.0-beta.1",
"@storybook/api": "6.0.0-beta.1",
"@storybook/channels": "6.0.0-beta.1",
"@storybook/client-api": "6.0.0-beta.1",
"@storybook/client-logger": "6.0.0-beta.1",
"@storybook/components": "6.0.0-beta.1",
"@storybook/core-events": "6.0.0-beta.1",
"@storybook/theming": "6.0.0-beta.1",
"axe-core": "^3.5.2",
"core-js": "^3.0.1",
"global": "^4.3.2",
"memoizerific": "^1.11.3",
"react": "^16.8.3",
"react-redux": "^7.0.2",
"lodash": "^4.17.15",
"react-sizeme": "^2.5.2",
"redux": "^4.0.1",
"ts-dedent": "^1.1.0",
"util-deprecate": "^1.0.2"
"regenerator-runtime": "^0.13.3"
},
"devDependencies": {
"@types/react-redux": "^7.0.6",
"@types/webpack-env": "^1.15.0"
"@testing-library/react": "^10.0.4",
"@types/webpack-env": "^1.15.2",
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
},
"publishConfig": {
"access": "public"
},
"gitHead": "4b9d901add9452525135caae98ae5f78dd8da9ff"
"gitHead": "4b9d901add9452525135caae98ae5f78dd8da9ff",
"typesVersions": {
"<=3.5": {
"*": [
"ts3.5/*"
]
}
}
}

1
addons/a11y/preset.js Normal file
View File

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

View File

@ -0,0 +1,46 @@
import { document } from 'global';
import addons from '@storybook/addons';
import { STORY_CHANGED } from '@storybook/core-events';
import { EVENTS, HIGHLIGHT_STYLE_ID } from './constants';
import { higlightStyle } from './highlight';
if (module && module.hot && module.hot.decline) {
module.hot.decline();
}
interface HighlightInfo {
/** html selector of the element */
elements: string[];
color: string;
}
const channel = addons.getChannel();
const highlight = (infos: HighlightInfo) => {
const id = HIGHLIGHT_STYLE_ID;
resetHighlight();
const sheet = document.createElement('style');
sheet.setAttribute('id', id);
sheet.innerHTML = infos.elements
.map(
(target) =>
`${target}{
${higlightStyle(infos.color)}
}`
)
.join(' ');
document.head.appendChild(sheet);
};
const resetHighlight = () => {
const id = HIGHLIGHT_STYLE_ID;
const sheetToBeRemoved = document.getElementById(id);
if (sheetToBeRemoved) {
sheetToBeRemoved.parentNode.removeChild(sheetToBeRemoved);
}
};
channel.on(STORY_CHANGED, resetHighlight);
channel.on(EVENTS.HIGHLIGHT, highlight);

View File

@ -0,0 +1,25 @@
import addons from '@storybook/addons';
import { EVENTS } from './constants';
jest.mock('@storybook/addons');
const mockedAddons = addons as jest.Mocked<typeof addons>;
describe('a11yRunner', () => {
let mockChannel: { on: jest.Mock; emit?: jest.Mock };
beforeEach(() => {
mockedAddons.getChannel.mockReset();
mockChannel = { on: jest.fn(), emit: jest.fn() };
mockedAddons.getChannel.mockReturnValue(mockChannel as any);
});
it('should listen to events', () => {
// eslint-disable-next-line global-require
require('./a11yRunner');
expect(mockedAddons.getChannel).toHaveBeenCalled();
expect(mockChannel.on).toHaveBeenCalledWith(EVENTS.REQUEST, expect.any(Function));
expect(mockChannel.on).toHaveBeenCalledWith(EVENTS.MANUAL, expect.any(Function));
});
});

View File

@ -0,0 +1,58 @@
import { document, window } from 'global';
import axe from 'axe-core';
import addons from '@storybook/addons';
import { EVENTS } from './constants';
import { Setup } from './params';
if (module && module.hot && module.hot.decline) {
module.hot.decline();
}
const channel = addons.getChannel();
let active = false;
const getElement = () => {
const storyRoot = document.getElementById('story-root');
return storyRoot ? storyRoot.children : document.getElementById('root');
};
const run = async (storyId: string) => {
try {
const input = getParams(storyId);
if (!active) {
active = true;
channel.emit(EVENTS.RUNNING);
const { element = getElement(), config, options } = input;
axe.reset();
if (config) {
axe.configure(config);
}
const result = await axe.run(element, options);
channel.emit(EVENTS.RESULT, result);
}
} catch (error) {
channel.emit(EVENTS.ERROR, error);
} finally {
active = false;
}
};
/** Returns story parameters or default ones. */
const getParams = (storyId: string): Setup => {
// eslint-disable-next-line no-underscore-dangle
const { parameters } = window.__STORYBOOK_STORY_STORE__._stories[storyId] || {};
return (
parameters.a11y || {
config: {},
options: {
restoreScroll: true,
},
}
);
};
channel.on(EVENTS.REQUEST, run);
channel.on(EVENTS.MANUAL, run);

View File

@ -1,170 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import { EventEmitter } from 'events';
import { ThemeProvider, themes, convert } from '@storybook/theming';
import { A11YPanel } from './A11YPanel';
import { EVENTS } from '../constants';
function createApi() {
const emitter = new EventEmitter();
jest.spyOn(emitter, 'emit');
jest.spyOn(emitter, 'on');
jest.spyOn(emitter, 'off');
return emitter;
}
const axeResult = {
incomplete: [
{
id: 'color-contrast',
impact: 'serious',
tags: ['cat.color', 'wcag2aa', 'wcag143'],
description:
'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
help: 'Elements must have sufficient color contrast',
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/color-contrast?application=axeAPI',
nodes: [],
},
],
passes: [
{
id: 'aria-allowed-attr',
impact: null,
tags: ['cat.aria', 'wcag2a', 'wcag412'],
description: "Ensures ARIA attributes are allowed for an element's role",
help: 'Elements must only use allowed ARIA attributes',
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/aria-allowed-attr?application=axeAPI',
nodes: [],
},
],
violations: [
{
id: 'color-contrast',
impact: 'serious',
tags: ['cat.color', 'wcag2aa', 'wcag143'],
description:
'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
help: 'Elements must have sufficient color contrast',
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/color-contrast?application=axeAPI',
nodes: [],
},
],
};
function ThemedA11YPanel(props) {
return (
<ThemeProvider theme={convert(themes.light)}>
<A11YPanel {...props} />
</ThemeProvider>
);
}
describe('A11YPanel', () => {
it('should register event listener on mount', () => {
// given
const api = createApi();
expect(api.on).not.toHaveBeenCalled();
// when
mount(<ThemedA11YPanel api={api} />);
// then
expect(api.on.mock.calls.length).toBe(3);
expect(api.on.mock.calls[0][0]).toBe(EVENTS.RESULT);
expect(api.on.mock.calls[1][0]).toBe(EVENTS.ERROR);
expect(api.on.mock.calls[2][0]).toBe(EVENTS.MANUAL);
});
it('should deregister event listener on unmount', () => {
// given
const api = createApi();
expect(api.off).not.toHaveBeenCalled();
// when
const wrapper = mount(<ThemedA11YPanel api={api} />);
wrapper.unmount();
// then
expect(api.off.mock.calls.length).toBe(3);
expect(api.off.mock.calls[0][0]).toBe(EVENTS.RESULT);
expect(api.off.mock.calls[1][0]).toBe(EVENTS.ERROR);
expect(api.off.mock.calls[2][0]).toBe(EVENTS.MANUAL);
});
it('should handle "initial" status', () => {
// given
const api = createApi();
// when
const wrapper = mount(<ThemedA11YPanel api={api} active />);
// then
expect(api.emit).not.toHaveBeenCalled();
expect(wrapper.text()).toMatch(/Initializing/);
});
it('should handle "manual" status', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} active />);
// when
api.emit(EVENTS.MANUAL, true);
wrapper.update();
// then
expect(wrapper.text()).toMatch(/Manually run the accessibility scan/);
expect(api.emit).not.toHaveBeenCalledWith(EVENTS.REQUEST);
});
it('should handle "running" status', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} active />);
// when
api.emit(EVENTS.MANUAL, false);
wrapper.update();
// then
expect(wrapper.text()).toMatch(/Please wait while the accessibility scan is running/);
expect(api.emit).toHaveBeenCalledWith(EVENTS.REQUEST);
});
it('should handle "ran" status', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} active />);
// when
api.emit(EVENTS.RESULT, axeResult);
wrapper.update();
// then
expect(
wrapper
.find('button')
.last()
.text()
.trim()
).toBe('Tests completed');
expect(wrapper.find('Tabs').prop('tabs').length).toBe(3);
expect(wrapper.find('Tabs').prop('tabs')[0].label.props.children).toEqual([1, ' Violations']);
expect(wrapper.find('Tabs').prop('tabs')[1].label.props.children).toEqual([1, ' Passes']);
expect(wrapper.find('Tabs').prop('tabs')[2].label.props.children).toEqual([1, ' Incomplete']);
});
it('should handle inactive state', () => {
// given
const api = createApi();
// when
const wrapper = mount(<ThemedA11YPanel api={api} active={false} />);
// then
expect(wrapper.text()).toBe('');
expect(api.emit).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,139 @@
import React from 'react';
import { render, waitFor, fireEvent, act } from '@testing-library/react';
import { ThemeProvider, themes, convert } from '@storybook/theming';
import * as api from '@storybook/api';
import { A11YPanel } from './A11YPanel';
import { EVENTS } from '../constants';
jest.mock('@storybook/api');
const mockedApi = api as jest.Mocked<typeof api>;
const axeResult = {
incomplete: [
{
id: 'color-contrast',
impact: 'serious',
tags: ['cat.color', 'wcag2aa', 'wcag143'],
description:
'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
help: 'Elements must have sufficient color contrast',
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/color-contrast?application=axeAPI',
nodes: [],
},
],
passes: [
{
id: 'aria-allowed-attr',
impact: null,
tags: ['cat.aria', 'wcag2a', 'wcag412'],
description: "Ensures ARIA attributes are allowed for an element's role",
help: 'Elements must only use allowed ARIA attributes',
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/aria-allowed-attr?application=axeAPI',
nodes: [],
},
],
violations: [
{
id: 'color-contrast',
impact: 'serious',
tags: ['cat.color', 'wcag2aa', 'wcag143'],
description:
'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
help: 'Elements must have sufficient color contrast',
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/color-contrast?application=axeAPI',
nodes: [],
},
],
};
function ThemedA11YPanel() {
return (
<ThemeProvider theme={convert(themes.light)}>
<A11YPanel />
</ThemeProvider>
);
}
describe('A11YPanel', () => {
beforeEach(() => {
mockedApi.useChannel.mockReset();
mockedApi.useParameter.mockReset();
mockedApi.useStorybookState.mockReset();
mockedApi.useAddonState.mockReset();
mockedApi.useChannel.mockReturnValue(jest.fn());
mockedApi.useParameter.mockReturnValue({ manual: false });
const state: Partial<api.State> = { storyId: 'jest' };
// Lazy to mock entire state
mockedApi.useStorybookState.mockReturnValue(state as any);
mockedApi.useAddonState.mockImplementation(React.useState);
});
it('should render', () => {
const { container } = render(<A11YPanel />);
expect(container.firstChild).toBeTruthy();
});
it('should register event listener on mount', () => {
render(<A11YPanel />);
expect(mockedApi.useChannel).toHaveBeenCalledWith(
expect.objectContaining({
[EVENTS.RESULT]: expect.any(Function),
[EVENTS.ERROR]: expect.any(Function),
})
);
});
it('should handle "initial" status', () => {
const { getByText } = render(<A11YPanel />);
expect(getByText(/Initializing/)).toBeTruthy();
});
it('should handle "manual" status', async () => {
mockedApi.useParameter.mockReturnValue({ manual: true });
const { getByText } = render(<ThemedA11YPanel />);
await waitFor(() => {
expect(getByText(/Manually run the accessibility scan/)).toBeTruthy();
});
});
describe('running', () => {
it('should handle "running" status', async () => {
const emit = jest.fn();
mockedApi.useChannel.mockReturnValue(emit);
mockedApi.useParameter.mockReturnValue({ manual: true });
const { getByRole, getByText } = render(<ThemedA11YPanel />);
await waitFor(() => {
const button = getByRole('button', { name: 'Run test' });
fireEvent.click(button);
});
await waitFor(() => {
expect(getByText(/Please wait while the accessibility scan is running/)).toBeTruthy();
expect(emit).toHaveBeenCalledWith(EVENTS.MANUAL, 'jest');
});
});
it('should set running status on event', async () => {
const { getByText } = render(<ThemedA11YPanel />);
const useChannelArgs = mockedApi.useChannel.mock.calls[0][0];
act(() => useChannelArgs[EVENTS.RUNNING]());
await waitFor(() => {
expect(getByText(/Please wait while the accessibility scan is running/)).toBeTruthy();
});
});
});
it('should handle "ran" status', async () => {
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();
});
});
});

View File

@ -1,18 +1,17 @@
/* eslint-disable react/destructuring-assignment,default-case,consistent-return,no-case-declarations */
import React, { Component, Fragment } from 'react';
import React, { useCallback, useMemo } from 'react';
import { styled } from '@storybook/theming';
import { ActionBar, Icons, ScrollArea } from '@storybook/components';
import { AxeResults, Result } from 'axe-core';
import { API } from '@storybook/api';
import { Provider } from 'react-redux';
import { AxeResults } from 'axe-core';
import { useChannel, useParameter, useStorybookState, useAddonState } from '@storybook/api';
import { Report } from './Report';
import { Tabs } from './Tabs';
import { EVENTS } from '../constants';
import store, { clearElements } from '../redux-config';
import { useA11yContext } from './A11yContext';
import { EVENTS, ADDON_ID } from '../constants';
import { A11yParameters } from '../params';
export enum RuleType {
VIOLATION,
@ -26,7 +25,7 @@ const Icon = styled(Icons)({
marginRight: 4,
});
const RotatingIcon = styled(Icon)(({ theme }) => ({
const RotatingIcon = styled(Icon)<{}>(({ theme }) => ({
animation: `${theme.animation.rotate360} 1s linear infinite;`,
}));
@ -49,228 +48,138 @@ const Centered = styled.span<{}>({
height: '100%',
});
interface InitialState {
status: 'initial';
}
type Status = 'initial' | 'manual' | 'running' | 'error' | 'ran' | 'ready';
interface ManualState {
status: 'manual';
}
export const A11YPanel: React.FC = () => {
const [status, setStatus] = useAddonState<Status>(ADDON_ID, 'initial');
const [error, setError] = React.useState<unknown>(undefined);
const { setResults, results } = useA11yContext();
const { storyId } = useStorybookState();
const { manual } = useParameter<Pick<A11yParameters, 'manual'>>('a11y', {
manual: false,
});
interface RunningState {
status: 'running';
}
React.useEffect(() => {
setStatus(manual ? 'manual' : 'initial');
}, [manual]);
interface RanState {
status: 'ran';
passes: Result[];
violations: Result[];
incomplete: Result[];
}
const handleResult = (axeResults: AxeResults) => {
setStatus('ran');
setResults(axeResults);
interface ReadyState {
status: 'ready';
passes: Result[];
violations: Result[];
incomplete: Result[];
}
interface ErrorState {
status: 'error';
error: unknown;
}
type A11YPanelState =
| InitialState
| ManualState
| RunningState
| RanState
| ReadyState
| ErrorState;
interface A11YPanelProps {
active: boolean;
api: API;
}
export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
state: A11YPanelState = {
status: 'initial',
};
componentDidMount() {
const { api } = this.props;
api.on(EVENTS.RESULT, this.onResult);
api.on(EVENTS.ERROR, this.onError);
api.on(EVENTS.MANUAL, this.onManual);
}
componentDidUpdate(prevProps: A11YPanelProps) {
// TODO: might be able to remove this
const { active } = this.props;
if (!prevProps.active && active) {
// removes all elements from the redux map in store from the previous panel
store.dispatch(clearElements());
}
}
componentWillUnmount() {
const { api } = this.props;
api.off(EVENTS.RESULT, this.onResult);
api.off(EVENTS.ERROR, this.onError);
api.off(EVENTS.MANUAL, this.onManual);
}
onResult = ({ passes, violations, incomplete }: AxeResults) => {
this.setState(
{
status: 'ran',
passes,
violations,
incomplete,
},
() => {
setTimeout(() => {
const { status } = this.state;
if (status === 'ran') {
this.setState({
status: 'ready',
});
}
}, 900);
setTimeout(() => {
if (status === 'ran') {
setStatus('ready');
}
);
}, 900);
};
onError = (error: unknown) => {
this.setState({
status: 'error',
error,
});
};
const handleRun = useCallback(() => {
setStatus('running');
}, []);
onManual = (manual: boolean) => {
if (manual) {
this.setState({
status: 'manual',
});
} else {
this.request();
}
};
const handleError = useCallback((err: unknown) => {
setStatus('error');
setError(err);
}, []);
request = () => {
const { api } = this.props;
this.setState(
const emit = useChannel({
[EVENTS.RUNNING]: handleRun,
[EVENTS.RESULT]: handleResult,
[EVENTS.ERROR]: handleError,
});
const handleManual = useCallback(() => {
setStatus('running');
emit(EVENTS.MANUAL, storyId);
}, [storyId]);
const manualActionItems = useMemo(() => [{ title: 'Run test', onClick: handleManual }], [
handleManual,
]);
const readyActionItems = useMemo(
() => [
{
status: 'running',
},
() => {
api.emit(EVENTS.REQUEST);
// removes all elements from the redux map in store from the previous panel
store.dispatch(clearElements());
}
);
};
render() {
const { active } = this.props;
if (!active) return null;
switch (this.state.status) {
case 'initial':
return <Centered>Initializing...</Centered>;
case 'manual':
return (
<Fragment>
<Centered>Manually run the accessibility scan.</Centered>
<ActionBar
key="actionbar"
actionItems={[{ title: 'Run test', onClick: this.request }]}
/>
</Fragment>
);
case 'running':
return (
<Centered>
<RotatingIcon inline icon="sync" /> Please wait while the accessibility scan is running
...
</Centered>
);
case 'ready':
case 'ran':
const { passes, violations, incomplete, status } = this.state;
const actionTitle =
title:
status === 'ready' ? (
'Rerun tests'
) : (
<Fragment>
<>
<Icon inline icon="check" /> Tests completed
</Fragment>
);
return (
<Provider store={store}>
<ScrollArea vertical horizontal>
<Tabs
key="tabs"
tabs={[
{
label: <Violations>{violations.length} Violations</Violations>,
panel: (
<Report
items={violations}
type={RuleType.VIOLATION}
empty="No accessibility violations found."
/>
),
items: violations,
type: RuleType.VIOLATION,
},
{
label: <Passes>{passes.length} Passes</Passes>,
panel: (
<Report
items={passes}
type={RuleType.PASS}
empty="No accessibility checks passed."
/>
),
items: passes,
type: RuleType.PASS,
},
{
label: <Incomplete>{incomplete.length} Incomplete</Incomplete>,
panel: (
<Report
items={incomplete}
type={RuleType.INCOMPLETION}
empty="No accessibility checks incomplete."
/>
),
items: incomplete,
type: RuleType.INCOMPLETION,
},
]}
/>
</ScrollArea>
<ActionBar
key="actionbar"
actionItems={[{ title: actionTitle, onClick: this.request }]}
/>
</Provider>
);
case 'error':
const { error } = this.state;
return (
<Centered>
The accessibility scan encountered an error.
<br />
{error}
</Centered>
);
}
}
}
</>
),
onClick: handleManual,
},
],
[status, handleManual]
);
const tabs = useMemo(() => {
const { passes, incomplete, violations } = results;
return [
{
label: <Violations>{violations.length} Violations</Violations>,
panel: (
<Report
items={violations}
type={RuleType.VIOLATION}
empty="No accessibility violations found."
/>
),
items: violations,
type: RuleType.VIOLATION,
},
{
label: <Passes>{passes.length} Passes</Passes>,
panel: (
<Report items={passes} type={RuleType.PASS} empty="No accessibility checks passed." />
),
items: passes,
type: RuleType.PASS,
},
{
label: <Incomplete>{incomplete.length} Incomplete</Incomplete>,
panel: (
<Report
items={incomplete}
type={RuleType.INCOMPLETION}
empty="No accessibility checks incomplete."
/>
),
items: incomplete,
type: RuleType.INCOMPLETION,
},
];
}, [results]);
return (
<>
{status === 'initial' && <Centered>Initializing...</Centered>}
{status === 'manual' && (
<>
<Centered>Manually run the accessibility scan.</Centered>
<ActionBar key="actionbar" actionItems={manualActionItems} />
</>
)}
{status === 'running' && (
<Centered>
<RotatingIcon inline icon="sync" /> Please wait while the accessibility scan is running
...
</Centered>
)}
{(status === 'ready' || status === 'ran') && (
<>
<ScrollArea vertical horizontal>
<Tabs key="tabs" tabs={tabs} />
</ScrollArea>
<ActionBar key="actionbar" actionItems={readyActionItems} />
</>
)}
{status === 'error' && (
<Centered>
The accessibility scan encountered an error.
<br />
{typeof error === 'string' ? error : JSON.stringify(error)}
</Centered>
)}
</>
);
};

View File

@ -0,0 +1,133 @@
import * as React from 'react';
import { AxeResults } from 'axe-core';
import { render, act } from '@testing-library/react';
import * as api from '@storybook/api';
import { STORY_CHANGED } from '@storybook/core-events';
import { A11yContextProvider, useA11yContext } from './A11yContext';
import { EVENTS } from '../constants';
jest.mock('@storybook/api');
const mockedApi = api as jest.Mocked<typeof api>;
const storyId = 'jest';
const axeResult: Partial<AxeResults> = {
incomplete: [
{
id: 'color-contrast',
impact: 'serious',
tags: [],
description:
'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
help: 'Elements must have sufficient color contrast',
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/color-contrast?application=axeAPI',
nodes: [],
},
],
passes: [
{
id: 'aria-allowed-attr',
impact: undefined,
tags: [],
description: "Ensures ARIA attributes are allowed for an element's role",
help: 'Elements must only use allowed ARIA attributes',
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/aria-allowed-attr?application=axeAPI',
nodes: [],
},
],
violations: [
{
id: 'color-contrast',
impact: 'serious',
tags: [],
description:
'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
help: 'Elements must have sufficient color contrast',
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/color-contrast?application=axeAPI',
nodes: [],
},
],
};
describe('A11YPanel', () => {
beforeEach(() => {
mockedApi.useChannel.mockReset();
mockedApi.useStorybookState.mockReset();
mockedApi.useChannel.mockReturnValue(jest.fn());
const state: Partial<api.State> = { storyId };
// Lazy to mock entire state
mockedApi.useStorybookState.mockReturnValue(state as any);
});
it('should render children', () => {
const { getByTestId } = render(
<A11yContextProvider active>
<div data-testid="child" />
</A11yContextProvider>
);
expect(getByTestId('child')).toBeTruthy();
});
it('should not render when inactive', () => {
const emit = jest.fn();
mockedApi.useChannel.mockReturnValue(emit);
const { queryByTestId } = render(
<A11yContextProvider active={false}>
<div data-testid="child" />
</A11yContextProvider>
);
expect(queryByTestId('child')).toBeFalsy();
expect(emit).not.toHaveBeenCalledWith(EVENTS.REQUEST);
});
it('should emit request when moving from inactive to active', () => {
const emit = jest.fn();
mockedApi.useChannel.mockReturnValue(emit);
const { rerender } = render(<A11yContextProvider active={false} />);
rerender(<A11yContextProvider active />);
expect(emit).toHaveBeenLastCalledWith(EVENTS.REQUEST, storyId);
});
it('should emit highlight with no values when inactive', () => {
const emit = jest.fn();
mockedApi.useChannel.mockReturnValue(emit);
const { rerender } = render(<A11yContextProvider active />);
rerender(<A11yContextProvider active={false} />);
expect(emit).toHaveBeenLastCalledWith(
EVENTS.HIGHLIGHT,
expect.objectContaining({
color: expect.any(String),
elements: [],
})
);
});
it('should emit highlight with no values when story changed', () => {
const Component = () => {
const { results, setResults } = useA11yContext();
// As any because of unit tests...
React.useEffect(() => setResults(axeResult as any), []);
return (
<>
{!!results.passes.length && <div data-testid="anyPassesResults" />}
{!!results.incomplete.length && <div data-testid="anyIncompleteResults" />}
{!!results.violations.length && <div data-testid="anyViolationsResults" />}
</>
);
};
const { queryByTestId } = render(
<A11yContextProvider active>
<Component />
</A11yContextProvider>
);
expect(queryByTestId('anyPassesResults')).toBeTruthy();
expect(queryByTestId('anyIncompleteResults')).toBeTruthy();
expect(queryByTestId('anyViolationsResults')).toBeTruthy();
const useChannelArgs = mockedApi.useChannel.mock.calls[0][0];
act(() => useChannelArgs[STORY_CHANGED]());
expect(queryByTestId('anyPassesResults')).toBeFalsy();
expect(queryByTestId('anyIncompleteResults')).toBeFalsy();
expect(queryByTestId('anyViolationsResults')).toBeFalsy();
});
});

View File

@ -0,0 +1,117 @@
import * as React from 'react';
import { themes, convert } from '@storybook/theming';
import { Result } from 'axe-core';
import { useChannel, useStorybookState } from '@storybook/api';
import { STORY_CHANGED, STORY_RENDERED } from '@storybook/core-events';
import { EVENTS } from '../constants';
interface Results {
passes: Result[];
violations: Result[];
incomplete: Result[];
}
interface A11yContextStore {
results: Results;
setResults: (results: Results) => void;
highlighted: string[];
toggleHighlight: (target: string[], highlight: boolean) => void;
clearHighlights: () => void;
tab: number;
setTab: (index: number) => void;
}
const colorsByType = [
convert(themes.normal).color.negative, // VIOLATION,
convert(themes.normal).color.positive, // PASS,
convert(themes.normal).color.warning, // INCOMPLETION,
];
export const A11yContext = React.createContext<A11yContextStore>({
results: {
passes: [],
incomplete: [],
violations: [],
},
setResults: () => {},
highlighted: [],
toggleHighlight: () => {},
clearHighlights: () => {},
tab: 0,
setTab: () => {},
});
interface A11yContextProviderProps {
active: boolean;
}
const defaultResult = {
passes: [],
incomplete: [],
violations: [],
};
export const A11yContextProvider: React.FC<A11yContextProviderProps> = ({ active, ...props }) => {
const [results, setResults] = React.useState<Results>(defaultResult);
const [tab, setTab] = React.useState(0);
const [highlighted, setHighlighted] = React.useState<string[]>([]);
const { storyId } = useStorybookState();
const handleToggleHighlight = React.useCallback((target: string[], highlight: boolean) => {
setHighlighted((prevHighlighted) =>
highlight
? [...prevHighlighted, ...target]
: prevHighlighted.filter((t) => !target.includes(t))
);
}, []);
const handleRun = React.useCallback(() => {
emit(EVENTS.REQUEST, storyId);
}, [storyId]);
const handleClearHighlights = React.useCallback(() => setHighlighted([]), []);
const handleSetTab = React.useCallback((index: number) => {
handleClearHighlights();
setTab(index);
}, []);
const handleReset = React.useCallback(() => {
setTab(0);
setResults(defaultResult);
// Highlights is cleared by a11yHighlights.ts
}, []);
const emit = useChannel({
[STORY_RENDERED]: handleRun,
[STORY_CHANGED]: handleReset,
});
React.useEffect(() => {
emit(EVENTS.HIGHLIGHT, { elements: highlighted, color: colorsByType[tab] });
}, [highlighted, tab]);
React.useEffect(() => {
if (active) {
handleRun();
} else {
handleClearHighlights();
}
}, [active, handleClearHighlights, emit, storyId]);
if (!active) return null;
return (
<A11yContext.Provider
value={{
results,
setResults,
highlighted,
toggleHighlight: handleToggleHighlight,
clearHighlights: handleClearHighlights,
tab,
setTab: handleSetTab,
}}
{...props}
/>
);
};
export const useA11yContext = () => React.useContext(A11yContext);

View File

@ -1,15 +1,27 @@
import { document } from 'global';
import React, { FunctionComponent, ReactNode, useState } from 'react';
import memoize from 'memoizerific';
import { styled } from '@storybook/theming';
import { logger } from '@storybook/client-logger';
import { Global, styled } from '@storybook/theming';
import { Icons, IconButton, WithTooltip, TooltipLinkList } from '@storybook/components';
const getIframe = memoize(1)(() => document.getElementById('storybook-preview-iframe'));
import { Filters } from './ColorFilters';
const getFilter = (filter: string | null) => {
if (filter === null) {
const iframeId = 'storybook-preview-iframe';
const baseList = [
'protanopia',
'protanomaly',
'deuteranopia',
'deuteranomaly',
'tritanopia',
'tritanomaly',
'achromatopsia',
'achromatomaly',
'mono',
] as const;
type Filter = typeof baseList[number] | null;
const getFilter = (filter: Filter) => {
if (!filter) {
return 'none';
}
if (filter === 'mono') {
@ -18,7 +30,15 @@ const getFilter = (filter: string | null) => {
return `url('#${filter}')`;
};
const ColorIcon = styled.span<{ filter: string | null }>(
const Hidden = styled.div(() => ({
'&, & svg': {
position: 'absolute',
width: 0,
height: 0,
},
}));
const ColorIcon = styled.span<{ filter: Filter }>(
{
background: 'linear-gradient(to right, #F44336, #FF9800, #FFEB3B, #8BC34A, #2196F3, #9C27B0)',
borderRadius: '1rem',
@ -34,18 +54,6 @@ const ColorIcon = styled.span<{ filter: string | null }>(
})
);
const baseList = [
'protanopia',
'protanomaly',
'deuteranopia',
'deuteranomaly',
'tritanopia',
'tritanomaly',
'achromatopsia',
'achromatomaly',
'mono',
];
export interface Link {
id: string;
title: ReactNode;
@ -54,7 +62,7 @@ export interface Link {
onClick: () => void;
}
const getColorList = (active: string | null, set: (i: string | null) => void): Link[] => [
const getColorList = (active: Filter, set: (i: Filter) => void): Link[] => [
...(active !== null
? [
{
@ -68,7 +76,7 @@ const getColorList = (active: string | null, set: (i: string | null) => void): L
},
]
: []),
...baseList.map(i => ({
...baseList.map((i) => ({
id: i,
title: i.charAt(0).toUpperCase() + i.slice(1),
onClick: () => {
@ -80,36 +88,39 @@ const getColorList = (active: string | null, set: (i: string | null) => void): L
];
export const ColorBlindness: FunctionComponent = () => {
const [active, setActiveState] = useState<string | null>(null);
const setActive = (activeState: string | null): void => {
const iframe = getIframe();
if (iframe) {
iframe.style.filter = getFilter(activeState);
setActiveState(activeState);
} else {
logger.error('Cannot find Storybook iframe');
}
};
const [filter, setFilter] = useState<Filter>(null);
return (
<WithTooltip
placement="top"
trigger="click"
tooltip={({ onHide }) => {
const colorList = getColorList(active, i => {
setActive(i);
onHide();
});
return <TooltipLinkList links={colorList} />;
}}
closeOnClick
onDoubleClick={() => setActive(null)}
>
<IconButton key="filter" active={!!active} title="Color Blindness Emulation">
<Icons icon="mirror" />
</IconButton>
</WithTooltip>
<>
{filter && (
<Global
styles={{
[`#${iframeId}`]: {
filter: getFilter(filter),
},
}}
/>
)}
<WithTooltip
placement="top"
trigger="click"
tooltip={({ onHide }) => {
const colorList = getColorList(filter, (i) => {
setFilter(i);
onHide();
});
return <TooltipLinkList links={colorList} />;
}}
closeOnClick
onDoubleClick={() => setFilter(null)}
>
<IconButton key="filter" active={!!filter} title="Color Blindness Emulation">
<Icons icon="mirror" />
</IconButton>
</WithTooltip>
<Hidden>
<Filters />
</Hidden>
</>
);
};

View File

@ -0,0 +1,64 @@
import * as React from 'react';
export const Filters: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg {...props}>
<defs>
<filter id="protanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.567, 0.433, 0, 0, 0 0.558, 0.442, 0, 0, 0 0, 0.242, 0.758, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="protanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.817, 0.183, 0, 0, 0 0.333, 0.667, 0, 0, 0 0, 0.125, 0.875, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="deuteranopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.625, 0.375, 0, 0, 0 0.7, 0.3, 0, 0, 0 0, 0.3, 0.7, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="deuteranomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.8, 0.2, 0, 0, 0 0.258, 0.742, 0, 0, 0 0, 0.142, 0.858, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="tritanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.95, 0.05, 0, 0, 0 0, 0.433, 0.567, 0, 0 0, 0.475, 0.525, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="tritanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.967, 0.033, 0, 0, 0 0, 0.733, 0.267, 0, 0 0, 0.183, 0.817, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="achromatopsia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.299, 0.587, 0.114, 0, 0 0.299, 0.587, 0.114, 0, 0 0.299, 0.587, 0.114, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="achromatomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.618, 0.320, 0.062, 0, 0 0.163, 0.775, 0.062, 0, 0 0.163, 0.320, 0.516, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
</defs>
</svg>
);

View File

@ -36,19 +36,13 @@ const Element: FunctionComponent<ElementProps> = ({ element, type }) => {
const { any, all, none } = element;
const rules = [...any, ...all, ...none];
const highlightToggleId = `${type}-${element.target[0]}`;
const highlightLabel = `Highlight`;
return (
<Item>
<ItemTitle>
{element.target[0]}
<HighlightToggleElement>
<HighlightToggle
toggleId={highlightToggleId}
type={type}
elementsToHighlight={[element]}
label={highlightLabel}
/>
<HighlightToggle toggleId={highlightToggleId} elementsToHighlight={[element]} />
</HighlightToggleElement>
</ItemTitle>
<Rules rules={rules} />

View File

@ -1,40 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import { ThemeProvider, themes, convert } from '@storybook/theming';
import HighlightToggle from './HighlightToggle';
import store from '../../redux-config';
function ThemedHighlightToggle(props) {
return (
<ThemeProvider theme={convert(themes.normal)}>
<HighlightToggle {...props} />
</ThemeProvider>
);
}
describe('HighlightToggle component', () => {
test('should render', () => {
// given
const wrapper = mount(
<Provider store={store}>
<ThemedHighlightToggle />
</Provider>
);
// then
expect(wrapper.exists()).toBe(true);
});
test('should match snapshot', () => {
// given
const wrapper = mount(
<Provider store={store}>
<ThemedHighlightToggle />
</Provider>
);
// then
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,93 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { NodeResult } from 'axe-core';
import HighlightToggle from './HighlightToggle';
import { A11yContext } from '../A11yContext';
const nodeResult = (target: string): NodeResult => ({
html: '',
target: [target],
any: [],
all: [],
none: [],
});
const defaultProviderValue = {
results: {
passes: [],
incomplete: [],
violations: [],
},
setResults: jest.fn(),
highlighted: [],
toggleHighlight: jest.fn(),
clearHighlights: jest.fn(),
tab: 0,
setTab: jest.fn(),
};
describe('<HighlightToggle />', () => {
it('should render', () => {
const { container } = render(<HighlightToggle elementsToHighlight={[nodeResult('#root')]} />);
expect(container.firstChild).toBeTruthy();
});
it('should be checked when all targets are highlighted', () => {
const { getByRole } = render(
<A11yContext.Provider
value={{
...defaultProviderValue,
highlighted: ['#root'],
}}
>
<HighlightToggle elementsToHighlight={[nodeResult('#root')]} />
</A11yContext.Provider>
);
const checkbox = getByRole('checkbox') as HTMLInputElement;
expect(checkbox.checked).toBeTruthy();
});
it('should be mixed when some targets are highlighted', () => {
const { getByRole } = render(
<A11yContext.Provider
value={{
...defaultProviderValue,
highlighted: ['#root'],
}}
>
<HighlightToggle elementsToHighlight={[nodeResult('#root'), nodeResult('#root1')]} />
</A11yContext.Provider>
);
const checkbox = getByRole('checkbox') as HTMLInputElement;
expect(checkbox.indeterminate).toBeTruthy();
});
describe('toggleHighlight', () => {
it.each`
highlighted | elementsToHighlight | expected
${[]} | ${['#root']} | ${true}
${['#root']} | ${['#root']} | ${false}
${['#root']} | ${['#root', '#root1']} | ${true}
`(
'should be triggerd with $expected when highlighted is $highlighted and elementsToHighlight is $elementsToHighlight',
({ highlighted, elementsToHighlight, expected }) => {
const { getByRole } = render(
<A11yContext.Provider
value={{
...defaultProviderValue,
highlighted,
}}
>
<HighlightToggle elementsToHighlight={elementsToHighlight.map(nodeResult)} />
</A11yContext.Provider>
);
const checkbox = getByRole('checkbox') as HTMLInputElement;
fireEvent.click(checkbox);
expect(defaultProviderValue.toggleHighlight).toHaveBeenCalledWith(
elementsToHighlight,
expected
);
}
);
});
});

View File

@ -1,28 +1,12 @@
import { document } from 'global';
import React, { Component, createRef } from 'react';
import { connect } from 'react-redux';
import { styled, themes, convert } from '@storybook/theming';
import memoize from 'memoizerific';
import React from 'react';
import { styled } from '@storybook/theming';
import { NodeResult } from 'axe-core';
import { Dispatch } from 'redux';
import { RuleType } from '../A11YPanel';
import { addElement } from '../../redux-config';
import { IFRAME } from '../../constants';
export interface HighlightedElementData {
originalOutline: string;
isHighlighted: boolean;
}
import { useA11yContext } from '../A11yContext';
interface ToggleProps {
elementsToHighlight: NodeResult[];
type: RuleType;
addElement: (data: any) => void;
highlightedElementsMap: Map<HTMLElement, HighlightedElementData>;
isToggledOn?: boolean;
toggleId?: string;
indeterminate: boolean;
}
enum CheckBoxStates {
@ -35,38 +19,13 @@ const Checkbox = styled.input<{ disabled: boolean }>(({ disabled }) => ({
cursor: disabled ? 'not-allowed' : 'pointer',
}));
const colorsByType = [
convert(themes.normal).color.negative, // VIOLATION,
convert(themes.normal).color.positive, // PASS,
convert(themes.normal).color.warning, // INCOMPLETION,
];
const getIframe = memoize(1)(() => document.getElementsByTagName(IFRAME)[0]);
function getElementBySelectorPath(elementPath: string): HTMLElement {
const iframe = getIframe();
if (iframe && iframe.contentDocument && elementPath) {
return iframe.contentDocument.querySelector(elementPath);
}
return (null as unknown) as HTMLElement;
}
function setElementOutlineStyle(targetElement: HTMLElement, outlineStyle: string): void {
// eslint-disable-next-line no-param-reassign
targetElement.style.outline = outlineStyle;
}
function areAllRequiredElementsHighlighted(
elementsToHighlight: NodeResult[],
highlightedElementsMap: Map<HTMLElement, HighlightedElementData>
highlighted: string[]
): CheckBoxStates {
const highlightedCount = elementsToHighlight.filter(item => {
const targetElement = getElementBySelectorPath(item.target[0]);
return (
highlightedElementsMap.has(targetElement) &&
(highlightedElementsMap.get(targetElement) as HighlightedElementData).isHighlighted
);
}).length;
const highlightedCount = elementsToHighlight.filter((item) =>
highlighted.includes(item.target[0])
).length;
// eslint-disable-next-line no-nested-ternary
return highlightedCount === 0
@ -76,112 +35,39 @@ function areAllRequiredElementsHighlighted(
: CheckBoxStates.INDETERMINATE;
}
function mapDispatchToProps(dispatch: Dispatch) {
return {
addElement: (data: { element: HTMLElement; data: HighlightedElementData }) =>
dispatch(addElement(data)),
};
}
const mapStateToProps = (state: any, ownProps: any) => {
const checkBoxState = areAllRequiredElementsHighlighted(
ownProps.elementsToHighlight || [],
state.highlightedElementsMap
const HighlightToggle: React.FC<ToggleProps> = ({ toggleId, elementsToHighlight = [] }) => {
const { toggleHighlight, highlighted } = useA11yContext();
const checkBoxRef = React.useRef<HTMLInputElement>(null);
const [checkBoxState, setChecked] = React.useState(
areAllRequiredElementsHighlighted(elementsToHighlight, highlighted)
);
React.useEffect(() => {
const newState = areAllRequiredElementsHighlighted(elementsToHighlight, highlighted);
if (checkBoxRef.current) {
checkBoxRef.current.indeterminate = newState === CheckBoxStates.INDETERMINATE;
}
setChecked(newState);
}, [elementsToHighlight, highlighted]);
const handleToggle = React.useCallback((): void => {
toggleHighlight(
elementsToHighlight.map((e) => e.target[0]),
checkBoxState !== CheckBoxStates.CHECKED
);
}, [elementsToHighlight, checkBoxState, toggleHighlight]);
return (
<Checkbox
ref={checkBoxRef}
id={toggleId}
type="checkbox"
aria-label="Highlight result"
disabled={!elementsToHighlight.length}
onChange={handleToggle}
checked={checkBoxState === CheckBoxStates.CHECKED}
/>
);
return {
highlightedElementsMap: state.highlightedElementsMap,
isToggledOn: checkBoxState === CheckBoxStates.CHECKED,
indeterminate: checkBoxState === CheckBoxStates.INDETERMINATE,
};
};
class HighlightToggle extends Component<ToggleProps> {
static defaultProps: Partial<ToggleProps> = {
elementsToHighlight: [],
};
private checkBoxRef = createRef<HTMLInputElement>();
componentDidMount() {
const { elementsToHighlight, highlightedElementsMap } = this.props;
elementsToHighlight.forEach(element => {
const targetElement = getElementBySelectorPath(element.target[0]);
if (targetElement && !highlightedElementsMap.has(targetElement)) {
this.saveElementDataToMap(targetElement, false, targetElement.style.outline);
}
});
}
componentDidUpdate(): void {
const { indeterminate } = this.props;
if (this.checkBoxRef.current) {
this.checkBoxRef.current.indeterminate = indeterminate;
}
}
onToggle = (): void => {
const { elementsToHighlight, highlightedElementsMap } = this.props;
elementsToHighlight.forEach(element => {
const targetElement = getElementBySelectorPath(element.target[0]);
if (!highlightedElementsMap.has(targetElement)) {
return;
}
const { originalOutline, isHighlighted } = highlightedElementsMap.get(
targetElement
) as HighlightedElementData;
const { isToggledOn } = this.props;
if ((isToggledOn && isHighlighted) || (!isToggledOn && !isHighlighted)) {
const addHighlight = !isToggledOn && !isHighlighted;
this.highlightRuleLocation(targetElement, addHighlight);
this.saveElementDataToMap(targetElement, addHighlight, originalOutline);
}
});
};
highlightRuleLocation(targetElement: HTMLElement, addHighlight: boolean): void {
const { highlightedElementsMap, type } = this.props;
if (!targetElement) {
return;
}
if (addHighlight) {
setElementOutlineStyle(targetElement, `${colorsByType[type]} dotted 1px`);
return;
}
if (highlightedElementsMap.has(targetElement)) {
setElementOutlineStyle(
targetElement,
highlightedElementsMap.get(targetElement)!.originalOutline
);
}
}
saveElementDataToMap(
targetElement: HTMLElement,
isHighlighted: boolean,
originalOutline: string
): void {
const { addElement: localAddElement } = this.props;
const data: HighlightedElementData = { isHighlighted, originalOutline };
const payload = { element: targetElement, highlightedElementData: data };
localAddElement(payload);
}
render() {
const { toggleId, elementsToHighlight, isToggledOn } = this.props;
return (
<Checkbox
ref={this.checkBoxRef}
id={toggleId}
type="checkbox"
aria-label="Highlight result"
disabled={!elementsToHighlight.length}
onChange={this.onToggle}
checked={isToggledOn}
/>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(HighlightToggle);
export default HighlightToggle;

View File

@ -81,11 +81,7 @@ export const Item = (props: ItemProps) => {
{item.description}
</HeaderBar>
<HighlightToggleElement>
<HighlightToggle
toggleId={highlightToggleId}
type={type}
elementsToHighlight={item ? item.nodes : null}
/>
<HighlightToggle toggleId={highlightToggleId} elementsToHighlight={item.nodes} />
</HighlightToggleElement>
</Wrapper>
{open ? (

View File

@ -48,10 +48,7 @@ interface RuleProps {
}
const formatSeverityText = (severity: string) => {
return severity
.charAt(0)
.toUpperCase()
.concat(severity.slice(1));
return severity.charAt(0).toUpperCase().concat(severity.slice(1));
};
const Rule: FunctionComponent<RuleProps> = ({ rule }) => {

View File

@ -23,7 +23,7 @@ interface TagsProps {
export const Tags: FunctionComponent<TagsProps> = ({ tags }) => {
return (
<Wrapper>
{tags.map(tag => (
{tags.map((tag) => (
<Item key={tag}>{tag}</Item>
))}
</Wrapper>

View File

@ -1,355 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HighlightToggle component should match snapshot 1`] = `
.emotion-0 {
cursor: not-allowed;
}
<Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
}
}
>
<ThemedHighlightToggle>
<ThemeProvider
theme={
Object {
"addonActionsTheme": Object {
"ARROW_ANIMATION_DURATION": "0",
"ARROW_COLOR": "rgba(0,0,0,0.3)",
"ARROW_FONT_SIZE": 8,
"ARROW_MARGIN_RIGHT": 4,
"BASE_BACKGROUND_COLOR": "transparent",
"BASE_COLOR": "#333333",
"BASE_FONT_FAMILY": "\\"Operator Mono\\", \\"Fira Code Retina\\", \\"Fira Code\\", \\"FiraCode-Retina\\", \\"Andale Mono\\", \\"Lucida Console\\", Consolas, Monaco, monospace",
"BASE_FONT_SIZE": 13,
"BASE_LINE_HEIGHT": "18px",
"HTML_ATTRIBUTE_NAME_COLOR": "rgb(153, 69, 0)",
"HTML_ATTRIBUTE_VALUE_COLOR": "rgb(26, 26, 166)",
"HTML_COMMENT_COLOR": "rgb(35, 110, 37)",
"HTML_DOCTYPE_COLOR": "rgb(192, 192, 192)",
"HTML_TAGNAME_COLOR": "rgb(136, 18, 128)",
"HTML_TAGNAME_TEXT_TRANSFORM": "lowercase",
"HTML_TAG_COLOR": "rgb(168, 148, 166)",
"OBJECT_NAME_COLOR": "rgb(136, 19, 145)",
"OBJECT_PREVIEW_ARRAY_MAX_PROPERTIES": 10,
"OBJECT_PREVIEW_OBJECT_MAX_PROPERTIES": 5,
"OBJECT_VALUE_BOOLEAN_COLOR": "rgb(28, 0, 207)",
"OBJECT_VALUE_FUNCTION_PREFIX_COLOR": "rgb(13, 34, 170)",
"OBJECT_VALUE_NULL_COLOR": "rgb(128, 128, 128)",
"OBJECT_VALUE_NUMBER_COLOR": "rgb(28, 0, 207)",
"OBJECT_VALUE_REGEXP_COLOR": "rgb(196, 26, 22)",
"OBJECT_VALUE_STRING_COLOR": "rgb(196, 26, 22)",
"OBJECT_VALUE_SYMBOL_COLOR": "rgb(196, 26, 22)",
"OBJECT_VALUE_UNDEFINED_COLOR": "rgb(128, 128, 128)",
"TABLE_BORDER_COLOR": "#aaa",
"TABLE_DATA_BACKGROUND_IMAGE": "linear-gradient(to bottom, white, white 50%, rgb(234, 243, 255) 50%, rgb(234, 243, 255))",
"TABLE_DATA_BACKGROUND_SIZE": "128px 32px",
"TABLE_SORT_ICON_COLOR": "#6e6e6e",
"TABLE_TH_BACKGROUND_COLOR": "#eee",
"TABLE_TH_HOVER_COLOR": "hsla(0, 0%, 90%, 1)",
"TREENODE_FONT_FAMILY": "\\"Operator Mono\\", \\"Fira Code Retina\\", \\"Fira Code\\", \\"FiraCode-Retina\\", \\"Andale Mono\\", \\"Lucida Console\\", Consolas, Monaco, monospace",
"TREENODE_FONT_SIZE": 13,
"TREENODE_LINE_HEIGHT": "18px",
"TREENODE_PADDING_LEFT": 12,
},
"animation": Object {
"float": Object {
"anim": 1,
"name": "animation-6tolu8",
"styles": "@keyframes animation-6tolu8{
0% { transform: translateY(1px); }
25% { transform: translateY(0px); }
50% { transform: translateY(-3px); }
100% { transform: translateY(1px); }
}",
"toString": [Function],
},
"glow": Object {
"anim": 1,
"name": "animation-r0iffl",
"styles": "@keyframes animation-r0iffl{
0%, 100% { opacity: 1; }
50% { opacity: .4; }
}",
"toString": [Function],
},
"hoverable": Object {
"map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9hbmltYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBd0NxQiIsImZpbGUiOiIuLi9zcmMvYW5pbWF0aW9uLnRzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9jb3JlJztcblxuZXhwb3J0IGNvbnN0IGVhc2luZyA9IHtcbiAgcnViYmVyOiAnY3ViaWMtYmV6aWVyKDAuMTc1LCAwLjg4NSwgMC4zMzUsIDEuMDUpJyxcbn07XG5cbmNvbnN0IHJvdGF0ZTM2MCA9IGtleWZyYW1lc2Bcblx0ZnJvbSB7XG5cdFx0dHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG5cdH1cblx0dG8ge1xuXHRcdHRyYW5zZm9ybTogcm90YXRlKDM2MGRlZyk7XG5cdH1cbmA7XG5cbmNvbnN0IGdsb3cgPSBrZXlmcmFtZXNgXG4gIDAlLCAxMDAlIHsgb3BhY2l0eTogMTsgfVxuICA1MCUgeyBvcGFjaXR5OiAuNDsgfVxuYDtcblxuY29uc3QgZmxvYXQgPSBrZXlmcmFtZXNgXG4gIDAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDFweCk7IH1cbiAgMjUlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDBweCk7IH1cbiAgNTAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKC0zcHgpOyB9XG4gIDEwMCUgeyB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoMXB4KTsgfVxuYDtcblxuY29uc3QgamlnZ2xlID0ga2V5ZnJhbWVzYFxuICAwJSwgMTAwJSB7IHRyYW5zZm9ybTp0cmFuc2xhdGUzZCgwLDAsMCk7IH1cbiAgMTIuNSUsIDYyLjUlIHsgdHJhbnNmb3JtOnRyYW5zbGF0ZTNkKC00cHgsMCwwKTsgfVxuICAzNy41JSwgODcuNSUgeyAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCg0cHgsMCwwKTsgIH1cbmA7XG5cbmNvbnN0IGlubGluZUdsb3cgPSBjc3NgXG4gIGFuaW1hdGlvbjogJHtnbG93fSAxLjVzIGVhc2UtaW4tb3V0IGluZmluaXRlO1xuICBjb2xvcjogdHJhbnNwYXJlbnQ7XG4gIGN1cnNvcjogcHJvZ3Jlc3M7XG5gO1xuXG4vLyBob3ZlciAmIGFjdGl2ZSBzdGF0ZSBmb3IgbGlua3MgYW5kIGJ1dHRvbnNcbmNvbnN0IGhvdmVyYWJsZSA9IGNzc2BcbiAgdHJhbnNpdGlvbjogYWxsIDE1MG1zIGVhc2Utb3V0O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDAsIDAsIDApO1xuXG4gICY6aG92ZXIge1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlM2QoMCwgLTJweCwgMCk7XG4gIH1cblxuICAmOmFjdGl2ZSB7XG4gICAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCgwLCAwLCAwKTtcbiAgfVxuYDtcblxuZXhwb3J0IGNvbnN0IGFuaW1hdGlvbiA9IHtcbiAgcm90YXRlMzYwLFxuICBnbG93LFxuICBmbG9hdCxcbiAgamlnZ2xlLFxuICBpbmxpbmVHbG93LFxuICBob3ZlcmFibGUsXG59O1xuIl19 */",
"name": "1o7rzh8-hoverable",
"styles": "transition:all 150ms ease-out;transform:translate3d(0,0,0);&:hover{transform:translate3d(0,-2px,0);}&:active{transform:translate3d(0,0,0);};label:hoverable;",
"toString": [Function],
},
"inlineGlow": Object {
"map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9hbmltYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBaUNzQiIsImZpbGUiOiIuLi9zcmMvYW5pbWF0aW9uLnRzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9jb3JlJztcblxuZXhwb3J0IGNvbnN0IGVhc2luZyA9IHtcbiAgcnViYmVyOiAnY3ViaWMtYmV6aWVyKDAuMTc1LCAwLjg4NSwgMC4zMzUsIDEuMDUpJyxcbn07XG5cbmNvbnN0IHJvdGF0ZTM2MCA9IGtleWZyYW1lc2Bcblx0ZnJvbSB7XG5cdFx0dHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG5cdH1cblx0dG8ge1xuXHRcdHRyYW5zZm9ybTogcm90YXRlKDM2MGRlZyk7XG5cdH1cbmA7XG5cbmNvbnN0IGdsb3cgPSBrZXlmcmFtZXNgXG4gIDAlLCAxMDAlIHsgb3BhY2l0eTogMTsgfVxuICA1MCUgeyBvcGFjaXR5OiAuNDsgfVxuYDtcblxuY29uc3QgZmxvYXQgPSBrZXlmcmFtZXNgXG4gIDAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDFweCk7IH1cbiAgMjUlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDBweCk7IH1cbiAgNTAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKC0zcHgpOyB9XG4gIDEwMCUgeyB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoMXB4KTsgfVxuYDtcblxuY29uc3QgamlnZ2xlID0ga2V5ZnJhbWVzYFxuICAwJSwgMTAwJSB7IHRyYW5zZm9ybTp0cmFuc2xhdGUzZCgwLDAsMCk7IH1cbiAgMTIuNSUsIDYyLjUlIHsgdHJhbnNmb3JtOnRyYW5zbGF0ZTNkKC00cHgsMCwwKTsgfVxuICAzNy41JSwgODcuNSUgeyAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCg0cHgsMCwwKTsgIH1cbmA7XG5cbmNvbnN0IGlubGluZUdsb3cgPSBjc3NgXG4gIGFuaW1hdGlvbjogJHtnbG93fSAxLjVzIGVhc2UtaW4tb3V0IGluZmluaXRlO1xuICBjb2xvcjogdHJhbnNwYXJlbnQ7XG4gIGN1cnNvcjogcHJvZ3Jlc3M7XG5gO1xuXG4vLyBob3ZlciAmIGFjdGl2ZSBzdGF0ZSBmb3IgbGlua3MgYW5kIGJ1dHRvbnNcbmNvbnN0IGhvdmVyYWJsZSA9IGNzc2BcbiAgdHJhbnNpdGlvbjogYWxsIDE1MG1zIGVhc2Utb3V0O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDAsIDAsIDApO1xuXG4gICY6aG92ZXIge1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlM2QoMCwgLTJweCwgMCk7XG4gIH1cblxuICAmOmFjdGl2ZSB7XG4gICAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCgwLCAwLCAwKTtcbiAgfVxuYDtcblxuZXhwb3J0IGNvbnN0IGFuaW1hdGlvbiA9IHtcbiAgcm90YXRlMzYwLFxuICBnbG93LFxuICBmbG9hdCxcbiAgamlnZ2xlLFxuICBpbmxpbmVHbG93LFxuICBob3ZlcmFibGUsXG59O1xuIl19 */",
"name": "x4tfcc-inlineGlow",
"next": Object {
"name": "animation-r0iffl",
"next": undefined,
"styles": "@keyframes animation-r0iffl{
0%, 100% { opacity: 1; }
50% { opacity: .4; }
}",
},
"styles": "animation:animation-r0iffl 1.5s ease-in-out infinite;color:transparent;cursor:progress;;label:inlineGlow;",
"toString": [Function],
},
"jiggle": Object {
"anim": 1,
"name": "animation-ynpq7w",
"styles": "@keyframes animation-ynpq7w{
0%, 100% { transform:translate3d(0,0,0); }
12.5%, 62.5% { transform:translate3d(-4px,0,0); }
37.5%, 87.5% { transform: translate3d(4px,0,0); }
}",
"toString": [Function],
},
"rotate360": Object {
"anim": 1,
"name": "animation-u07e3c",
"styles": "@keyframes animation-u07e3c{
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}",
"toString": [Function],
},
},
"appBorderColor": "rgba(0,0,0,.1)",
"appBorderRadius": 4,
"background": Object {
"app": "#F6F9FC",
"bar": "#FFFFFF",
"content": "#FFFFFF",
"critical": "#FF4400",
"gridCellSize": 10,
"hoverable": "rgba(0,0,0,.05)",
"negative": "#FEDED2",
"positive": "#E1FFD4",
"warning": "#FFF5CF",
},
"barBg": "#FFFFFF",
"barSelectedColor": "#1EA7FD",
"barTextColor": "#999999",
"base": "light",
"brand": Object {
"image": undefined,
"title": undefined,
"url": undefined,
},
"code": Object {
"language-json .token.boolean": Object {
"color": "#0000ff",
},
"language-json .token.number": Object {
"color": "#0000ff",
},
"language-json .token.property": Object {
"color": "#2B91AF",
},
"namespace": Object {
"opacity": 0.7,
},
"token": Object {
"&.atrule": Object {
"color": "#0000ff",
},
"&.attr-name": Object {
"color": "#ff0000",
},
"&.attr-value": Object {
"color": "#0000ff",
},
"&.bold": Object {
"fontWeight": "bold",
},
"&.boolean": Object {
"color": "#36acaa",
},
"&.cdata": Object {
"color": "#008000",
"fontStyle": "italic",
},
"&.class-name": Object {
"color": "#2B91AF",
},
"&.comment": Object {
"color": "#008000",
"fontStyle": "italic",
},
"&.constant": Object {
"color": "#36acaa",
},
"&.deleted": Object {
"color": "#9a050f",
},
"&.directive.tag .tag": Object {
"background": "#ffff00",
"color": "#393A34",
},
"&.doctype": Object {
"color": "#008000",
"fontStyle": "italic",
},
"&.entity": Object {
"color": "#ff0000",
},
"&.function": Object {
"color": "#393A34",
},
"&.important": Object {
"fontWeight": "bold",
},
"&.inserted": Object {
"color": "#36acaa",
},
"&.italic": Object {
"fontStyle": "italic",
},
"&.keyword": Object {
"color": "#0000ff",
},
"&.number": Object {
"color": "#36acaa",
},
"&.operator": Object {
"color": "#393A34",
},
"&.prolog": Object {
"color": "#008000",
"fontStyle": "italic",
},
"&.property": Object {
"color": "#ff0000",
},
"&.punctuation": Object {
"color": "#393A34",
},
"&.regex": Object {
"color": "#ff0000",
},
"&.selector": Object {
"color": "#800000",
},
"&.string": Object {
"color": "#A31515",
},
"&.symbol": Object {
"color": "#36acaa",
},
"&.tag": Object {
"color": "#800000",
},
"&.url": Object {
"color": "#36acaa",
},
"&.variable": Object {
"color": "#36acaa",
},
"WebkitFontSmoothing": "antialiased",
"fontFamily": "\\"Operator Mono\\", \\"Fira Code Retina\\", \\"Fira Code\\", \\"FiraCode-Retina\\", \\"Andale Mono\\", \\"Lucida Console\\", Consolas, Monaco, monospace",
},
},
"color": Object {
"ancillary": "#22a699",
"border": "rgba(0,0,0,.1)",
"critical": "#FFFFFF",
"dark": "#666666",
"darker": "#444444",
"darkest": "#333333",
"defaultText": "#333333",
"gold": "#FFAE00",
"green": "#66BF3C",
"inverseText": "#FFFFFF",
"light": "#F3F3F3",
"lighter": "#F8F8F8",
"lightest": "#FFFFFF",
"medium": "#DDDDDD",
"mediumdark": "#999999",
"mediumlight": "#EEEEEE",
"negative": "#FF4400",
"orange": "#FC521F",
"positive": "#66BF3C",
"primary": "#FF4785",
"purple": "#6F2CAC",
"seafoam": "#37D5D3",
"secondary": "#1EA7FD",
"tertiary": "#FAFBFC",
"ultraviolet": "#2A0481",
"warning": "#E69D00",
},
"easing": Object {
"rubber": "cubic-bezier(0.175, 0.885, 0.335, 1.05)",
},
"input": Object {
"background": "#FFFFFF",
"border": "rgba(0,0,0,.1)",
"borderRadius": 4,
"color": "#333333",
},
"layoutMargin": 10,
"typography": Object {
"fonts": Object {
"base": "\\"Nunito Sans\\", -apple-system, \\".SFNSText-Regular\\", \\"San Francisco\\", BlinkMacSystemFont, \\"Segoe UI\\", \\"Helvetica Neue\\", Helvetica, Arial, sans-serif",
"mono": "\\"Operator Mono\\", \\"Fira Code Retina\\", \\"Fira Code\\", \\"FiraCode-Retina\\", \\"Andale Mono\\", \\"Lucida Console\\", Consolas, Monaco, monospace",
},
"size": Object {
"code": 90,
"l1": 32,
"l2": 40,
"l3": 48,
"m1": 20,
"m2": 24,
"m3": 28,
"s1": 12,
"s2": 14,
"s3": 16,
},
"weight": Object {
"black": 900,
"bold": 700,
"regular": 400,
},
},
}
}
>
<Connect(HighlightToggle)>
<HighlightToggle
addElement={[Function]}
elementsToHighlight={Array []}
highlightedElementsMap={Map {}}
indeterminate={false}
isToggledOn={false}
>
<Styled(input)
aria-label="Highlight result"
checked={false}
disabled={true}
onChange={[Function]}
type="checkbox"
>
<input
aria-label="Highlight result"
checked={false}
className="emotion-0"
disabled={true}
onChange={[Function]}
type="checkbox"
/>
</Styled(input)>
</HighlightToggle>
</Connect(HighlightToggle)>
</ThemeProvider>
</ThemedHighlightToggle>
</Provider>
`;

View File

@ -13,7 +13,7 @@ export interface ReportProps {
export const Report: FunctionComponent<ReportProps> = ({ items, empty, type }) => (
<Fragment>
{items && items.length ? (
items.map(item => <Item item={item} key={`${type}:${item.id}`} type={type} />)
items.map((item) => <Item item={item} key={`${type}:${item.id}`} type={type} />)
) : (
<Placeholder key="placeholder">{empty}</Placeholder>
)}

View File

@ -1,11 +1,11 @@
import React, { Component, SyntheticEvent } from 'react';
import * as React from 'react';
import { styled } from '@storybook/theming';
import { NodeResult, Result } from 'axe-core';
import { SizeMe } from 'react-sizeme';
import store, { clearElements } from '../redux-config';
import HighlightToggle from './Report/HighlightToggle';
import { RuleType } from './A11YPanel';
import { useA11yContext } from './A11yContext';
// TODO: reuse the Tabs component from @storybook/theming instead of re-building identical functionality
@ -94,68 +94,55 @@ interface TabsProps {
}[];
}
interface TabsState {
active: number;
}
function retrieveAllNodesFromResults(items: Result[]): NodeResult[] {
return items.reduce((acc, item) => acc.concat(item.nodes), [] as NodeResult[]);
}
export class Tabs extends Component<TabsProps, TabsState> {
state: TabsState = {
active: 0,
};
export const Tabs: React.FC<TabsProps> = ({ tabs }) => {
const { tab: activeTab, setTab } = useA11yContext();
onToggle = (event: SyntheticEvent) => {
this.setState({
active: parseInt(event.currentTarget.getAttribute('data-index') || '', 10),
});
// removes all elements from the redux map in store from the previous panel
store.dispatch(clearElements());
};
const handleToggle = React.useCallback(
(event: React.SyntheticEvent) => {
setTab(parseInt(event.currentTarget.getAttribute('data-index') || '', 10));
},
[setTab]
);
render() {
const { tabs } = this.props;
const { active } = this.state;
const highlightToggleId = `${tabs[active].type}-global-checkbox`;
const highlightLabel = `Highlight results`;
return (
<SizeMe refreshMode="debounce">
{({ size }: { size: any }) => (
<Container>
<List>
<TabsWrapper>
{tabs.map((tab, index) => (
<Item
/* eslint-disable-next-line react/no-array-index-key */
key={index}
data-index={index}
active={active === index}
onClick={this.onToggle}
>
{tab.label}
</Item>
))}
</TabsWrapper>
</List>
{tabs[active].items.length > 0 ? (
<GlobalToggle elementWidth={size.width}>
<HighlightToggleLabel htmlFor={highlightToggleId}>
{highlightLabel}
</HighlightToggleLabel>
<HighlightToggle
toggleId={highlightToggleId}
type={tabs[active].type}
elementsToHighlight={retrieveAllNodesFromResults(tabs[active].items)}
label={highlightLabel}
/>
</GlobalToggle>
) : null}
{tabs[active].panel}
</Container>
)}
</SizeMe>
);
}
}
const highlightToggleId = `${tabs[activeTab].type}-global-checkbox`;
const highlightLabel = `Highlight results`;
return (
<SizeMe refreshMode="debounce">
{({ size }: { size: any }) => (
<Container>
<List>
<TabsWrapper>
{tabs.map((tab, index) => (
<Item
/* eslint-disable-next-line react/no-array-index-key */
key={index}
data-index={index}
active={activeTab === index}
onClick={handleToggle}
>
{tab.label}
</Item>
))}
</TabsWrapper>
</List>
{tabs[activeTab].items.length > 0 ? (
<GlobalToggle elementWidth={size.width}>
<HighlightToggleLabel htmlFor={highlightToggleId}>
{highlightLabel}
</HighlightToggleLabel>
<HighlightToggle
toggleId={highlightToggleId}
elementsToHighlight={retrieveAllNodesFromResults(tabs[activeTab].items)}
/>
</GlobalToggle>
) : null}
{tabs[activeTab].panel}
</Container>
)}
</SizeMe>
);
};

View File

@ -1,12 +1,12 @@
export const ADDON_ID = 'storybook/a11y';
export const PANEL_ID = `${ADDON_ID}/panel`;
export const PARAM_KEY = `a11y`;
export const IFRAME = 'iframe';
export const ADD_ELEMENT = 'ADD_ELEMENT';
export const CLEAR_ELEMENTS = 'CLEAR_ELEMENTS';
export const HIGHLIGHT_STYLE_ID = 'a11yHighlight';
const RESULT = `${ADDON_ID}/result`;
const REQUEST = `${ADDON_ID}/request`;
const RUNNING = `${ADDON_ID}/running`;
const ERROR = `${ADDON_ID}/error`;
const MANUAL = `${ADDON_ID}/manual`;
const HIGHLIGHT = `${ADDON_ID}/highlight`;
export const EVENTS = { RESULT, REQUEST, ERROR, MANUAL };
export const EVENTS = { RESULT, REQUEST, RUNNING, ERROR, MANUAL, HIGHLIGHT };

View File

@ -0,0 +1,12 @@
export const higlightStyle = (color: string) => `
outline: 2px dashed ${color};
outline-offset: 2px;
box-shadow: 0 0 0 6px rgba(255,255,255,0.6);
}
`;
export const highlightObject = (color: string) => ({
outline: `2px dashed ${color}`,
outlineOffset: 2,
boxShadow: '0 0 0 6px rgba(255,255,255,0.6),',
});

View File

@ -1,107 +1,5 @@
import { document } from 'global';
import axe, { AxeResults, ElementContext, RunOptions, Spec } from 'axe-core';
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
import addons, { makeDecorator } from '@storybook/addons';
import { EVENTS, PARAM_KEY } from './constants';
let progress = Promise.resolve();
interface Setup {
element?: ElementContext;
config: Spec;
options: RunOptions;
manual: boolean;
}
let setup: Setup = { element: undefined, config: {}, options: {}, manual: false };
const getElement = () => {
const storyRoot = document.getElementById('story-root');
if (storyRoot) {
return storyRoot.children;
}
return document.getElementById('root');
};
const report = (input: AxeResults) => addons.getChannel().emit(EVENTS.RESULT, input);
const run = (element: ElementContext, config: Spec, options: RunOptions) => {
progress = progress.then(() => {
axe.reset();
if (config) {
axe.configure(config);
}
return axe
.run(
element || getElement(),
options ||
({
restoreScroll: true,
} as RunOptions) // cast to RunOptions is necessary because axe types are not up to date
)
.then(report)
.catch(error => addons.getChannel().emit(EVENTS.ERROR, String(error)));
});
};
export * from './highlight';
if (module && module.hot && module.hot.decline) {
module.hot.decline();
}
let storedDefaultSetup: Setup | null = null;
export const withA11y = makeDecorator({
name: 'withA11Y',
parameterName: PARAM_KEY,
wrapper: (getStory, context, { parameters }) => {
if (parameters) {
if (storedDefaultSetup === null) {
storedDefaultSetup = { ...setup };
}
Object.assign(setup, parameters as Partial<Setup>);
} else if (storedDefaultSetup !== null) {
Object.assign(setup, storedDefaultSetup);
storedDefaultSetup = null;
}
addons
.getChannel()
.on(EVENTS.REQUEST, () => run(setup.element as ElementContext, setup.config, setup.options));
addons.getChannel().emit(EVENTS.MANUAL, setup.manual);
return getStory(context);
},
});
// TODO: REMOVE at v6.0.0
export const withA11Y = deprecate(
// @ts-ignore
(...args: any[]) => withA11y(...args),
'withA11Y has been renamed withA11y'
);
// TODO: REMOVE at v6.0.0
export const checkA11y = deprecate(
// @ts-ignore
(...args: any[]) => withA11y(...args),
'checkA11y has been renamed withA11y'
);
// TODO: REMOVE at v6.0.0
export const configureA11y = deprecate(
(config: any) => {
setup = config;
},
dedent`
configureA11y is deprecated, please configure addon-a11y using the addParameter api:
addParameters({
a11y: {
// ... axe options
element: '#root', // optional selector which element to inspect
},
});
`
);

14
addons/a11y/src/params.ts Normal file
View File

@ -0,0 +1,14 @@
import { ElementContext, Spec, RunOptions } from 'axe-core';
export interface Setup {
element?: ElementContext;
config: Spec;
options: RunOptions;
}
export interface A11yParameters {
element?: ElementContext;
config?: Spec;
options?: RunOptions;
manual?: boolean;
}

View File

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

View File

@ -1,44 +0,0 @@
import { createStore } from 'redux';
import { ADD_ELEMENT, CLEAR_ELEMENTS } from './constants';
import { HighlightedElementData } from './components/Report/HighlightToggle';
// actions
// add element is passed a HighlightedElementData object as the payload
export function addElement(payload: { element: HTMLElement; data: HighlightedElementData }) {
return { type: ADD_ELEMENT, payload };
}
// clear elements is a function to remove elements from the map and reset elements to their original state
export function clearElements() {
return { type: CLEAR_ELEMENTS };
}
// reducers
const initialState = {
highlightedElementsMap: new Map(),
};
function rootReducer(state = initialState, action: any) {
if (action.type === ADD_ELEMENT) {
return {
...state,
highlightedElementsMap: state.highlightedElementsMap.set(
action.payload.element,
action.payload.highlightedElementData
),
};
}
if (action.type === CLEAR_ELEMENTS) {
// eslint-disable-next-line no-restricted-syntax
for (const key of Array.from(state.highlightedElementsMap.keys())) {
key.style.outline = state.highlightedElementsMap.get(key).originalOutline;
state.highlightedElementsMap.delete(key);
}
}
return state;
}
// store
const store = createStore(rootReducer);
export default store;

View File

@ -1,88 +1,11 @@
import React, { Fragment, FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import React from 'react';
import { addons, types } from '@storybook/addons';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
import { ColorBlindness } from './components/ColorBlindness';
import { A11YPanel } from './components/A11YPanel';
import { A11yContextProvider } from './components/A11yContext';
const Hidden = styled.div(() => ({
'&, & svg': {
position: 'absolute',
width: 0,
height: 0,
},
}));
const PreviewWrapper: FunctionComponent<{}> = p => (
<Fragment>
{p.children}
<Hidden>
<svg key="svg">
<defs>
<filter id="protanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.567, 0.433, 0, 0, 0 0.558, 0.442, 0, 0, 0 0, 0.242, 0.758, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="protanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.817, 0.183, 0, 0, 0 0.333, 0.667, 0, 0, 0 0, 0.125, 0.875, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="deuteranopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.625, 0.375, 0, 0, 0 0.7, 0.3, 0, 0, 0 0, 0.3, 0.7, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="deuteranomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.8, 0.2, 0, 0, 0 0.258, 0.742, 0, 0, 0 0, 0.142, 0.858, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="tritanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.95, 0.05, 0, 0, 0 0, 0.433, 0.567, 0, 0 0, 0.475, 0.525, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="tritanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.967, 0.033, 0, 0, 0 0, 0.733, 0.267, 0, 0 0, 0.183, 0.817, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="achromatopsia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.299, 0.587, 0.114, 0, 0 0.299, 0.587, 0.114, 0, 0 0.299, 0.587, 0.114, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
<filter id="achromatomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.618, 0.320, 0.062, 0, 0 0.163, 0.775, 0.062, 0, 0 0.163, 0.320, 0.516, 0, 0 0, 0, 0, 1, 0"
/>
</filter>
</defs>
</svg>
</Hidden>
</Fragment>
);
addons.register(ADDON_ID, api => {
addons.register(ADDON_ID, () => {
addons.add(PANEL_ID, {
title: '',
type: types.TOOL,
@ -93,13 +16,11 @@ addons.register(ADDON_ID, api => {
addons.add(PANEL_ID, {
title: 'Accessibility',
type: types.PANEL,
render: ({ active, key }) => <A11YPanel key={key} api={api} active={active} />,
render: ({ active = true, key }) => (
<A11yContextProvider key={key} active={active}>
<A11YPanel />
</A11yContextProvider>
),
paramKey: PARAM_KEY,
});
addons.add(PANEL_ID, {
title: '',
type: types.PREVIEW,
render: PreviewWrapper as any,
});
});

View File

@ -2,7 +2,7 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env"],
"types": ["webpack-env", "jest"],
"forceConsistentCasingInFileNames": true,
"strict": true,
"noUnusedLocals": true,
@ -10,10 +10,6 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src/**/*"
],
"exclude": [
"src/__tests__/**/*"
]
"include": ["src/**/*"],
"exclude": ["src/__tests__/**/*"]
}

View File

@ -18,10 +18,45 @@ Then, add following content to `.storybook/main.js`
```js
module.exports = {
addons: ['@storybook/addon-actions/register']
}
addons: ['@storybook/addon-actions'],
};
```
## Actions args
Starting in SB6.0, we recommend using story parameters to specify actions which get passed into your story as [Args](https://docs.google.com/document/d/1Mhp1UFRCKCsN8pjlfPdz8ZdisgjNXeMXpXvGoALjxYM/edit?usp=sharing) (passed as the first argument when `passArgsFirst` is set to `true`).
The first option is to specify `argTypes` for your story with an `action` field. Take the following example:
```js
import Button from './button';
export default {
title: 'Button',
argTypes: { onClick: { action: 'clicked' } },
};
export const Basic = ({ onClick }) => <Button onClick={onClick}>Hello World!</Button>;
```
Alternatively, suppose you have a naming convention, like `onX` for event handlers. The following configuration automatically creates actions for each `onX` `argType` (which you can either specify manually or generate automatically using [Storybook Docs](https://www.npmjs.com/package/@storybook/addon-docs).
```js
import Button from './button';
export default {
title: 'Button',
component: Button,
parameters: { actions: { argTypesRegex: '^on.*' } },
};
export const Basic = ({ onClick }) => <Button onClick={onClick}>Hello World!</Button>;
```
> **NOTE:** If you're generating `argTypes` in using another addon (like Docs, which is the common behavior) you'll need to make sure that the actions addon loads **AFTER** the other addon. You can do this by listing it later in the `addons` registration code in `.storybook/main.js`.
## Manually-specified actions
Import the `action` function and use it to create actions handlers. When creating action handlers, provide a **name** to make it easier to identify.
> _Note: Make sure NOT to use reserved words as function names. [issues#29](https://github.com/storybookjs/storybook-addon-actions/issues/29#issuecomment-288274794)_
@ -35,9 +70,7 @@ export default {
component: Button,
};
export const defaultView = () => (
<Button onClick={action('button-click')}>Hello World!</Button>
);
export const defaultView = () => <Button onClick={action('button-click')}>Hello World!</Button>;
```
## Multiple actions
@ -59,48 +92,18 @@ const eventsFromNames = actions('onClick', 'onMouseOver');
// This will lead to { onClick: action('clicked'), ... }
const eventsFromObject = actions({ onClick: 'clicked', onMouseOver: 'hovered' });
export const first = () => (
<Button {...eventsFromNames}>Hello World!</Button>
);
export const first = () => <Button {...eventsFromNames}>Hello World!</Button>;
export const second = () => (
<Button {...eventsFromObject}>Hello World!</Button>
);
```
## Action Decorators
If you wish to process action data before sending them over to the logger, you can do it with action decorators.
`decorate` takes an array of decorator functions. Each decorator function is passed an array of arguments, and should return a new arguments array to use. `decorate` returns a object with two functions: `action` and `actions`, that act like the above, except they log the modified arguments instead of the original arguments.
```js
import { decorate } from '@storybook/addon-actions';
import Button from './button';
export default {
title: 'Button',
component: Button,
};
const firstArg = decorate([args => args.slice(0, 1)]);
export const first = () => (
<Button onClick={firstArg.action('button-click')}>Hello World!</Button>
);
export const second = () => <Button {...eventsFromObject}>Hello World!</Button>;
```
## Configuration
Arguments which are passed to the action call will have to be serialized while be "transferred"
over the channel.
Arguments which are passed to the action call will have to be serialized while be "transferred" over the channel.
This is not very optimal and can cause lag when large objects are being logged, for this reason it is possible
to configure a maximum depth.
This is not very optimal and can cause lag when large objects are being logged, for this reason it is possible to configure a maximum depth.
The action logger, by default, will log all actions fired during the lifetime of the story. After a while
this can make the storybook laggy. As a workaround, you can configure an upper limit to how many actions should
be logged.
The action logger, by default, will log all actions fired during the lifetime of the story. After a while this can make the storybook laggy. As a workaround, you can configure an upper limit to how many actions should be logged.
To apply the configuration globally use the `configureActions` function in your `preview.js` file.
@ -115,6 +118,7 @@ configureActions({
```
To apply the configuration per action use:
```js
action('my-action', {
depth: 5,
@ -123,27 +127,27 @@ action('my-action', {
### Available Options
|Name|Type|Description|Default|
|---|---|---|---|
|`depth`|Number|Configures the transferred depth of any logged objects.|`10`|
|`clearOnStoryChange`|Boolean|Flag whether to clear the action logger when switching away from the current story.|`true`|
|`limit`|Number|Limits the number of items logged in the action logger|`50`|
| Name | Type | Description | Default |
| -------------------- | ------- | ----------------------------------------------------------------------------------- | ------- |
| `depth` | Number | Configures the transferred depth of any logged objects. | `10` |
| `clearOnStoryChange` | Boolean | Flag whether to clear the action logger when switching away from the current story. | `true` |
| `limit` | Number | Limits the number of items logged in the action logger | `50` |
## withActions decorator
## Declarative Configuration via Parameters
You can define action handles in a declarative way using `withActions` decorators. It accepts the same arguments as [`actions`](#multiple-actions)
You can define action handles in a declarative way using parameters. They accepts the same arguments as [`actions`](#multiple-actions)
Keys have `'<eventName> <selector>'` format, e.g. `'click .btn'`. Selector is optional. This can be used with any framework but is especially useful for `@storybook/html`.
```js
import { withActions } from '@storybook/addon-actions';
import Button from './button';
export default {
title: 'Button',
decorators: [withActions('mouseover', 'click .btn')]
parameters: {
actions: {
handles: ['mouseover', 'click .btn']
}
};
export const first = () => (
<Button className="btn">Hello World!</Button>
);
export const first = () => <Button className="btn">Hello World!</Button>;
```

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-actions",
"version": "6.0.0-alpha.1",
"version": "6.0.0-beta.1",
"description": "Action Logger addon for storybook",
"keywords": [
"storybook"
@ -15,40 +15,55 @@
"directory": "addons/actions"
},
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts"
"*.d.ts",
"ts3.5/**/*"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.0.0-alpha.1",
"@storybook/api": "6.0.0-alpha.1",
"@storybook/client-api": "6.0.0-alpha.1",
"@storybook/components": "6.0.0-alpha.1",
"@storybook/core-events": "6.0.0-alpha.1",
"@storybook/theming": "6.0.0-alpha.1",
"@storybook/addons": "6.0.0-beta.1",
"@storybook/api": "6.0.0-beta.1",
"@storybook/client-api": "6.0.0-beta.1",
"@storybook/components": "6.0.0-beta.1",
"@storybook/core-events": "6.0.0-beta.1",
"@storybook/theming": "6.0.0-beta.1",
"core-js": "^3.0.1",
"fast-deep-equal": "^2.0.1",
"fast-deep-equal": "^3.1.1",
"global": "^4.3.2",
"polished": "^3.3.1",
"lodash": "^4.17.15",
"polished": "^3.4.4",
"prop-types": "^15.7.2",
"react": "^16.8.3",
"react-inspector": "^4.0.0",
"uuid": "^3.3.2"
"react-inspector": "^5.0.1",
"regenerator-runtime": "^0.13.3",
"ts-dedent": "^1.1.1",
"util-deprecate": "^1.0.2",
"uuid": "^8.0.0"
},
"devDependencies": {
"@types/lodash": "^4.14.149",
"@types/uuid": "^3.4.4",
"@types/webpack-env": "^1.15.0"
"@types/lodash": "^4.14.150",
"@types/uuid": "^7.0.3",
"@types/webpack-env": "^1.15.2"
},
"peerDependencies": {
"react-dom": "*"
},
"publishConfig": {
"access": "public"
},
"gitHead": "4b9d901add9452525135caae98ae5f78dd8da9ff"
"gitHead": "4b9d901add9452525135caae98ae5f78dd8da9ff",
"typesVersions": {
"<=3.5": {
"*": [
"ts3.5/*"
]
}
}
}

1
addons/actions/preset.js Normal file
View File

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

View File

@ -1 +1 @@
require('./dist/manager').register();
require('./dist/register');

View File

@ -8,6 +8,7 @@ export const Action = styled.div({
borderBottom: '1px solid transparent',
transition: 'all 0.1s',
alignItems: 'flex-start',
whiteSpace: 'pre',
});
export const Counter = styled.div<{}>(({ theme }) => ({

View File

@ -1,14 +0,0 @@
import * as React from 'react';
import addons from '@storybook/addons';
import ActionLogger from './containers/ActionLogger';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
export function register() {
addons.register(ADDON_ID, api => {
addons.addPanel(PANEL_ID, {
title: 'Actions',
render: ({ active, key }) => <ActionLogger key={key} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});
}

View File

@ -0,0 +1,76 @@
import { StoryContext } from '@storybook/addons';
import { inferActionsFromArgTypesRegex, addActionsFromArgTypes } from './addArgs';
const withDefaultValue = (argTypes) =>
Object.keys(argTypes).filter((key) => !!argTypes[key].defaultValue);
describe('actions parameter enhancers', () => {
describe('actions.argTypesRegex parameter', () => {
const baseParameters = {
argTypes: { onClick: {}, onFocus: {}, somethingElse: {} },
actions: { argTypesRegex: '^on.*' },
};
it('should add actions that match a pattern', () => {
const parameters = baseParameters;
const argTypes = inferActionsFromArgTypesRegex({ parameters } as StoryContext);
expect(withDefaultValue(argTypes)).toEqual(['onClick', 'onFocus']);
});
it('should prioritize pre-existing argTypes unless they are null', () => {
const parameters = {
...baseParameters,
argTypes: {
onClick: { defaultValue: 'pre-existing value' },
onFocus: { defaultValue: null },
},
};
const argTypes = inferActionsFromArgTypesRegex({ parameters } as StoryContext);
expect(withDefaultValue(argTypes)).toEqual(['onClick', 'onFocus']);
expect(argTypes.onClick.defaultValue).toEqual('pre-existing value');
expect(argTypes.onFocus.defaultValue).not.toBeNull();
});
it('should do nothing if actions are disabled', () => {
const parameters = {
...baseParameters,
actions: { ...baseParameters.actions, disable: true },
};
const result = inferActionsFromArgTypesRegex({ parameters } as StoryContext);
expect(result).toEqual(parameters.argTypes);
});
});
describe('argTypes.action parameter', () => {
const baseParameters = {
argTypes: {
onClick: { action: 'clicked!' },
onBlur: { action: 'blurred!' },
},
};
it('should add actions based on action.args', () => {
const parameters = baseParameters;
const argTypes = addActionsFromArgTypes({ parameters } as StoryContext);
expect(withDefaultValue(argTypes)).toEqual(['onClick', 'onBlur']);
});
it('should prioritize pre-existing args', () => {
const parameters = {
...baseParameters,
argTypes: {
onClick: { defaultValue: 'pre-existing value', action: 'onClick' },
onBlur: { action: 'onBlur' },
},
};
const argTypes = addActionsFromArgTypes({ parameters } as StoryContext);
expect(withDefaultValue(argTypes)).toEqual(['onClick', 'onBlur']);
expect(argTypes.onClick.defaultValue).toEqual('pre-existing value');
});
it('should do nothing if actions are disabled', () => {
const parameters = { ...baseParameters, actions: { disable: true } };
const result = addActionsFromArgTypes({ parameters } as StoryContext);
expect(result).toEqual(parameters.argTypes);
});
});
});

View File

@ -0,0 +1,48 @@
import mapValues from 'lodash/mapValues';
import { ArgTypesEnhancer } from '@storybook/client-api';
import { action } from '../index';
// interface ActionsParameter {
// disable?: boolean;
// argTypesRegex?: RegExp;
// }
/**
* Automatically add action args for argTypes whose name
* matches a regex, such as `^on.*` for react-style `onClick` etc.
*/
export const inferActionsFromArgTypesRegex: ArgTypesEnhancer = (context) => {
const { actions, argTypes } = context.parameters;
if (!actions || actions.disable || !actions.argTypesRegex || !argTypes) {
return argTypes;
}
const argTypesRegex = new RegExp(actions.argTypesRegex);
return mapValues(argTypes, (argType, name) => {
if (!argTypesRegex.test(name)) {
return argType;
}
return { ...argType, defaultValue: argType.defaultValue || action(name) };
});
};
/**
* Add action args for list of strings.
*/
export const addActionsFromArgTypes: ArgTypesEnhancer = (context) => {
const { argTypes, actions } = context.parameters;
if (actions?.disable || !argTypes) {
return argTypes;
}
return mapValues(argTypes, (argType, name) => {
if (!argType.action) {
return argType;
}
const message = typeof argType.action === 'string' ? argType.action : name;
return { ...argType, defaultValue: argType.defaultValue || action(message) };
});
};
export const argTypesEnhancers = [addActionsFromArgTypes, inferActionsFromArgTypesRegex];

View File

@ -0,0 +1,3 @@
import { withActions } from '../index';
export const decorators = [withActions];

View File

@ -0,0 +1,15 @@
interface ActionsOptions {
addDecorator?: boolean;
}
export function managerEntries(entry: any[] = [], options: any) {
return [...entry, require.resolve('../register')];
}
export function config(entry: any[] = [], { addDecorator = true }: ActionsOptions = {}) {
const actionConfig = [];
if (addDecorator) {
actionConfig.push(require.resolve('./addDecorator'));
}
return [...entry, ...actionConfig, require.resolve('./addArgs')];
}

View File

@ -8,7 +8,7 @@ const createChannel = () => {
addons.getChannel.mockReturnValue(channel);
return channel;
};
const getChannelData = channel => channel.emit.mock.calls[0][1].data.args;
const getChannelData = (channel) => channel.emit.mock.calls[0][1].data.args;
describe('Action', () => {
it('with one argument', () => {

View File

@ -0,0 +1,90 @@
import addons from '@storybook/addons';
import { actions } from '../..';
jest.mock('@storybook/addons');
const createChannel = () => {
const channel = { emit: jest.fn() };
addons.getChannel.mockReturnValue(channel);
return channel;
};
const getChannelData = (channel, callIndex) => channel.emit.mock.calls[callIndex][1].data;
const getChannelOptions = (channel, callIndex) => channel.emit.mock.calls[callIndex][1].options;
describe('Actions', () => {
it('with one argument', () => {
const channel = createChannel();
const actionsResult = actions('test-action');
expect(Object.keys(actionsResult)).toEqual(['test-action']);
actionsResult['test-action']('one');
expect(getChannelData(channel, 0)).toEqual({ name: 'test-action', args: ['one'] });
});
it('with multiple arguments', () => {
const channel = createChannel();
const actionsResult = actions('test-action', 'test-action2');
expect(Object.keys(actionsResult)).toEqual(['test-action', 'test-action2']);
actionsResult['test-action']('one');
actionsResult['test-action2']('two');
expect(getChannelData(channel, 0)).toEqual({ name: 'test-action', args: ['one'] });
expect(getChannelData(channel, 1)).toEqual({ name: 'test-action2', args: ['two'] });
});
it('with multiple arguments + config', () => {
const channel = createChannel();
const actionsResult = actions('test-action', 'test-action2', { some: 'config' });
expect(Object.keys(actionsResult)).toEqual(['test-action', 'test-action2']);
actionsResult['test-action']('one');
actionsResult['test-action2']('two');
expect(getChannelData(channel, 0)).toEqual({ name: 'test-action', args: ['one'] });
expect(getChannelData(channel, 1)).toEqual({ name: 'test-action2', args: ['two'] });
expect(getChannelOptions(channel, 0).some).toEqual('config');
expect(getChannelOptions(channel, 1).some).toEqual('config');
});
it('with multiple arguments as object', () => {
const channel = createChannel();
const actionsResult = actions({
'test-action': 'test action',
'test-action2': 'test action two',
});
expect(Object.keys(actionsResult)).toEqual(['test-action', 'test-action2']);
actionsResult['test-action']('one');
actionsResult['test-action2']('two');
expect(getChannelData(channel, 0)).toEqual({ name: 'test action', args: ['one'] });
expect(getChannelData(channel, 1)).toEqual({ name: 'test action two', args: ['two'] });
});
it('with first argument as array of arguments + config', () => {
const channel = createChannel();
const actionsResult = actions(['test-action', 'test-action2', { some: 'config' }]);
expect(Object.keys(actionsResult)).toEqual(['test-action', 'test-action2']);
actionsResult['test-action']('one');
actionsResult['test-action2']('two');
expect(getChannelData(channel, 0)).toEqual({ name: 'test-action', args: ['one'] });
expect(getChannelData(channel, 1)).toEqual({ name: 'test-action2', args: ['two'] });
expect(getChannelOptions(channel, 0).some).toEqual('config');
expect(getChannelOptions(channel, 1).some).toEqual('config');
});
});

View File

@ -1,4 +1,4 @@
import uuid from 'uuid/v4';
import { v4 as uuidv4 } from 'uuid';
import { addons } from '@storybook/addons';
import { EVENT_ID } from '../constants';
import { ActionDisplay, ActionOptions, HandlerFunction } from '../models';
@ -12,7 +12,7 @@ export function action(name: string, options: ActionOptions = {}): HandlerFuncti
const handler = function actionHandler(...args: any[]) {
const channel = addons.getChannel();
const id = uuid();
const id = uuidv4();
const minDepth = 5; // anything less is really just storybook internals
const actionDisplayToEmit: ActionDisplay = {

View File

@ -4,9 +4,13 @@ import { config } from './configureActions';
export const actions: ActionsFunction = (...args: any[]) => {
let options: ActionOptions = config;
const names = args;
let names = args;
// args argument can be a single argument as an array
if (names.length === 1 && Array.isArray(names[0])) {
[names] = names;
}
// last argument can be options
if (names.length !== 1 && typeof args[args.length - 1] !== 'string') {
if (names.length !== 1 && typeof names[names.length - 1] !== 'string') {
options = {
...config,
...names.pop(),
@ -16,13 +20,13 @@ export const actions: ActionsFunction = (...args: any[]) => {
let namesObject = names[0];
if (names.length !== 1 || typeof namesObject === 'string') {
namesObject = {};
names.forEach(name => {
names.forEach((name) => {
namesObject[name] = name;
});
}
const actionsObject: ActionsMap = {};
Object.keys(namesObject).forEach(name => {
Object.keys(namesObject).forEach((name) => {
actionsObject[name] = action(namesObject[name], options);
});
return actionsObject;

View File

@ -1,37 +1,36 @@
import { action } from './action';
import { actions } from './actions';
import { createDecorator } from './withActions';
import { ActionOptions, DecoratorFunction, HandlerFunction } from '../models';
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
const applyDecorators = (decorators: DecoratorFunction[], actionCallback: HandlerFunction) => {
return (..._args: any[]) => {
const decorated = decorators.reduce((args, storyFn) => storyFn(args), _args);
actionCallback(...decorated);
};
import { DecoratorFunction } from '../models';
export const decorateAction = (_decorators: DecoratorFunction[]) => {
return deprecate(
() => {},
dedent`
decorateAction is no longer supported as of Storybook 6.0.
`
);
};
export const decorateAction = (
decorators: DecoratorFunction[]
): ((name: string, options?: ActionOptions) => HandlerFunction) => {
return (name: string, options?: ActionOptions) => {
const callAction = action(name, options);
return applyDecorators(decorators, callAction);
};
};
export const decorate = (decorators: DecoratorFunction[]) => {
const decorated = decorateAction(decorators);
const decoratedActions = (...args: any[]) => {
const rawActions = actions(...args);
const actionsObject = {} as any;
Object.keys(rawActions).forEach(name => {
actionsObject[name] = applyDecorators(decorators, rawActions[name]);
});
return actionsObject;
};
return {
action: decorated,
actions: decoratedActions,
withActions: createDecorator(decoratedActions),
};
export const decorate = (_decorators: DecoratorFunction[]) => {
return deprecate(
() => {
return {
action: deprecate(() => {}, 'decorate.action is no longer supported as of Storybook 6.0.'),
actions: deprecate(() => {},
'decorate.actions is no longer supported as of Storybook 6.0.'),
withActions: deprecate(() => {},
'decorate.withActions is no longer supported as of Storybook 6.0.'),
};
},
dedent`
decorate is deprecated, please configure addon-actions using the addParameter api:
addParameters({
actions: {
handles: options
},
});
`
);
};

View File

@ -1,9 +1,14 @@
// Based on http://backbonejs.org/docs/backbone.html#section-164
import { document, Element } from 'global';
import { useEffect } from '@storybook/client-api';
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
import { makeDecorator } from '@storybook/addons';
import { actions } from './actions';
import { PARAM_KEY } from '../constants';
const delegateEventSplitter = /^(\S+)\s*(.*)$/;
const isIE = Element != null && !Element.prototype.matches;
@ -22,8 +27,8 @@ const hasMatchInAncestry = (element: any, selector: any): boolean => {
return hasMatchInAncestry(parent, selector);
};
const createHandlers = (actionsFn: (...arg: any[]) => object, ...args: any[]) => {
const actionsObject = actionsFn(...args);
const createHandlers = (actionsFn: (...arg: any[]) => object, ...handles: any[]) => {
const actionsObject = actionsFn(...handles);
return Object.entries(actionsObject).map(([key, action]) => {
const [_, eventName, selector] = key.match(delegateEventSplitter);
return {
@ -37,18 +42,44 @@ const createHandlers = (actionsFn: (...arg: any[]) => object, ...args: any[]) =>
});
};
export const createDecorator = (actionsFn: any) => (...args: any[]) => (storyFn: () => any) => {
const applyEventHandlers = (actionsFn: any, ...handles: any[]) => {
useEffect(() => {
if (root != null) {
const handlers = createHandlers(actionsFn, ...args);
const handlers = createHandlers(actionsFn, ...handles);
handlers.forEach(({ eventName, handler }) => root.addEventListener(eventName, handler));
return () =>
handlers.forEach(({ eventName, handler }) => root.removeEventListener(eventName, handler));
}
return undefined;
}, [root, actionsFn, args]);
return storyFn();
}, [root, actionsFn, handles]);
};
export const withActions = createDecorator(actions);
const applyDeprecatedOptions = (actionsFn: any, options: any[]) => {
if (options) {
deprecate(
() => applyEventHandlers(actionsFn, options),
dedent`
withActions(options) is deprecated, please configure addon-actions using the addParameter api:
addParameters({
actions: {
handles: options
},
});
`
)();
}
};
export const withActions = makeDecorator({
name: 'withActions',
parameterName: PARAM_KEY,
skipIfNoParametersOrOptions: true,
wrapper: (getStory, context, { parameters, options }) => {
applyDeprecatedOptions(actions, options as any[]);
if (parameters && parameters.handles) applyEventHandlers(actions, ...parameters.handles);
return getStory(context);
},
});

View File

@ -0,0 +1,13 @@
import React from 'react';
import { addons, types } from '@storybook/addons';
import ActionLogger from './containers/ActionLogger';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
addons.register(ADDON_ID, (api) => {
addons.addPanel(PANEL_ID, {
title: 'Actions',
type: types.PANEL,
render: ({ active, key }) => <ActionLogger key={key} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});

View File

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

View File

@ -20,7 +20,7 @@ Add following content to it:
```js
module.exports = {
addons: ['@storybook/addon-backgrounds/register']
addons: ['@storybook/addon-backgrounds']
}
```

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