mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-21 05:02:39 +08:00
Merge branch 'next' into pr/9531
This commit is contained in:
commit
8a882e9eaa
@ -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',
|
||||
|
@ -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'];
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
45
.eslintrc.js
45
.eslintrc.js
@ -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
1
.github/CODEOWNERS
vendored
@ -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
|
||||
|
1
.github/autolabeler.yml
vendored
1
.github/autolabeler.yml
vendored
@ -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/**"]
|
||||
|
7
.github/automention.yml
vendored
7
.github/automention.yml
vendored
@ -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']
|
||||
|
38
.github/workflows/tests-cli.yml
vendored
38
.github/workflows/tests-cli.yml
vendored
@ -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
|
||||
|
32
.github/workflows/tests-puppeteer.yml
vendored
32
.github/workflows/tests-puppeteer.yml
vendored
@ -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
|
14
.github/workflows/tests-unit.yml
vendored
14
.github/workflows/tests-unit.yml
vendored
@ -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
1
.gitignore
vendored
@ -7,6 +7,7 @@ node_modules
|
||||
*.sw*
|
||||
npm-shrinkwrap.json
|
||||
dist
|
||||
ts3.5
|
||||
.tern-port
|
||||
*.DS_Store
|
||||
.cache
|
||||
|
@ -1 +0,0 @@
|
||||
.yarn
|
@ -3,5 +3,6 @@
|
||||
"tabWidth": 2,
|
||||
"bracketSpacing": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true
|
||||
"singleQuote": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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"
|
||||
}
|
||||
})
|
@ -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
107
.teamcity/pom.xml
vendored
Normal 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
614
.teamcity/settings.kts
vendored
Normal 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()
|
||||
}
|
||||
}
|
||||
})
|
@ -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>)
|
||||
|
2589
CHANGELOG.md
2589
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
|
612
MIGRATION.md
612
MIGRATION.md
@ -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} />
|
||||
))
|
||||
)
|
||||
|
49
README.md
49
README.md
@ -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) | [](app/react) |
|
||||
| [React Native](app/react-native) | - | [](app/react-native) |
|
||||
| [Vue](app/vue) | [v5.1.0](https://storybooks-vue.netlify.com/) | [](app/vue) |
|
||||
| [Angular](app/angular) | [v5.1.0](https://storybooks-angular.netlify.com/) | [](app/angular) |
|
||||
| [Marionette.js](app/marionette) | - | [](app/marionette) |
|
||||
| [Mithril](app/mithril) | [v5.1.0](https://storybooks-mithril.netlify.com/) | [](app/mithril) |
|
||||
| [Marko](app/marko) | [v5.1.0](https://storybooks-marko.netlify.com/) | [](app/marko) |
|
||||
| [HTML](app/html) | [v5.1.0](https://storybooks-html.netlify.com/) | [](app/html) |
|
||||
| [Svelte](app/svelte) | [v5.1.0](https://storybooks-svelte.netlify.com/) | [](app/svelte) |
|
||||
| [Riot](app/riot) | [v5.1.0](https://storybooks-riot.netlify.com/) | [](app/riot) |
|
||||
| [Ember](app/ember) | [v5.1.0](https://storybooks-ember.netlify.com/) | [](app/ember) |
|
||||
| [Preact](app/preact) | [v5.1.0](https://storybooks-preact.netlify.com/) | [](app/preact) |
|
||||
| [Rax](app/rax) | [v5.1.0](https://storybooks-rax.netlify.com/) | [](app/rax) |
|
||||
| Framework | Demo | |
|
||||
| -------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| [React](app/react) | [v5.3.0](https://storybookjs.netlify.com/official-storybook/?path=/story/*) | [](app/react) |
|
||||
| [React Native](app/react-native) | - | [](app/react-native) |
|
||||
| [Vue](app/vue) | [v5.3.0](https://storybookjs.netlify.com/vue-kitchen-sink/) | [](app/vue) |
|
||||
| [Angular](app/angular) | [v5.3.0](https://storybookjs.netlify.com/angular-cli/) | [](app/angular) |
|
||||
| [Marionette.js](app/marionette) | - | [](app/marionette) |
|
||||
| [Mithril](app/mithril) | [v5.3.0](https://storybookjs.netlify.com/mithril-kitchen-sink/) | [](app/mithril) |
|
||||
| [Marko](app/marko) | [v5.3.0](https://storybookjs.netlify.com/marko-cli/) | [](app/marko) |
|
||||
| [HTML](app/html) | [v5.3.0](https://storybookjs.netlify.com/html-kitchen-sink/) | [](app/html) |
|
||||
| [Svelte](app/svelte) | [v5.3.0](https://storybookjs.netlify.com/svelte-kitchen-sink/) | [](app/svelte) |
|
||||
| [Riot](app/riot) | [v5.3.0](https://storybookjs.netlify.com/riot-kitchen-sink/) | [](app/riot) |
|
||||
| [Ember](app/ember) | [v5.3.0](https://storybookjs.netlify.com/ember-cli/) | [](app/ember) |
|
||||
| [Preact](app/preact) | [v5.3.0](https://storybookjs.netlify.com/preact-kitchen-sink/) | [](app/preact) |
|
||||
| [Rax](app/rax) | [v5.3.0](https://storybookjs.netlify.com/rax-kitchen-sink/) | [](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.
|
||||
|
88
ROADMAP.md
88
ROADMAP.md
@ -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.
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
1
addons/a11y/preset.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/preset');
|
46
addons/a11y/src/a11yHighlight.ts
Normal file
46
addons/a11y/src/a11yHighlight.ts
Normal 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);
|
25
addons/a11y/src/a11yRunner.test.ts
Normal file
25
addons/a11y/src/a11yRunner.test.ts
Normal 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));
|
||||
});
|
||||
});
|
58
addons/a11y/src/a11yRunner.ts
Normal file
58
addons/a11y/src/a11yRunner.ts
Normal 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);
|
@ -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();
|
||||
});
|
||||
});
|
139
addons/a11y/src/components/A11YPanel.test.tsx
Normal file
139
addons/a11y/src/components/A11YPanel.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
133
addons/a11y/src/components/A11yContext.test.tsx
Normal file
133
addons/a11y/src/components/A11yContext.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
117
addons/a11y/src/components/A11yContext.tsx
Normal file
117
addons/a11y/src/components/A11yContext.tsx
Normal 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);
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
64
addons/a11y/src/components/ColorFilters.tsx
Normal file
64
addons/a11y/src/components/ColorFilters.tsx
Normal 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>
|
||||
);
|
@ -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} />
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
93
addons/a11y/src/components/Report/HighlightToggle.test.tsx
Normal file
93
addons/a11y/src/components/Report/HighlightToggle.test.tsx
Normal 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
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
@ -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;
|
||||
|
@ -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 ? (
|
||||
|
@ -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 }) => {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
`;
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 };
|
||||
|
12
addons/a11y/src/highlight.ts
Normal file
12
addons/a11y/src/highlight.ts
Normal 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),',
|
||||
});
|
@ -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
14
addons/a11y/src/params.ts
Normal 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;
|
||||
}
|
7
addons/a11y/src/preset.ts
Normal file
7
addons/a11y/src/preset.ts
Normal 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')];
|
||||
}
|
@ -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;
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
@ -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__/**/*"]
|
||||
}
|
||||
|
@ -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>;
|
||||
```
|
||||
|
@ -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
1
addons/actions/preset.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/preset');
|
@ -1 +1 @@
|
||||
require('./dist/manager').register();
|
||||
require('./dist/register');
|
||||
|
@ -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 }) => ({
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
76
addons/actions/src/preset/addArgs.test.ts
Normal file
76
addons/actions/src/preset/addArgs.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
48
addons/actions/src/preset/addArgs.ts
Normal file
48
addons/actions/src/preset/addArgs.ts
Normal 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];
|
3
addons/actions/src/preset/addDecorator.ts
Normal file
3
addons/actions/src/preset/addDecorator.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { withActions } from '../index';
|
||||
|
||||
export const decorators = [withActions];
|
15
addons/actions/src/preset/index.ts
Normal file
15
addons/actions/src/preset/index.ts
Normal 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')];
|
||||
}
|
@ -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', () => {
|
||||
|
90
addons/actions/src/preview/__tests__/actions.test.js
Normal file
90
addons/actions/src/preview/__tests__/actions.test.js
Normal 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');
|
||||
});
|
||||
});
|
@ -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 = {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
},
|
||||
});
|
||||
`
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
13
addons/actions/src/register.tsx
Normal file
13
addons/actions/src/register.tsx
Normal 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,
|
||||
});
|
||||
});
|
@ -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"]
|
||||
}
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user