Merge branch 'next' into shilman/presets-docs

This commit is contained in:
Michael Shilman 2019-05-22 07:17:00 -07:00
commit c0f025d0aa
1283 changed files with 93420 additions and 45707 deletions

View File

@ -1,28 +1,68 @@
const withTests = {
presets: [
[
'@babel/preset-env',
{ shippedProposals: true, useBuiltIns: 'usage', corejs: '3', targets: { node: 'current' } },
],
],
plugins: [
'babel-plugin-require-context-hook',
'babel-plugin-dynamic-import-node',
'@babel/plugin-transform-runtime',
],
};
module.exports = {
presets: [
['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage' }],
['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage', corejs: '3' }],
'@babel/preset-typescript',
'@babel/preset-react',
'@babel/preset-flow',
],
plugins: [
['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
[
'@babel/plugin-proposal-decorators',
{
legacy: true,
},
],
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-proposal-class-properties', { loose: true }],
'babel-plugin-add-react-displayname',
['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
'babel-plugin-macros',
['emotion', { sourceMap: true, autoLabel: true }],
],
env: {
test: {
presets: [['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage' }]],
plugins: ['babel-plugin-require-context-hook', 'babel-plugin-dynamic-import-node'],
},
test: withTests,
},
overrides: [
{
test: './examples/vue-kitchen-sink',
presets: ['babel-preset-vue'],
env: {
test: withTests,
},
},
{
test: './lib',
presets: [
['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage', corejs: '3' }],
'@babel/preset-react',
],
plugins: [
['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-proposal-class-properties', { loose: true }],
'babel-plugin-macros',
['emotion', { sourceMap: true, autoLabel: true }],
'@babel/plugin-transform-react-constant-elements',
'babel-plugin-add-react-displayname',
],
env: {
test: withTests,
},
},
{
test: [
@ -33,7 +73,6 @@ module.exports = {
'./addons/storysource/src/loader',
'./app/**/src/server/**',
'./app/**/src/bin/**',
'./dangerfile.js',
],
presets: [
[
@ -44,9 +83,20 @@ module.exports = {
targets: {
node: '8.11',
},
corejs: '3',
},
],
],
plugins: [
'emotion',
'babel-plugin-macros',
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-export-default-from',
],
env: {
test: withTests,
},
},
],
};

View File

@ -1,7 +1,11 @@
import { fail, danger } from 'danger';
import { flatten, intersection, isEmpty } from 'lodash';
import { execSync } from 'child_process';
const pkg = require('./package.json'); // eslint-disable-line import/newline-after-import
execSync('npm install lodash');
const { flatten, intersection, isEmpty } = require('lodash');
const pkg = require('../../package.json'); // eslint-disable-line import/newline-after-import
const prLogConfig = pkg['pr-log'];
const Versions = {
@ -20,18 +24,11 @@ const checkRequiredLabels = labels => {
branchVersion === Versions.PATCH ? 'feature request' : [],
]);
const requiredLabels = flatten([
prLogConfig.skipLabels || [],
(prLogConfig.validLabels || []).map(keyVal => keyVal[0]),
]);
const requiredLabels = flatten([prLogConfig.skipLabels || [], (prLogConfig.validLabels || []).map(keyVal => keyVal[0])]);
const blockingLabels = intersection(forbiddenLabels, labels);
if (!isEmpty(blockingLabels)) {
fail(
`PR is marked with ${blockingLabels.map(label => `"${label}"`).join(', ')} label${
blockingLabels.length > 1 ? 's' : ''
}.`
);
fail(`PR is marked with ${blockingLabels.map(label => `"${label}"`).join(', ')} label${blockingLabels.length > 1 ? 's' : ''}.`);
}
const foundLabels = intersection(requiredLabels, labels);

View File

@ -40,6 +40,15 @@ jobs:
- addons
- app
- lib
chromatic:
<<: *defaults
steps:
- checkout
- attach_workspace:
at: .
- run:
name: Run chromatic on the pre-built storybook
command: yarn chromatic -- -d ./storybook-static
examples:
<<: *defaults
steps:
@ -109,17 +118,30 @@ jobs:
command: |
cd examples/preact-kitchen-sink
yarn build-storybook
- run:
name: Build cra react15
command: |
cd examples/cra-react15
yarn build-storybook
- run:
name: Build official-storybook
command: |
cd examples/official-storybook
yarn build-storybook
- run:
name: Run image snapshots
command: yarn test --image
# - run:
# name: Run image snapshots
# command: yarn test --image
- store_artifacts:
path: examples/official-storybook/image-snapshots/__image_snapshots__
destination: official_storybook_image_snapshots
- persist_to_workspace:
root: .
paths:
- node_modules
- examples
- addons
- app
- lib
smoke-tests:
<<: *defaults
steps:
@ -186,6 +208,11 @@ jobs:
command: |
cd examples/preact-kitchen-sink
yarn storybook --smoke-test --quiet
- run:
name: Run cra reac15 (smoke test)
command: |
cd examples/cra-react15
yarn storybook --smoke-test --quiet
native-smoke-tests:
<<: *defaults
steps:
@ -194,12 +221,35 @@ jobs:
at: .
- run:
name: Bootstrap
command: yarn bootstrap --reactnativeapp
command: yarn bootstrap --core
- run:
name: Run React-Native-App example
command: |
cd examples-native/crna-kitchen-sink
yarn storybook --smoke-test
- run:
name: Publish React-Native-App example
command: |
./scripts/crna-publish.js
frontpage:
<<: *defaults
steps:
- checkout
- restore_cache:
name: Restore core dependencies cache
keys:
- core-dependencies-v3-{{ checksum "yarn.lock" }}
- run:
name: Install dependencies
command: yarn install
- run:
name: Trigger build
command: ./scripts/build-frontpage.js
- save_cache:
name: Cache core dependencies
key: core-dependencies-v3-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
docs:
<<: *defaults
steps:
@ -245,7 +295,7 @@ jobs:
at: .
- run:
name: Test
command: yarn test --coverage --runInBand --core
command: yarn test --coverage --w2 --core
- persist_to_workspace:
root: .
paths:
@ -288,6 +338,7 @@ workflows:
jobs:
- build
- docs
- frontpage
- lint:
requires:
- docs
@ -313,3 +364,6 @@ workflows:
- cli-test-latest-cra:
requires:
- build
- chromatic:
requires:
- examples

View File

@ -7,10 +7,9 @@ docs/public
storybook-static
built-storybooks
lib/cli/test
scripts/storage
*.bundle.js
*.js.map
*.ts
*.tsx
!.remarkrc.js
!.babelrc.js
@ -18,3 +17,8 @@ lib/cli/test
!.eslintrc-markdown.js
!.jest.config.js
!.storybook
REACT_NATIVE
examples-native
react-native
ondevice-*

View File

@ -1,26 +1,68 @@
const error = 2;
const warn = 1;
const ignore = 0;
module.exports = {
root: true,
extends: [
'airbnb',
'plugin:jest/recommended',
'plugin:import/react-native',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/react',
'prettier/@typescript-eslint',
],
plugins: ['prettier', 'jest', 'import', 'react', 'jsx-a11y', 'json', 'html'],
parser: 'babel-eslint',
parserOptions: { ecmaVersion: 8, sourceType: 'module' },
plugins: [
'@typescript-eslint',
'prettier',
'jest',
'import',
'react',
'jsx-a11y',
'json',
'html',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 8,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
env: { es6: true, node: true, 'jest/globals': true },
settings: {
'import/core-modules': ['enzyme'],
'import/ignore': ['node_modules\\/(?!@storybook)'],
'import/resolver': { node: { extensions: ['.js', '.ts'] } },
'import/resolver': { node: { extensions: ['.js', '.ts', '.tsx', '.mjs'] } },
'html/html-extensions': ['.html'],
},
rules: {
'no-restricted-imports': [
error,
{
paths: [
{
name: 'lodash.isequal',
message:
'Lodash modularised (and lodash < 4.17.11) have CVE vulnerabilities. Please use tree-shakeable imports like lodash/xxx instead',
},
{
name: 'lodash.mergewith',
message:
'Lodash modularised (and lodash < 4.17.11) have CVE vulnerabilities. Please use tree-shakeable imports like lodash/xxx instead',
},
{
name: 'lodash.pick',
message:
'Lodash modularised (and lodash < 4.17.11) have CVE vulnerabilities. Please use tree-shakeable imports like lodash/xxx instead',
},
],
// catch-all for any lodash modularised. The CVE is listed against the entire family for lodash < 4.17.11
patterns: ['lodash.*'],
},
],
'prettier/prettier': [warn],
'no-debugger': process.env.NODE_ENV === 'production' ? error : ignore,
'class-methods-use-this': ignore,
@ -30,6 +72,7 @@ module.exports = {
{
js: 'never',
ts: 'never',
tsx: 'never',
mjs: 'never',
},
],
@ -42,11 +85,11 @@ module.exports = {
'**/example/**',
'*.js',
'**/*.test.js',
'**/*.stories.js',
'**/*.stories.*',
'**/scripts/*.js',
'**/stories/**/*.js',
'**/__tests__/**/*.js',
'**/.storybook/**/*.js',
'**/.storybook/**/*.*',
],
peerDependencies: true,
},
@ -94,19 +137,50 @@ module.exports = {
error,
{ allow: ['__STORYBOOK_CLIENT_API__', '__STORYBOOK_ADDONS_CHANNEL__'] },
],
'@typescript-eslint/no-var-requires': ignore,
'@typescript-eslint/camelcase': ignore,
'@typescript-eslint/no-unused-vars': ignore,
'@typescript-eslint/explicit-member-accessibility': ignore,
'@typescript-eslint/explicit-function-return-type': ignore,
'@typescript-eslint/no-explicit-any': ignore, // would prefer to enable this
'@typescript-eslint/no-use-before-define': ignore, // this is duplicated
'@typescript-eslint/interface-name-prefix': ignore, // I don't agree
},
overrides: [
{
files: [
'**/__tests__/**',
'**/*.test.js/**',
'**/*.stories.js',
'**/*.test.*',
'**/*.stories.*',
'**/storyshots/**/stories/**',
'docs/src/new-components/lib/StoryLinkWrapper.js',
'docs/src/stories/**',
],
rules: {
'import/no-extraneous-dependencies': ignore,
},
},
{ files: '**/.storybook/config.js', rules: { 'global-require': ignore } },
{
files: ['**/*.stories.*'],
rules: {
'no-console': ignore,
},
},
{
files: ['**/*.tsx', '**/*.ts'],
rules: {
'react/prop-types': ignore, // we should use types
'no-dupe-class-members': ignore, // this is called overloads in typescript
},
},
{
files: ['**/*.d.ts'],
rules: {
'vars-on-top': ignore,
'no-var': ignore, // this is how typescript works
'spaced-comment': ignore,
},
},
],
};

29
.github/CODEOWNERS vendored
View File

@ -1,19 +1,20 @@
.circleci/ @hypnosphi @ndelangen
.circleci/ @ndelangen
.teamcity/ @hypnosphi
.github/ @danielduan
/addons/a11y/ @jbovenschen
/addons/a11y/ @jbovenschen @codebyalex
/addons/actions/ @rhalff
/addons/backgrounds/ @ndelangen @hypnosphi
/addons/backgrounds/ @ndelangen
/addons/centered/ @kazupon
/addons/events/ @z4o4z @ndelangen
/addons/graphql/ @mnmtanish
/addons/info/ @theinterned @z4o4z @UsulPro @hypnosphi @dangreenisrael @danielduan
/addons/info/ @theinterned @z4o4z @UsulPro @dangreenisrael @danielduan
/addons/jest/ @renaudtertrais
/addons/knobs/ @alexandrebodin @theinterned @leonrodenburg @alterx @hypnosphi
/addons/links/ @hypnosphi @ndelangen
/addons/knobs/ @alexandrebodin @theinterned @leonrodenburg @alterx
/addons/links/ @ndelangen
/addons/notes/ @alexandrebodin
/addons/options/ @danielduan @UsulPro
/addons/storyshots/ @igor-dv @thomasbertet @hypnosphi
/addons/storyshots/ @igor-dv @thomasbertet
/addons/storysource/ @igor-dv
/addons/viewport/ @saponifi3d
@ -24,12 +25,12 @@
/app/vue/ @thomasbertet @kazupon
/app/svelte/ @plumpNation
/docs/ @ndelangen @shilman @hypnosphi
/docs/ @ndelangen @shilman
/examples/angular-cli/ @igor-dv @alterx
/examples/cra-kitchen-sink/ @ndelangen @UsulPro @hypnosphi
/examples/cra-kitchen-sink/ @ndelangen @UsulPro
/examples/cra-ts-kitchen-sink/ @mucsi96
/examples/official-storybook/ @hypnosphi @danielduan @UsulPro
/examples/official-storybook/ @danielduan @UsulPro
/examples/polymer-cli/ @naipath @igor-dv
/examples/vue-kitchen-sink/ @igor-dv @alexandrebodin
/examples/svelte-kitchen-sink/ @plumpNation
@ -40,12 +41,12 @@
/lib/channel-postmessage/ @mnmtanish @ndelangen
/lib/channel-websocket/ @mnmtanish @ndelangen
/lib/channels/ @mnmtanish @ndelangen
/lib/cli/ @hypnosphi @ndelangen @shilman @stijnkoopal
/lib/cli/ @ndelangen @shilman @stijnkoopal
/lib/client-logger/ @dangreenisrael
/lib/codemod/ @aaronmcadam @ndelangen
/lib/components/ @ndelangen @hypnosphi @tmeasday
/lib/components/ @ndelangen @tmeasday
/lib/core/ @tmeasday @igor-dv @alterx
/lib/node-logger/ @dangreenisrael
/lib/ui/ @tmeasday @igor-dv @hypnosphi @ndelangen
/lib/ui/ @tmeasday @igor-dv @ndelangen
/scripts/ @ndelangen @igor-dv @hypnosphi
/scripts/ @ndelangen @igor-dv

23
.github/automention.yml vendored Normal file
View File

@ -0,0 +1,23 @@
'app: angular': ['kroeder', 'igor-dv', 'MaximSagan']
'app: ember': ['gabrielcsapo']
'app: html': ['Hypnosphi']
'app: marko': ['nm123github']
'app: polymer': ['stijnkoopal', 'ndelangen']
'app: preact': ['BartWaardenburg']
'app: react-native': ['benoitdion', 'gongreg']
'app: react-native-server': ['benoitdion', 'igor-dv']
'app: vue': ['backbone87', 'elevatebart']
'api: addons': ['ndelangen']
'addon: a11y': ['CodeByAlex', 'Armanio', 'jsomsanith']
'addon: contexts': ['leoyli']
'addon: docs': ['shilman', 'elevatebart']
'addon: info': ['shilman', 'elevatebart']
'addon: knobs': ['leoyli', 'Armanio']
'addon: storysource': ['igor-dv', 'libetl']
typescript: ['kroeder', 'gaetanmaisse', 'ndelangen']
theming: ['ndelangen', 'domyen']
cra: ['mrmckeb']
cli: ['Keraito']
dependencies: ['ndelangen']
'babel / webpack': ['ndelangen', 'igor-dv', 'shilman']
'yarn / npm': ['ndelangen', 'tmeasday']

38
.github/main.workflow vendored
View File

@ -1,12 +1,32 @@
workflow "New workflow" {
on = "push"
resolves = ["Hello World"]
action "Danger JS" {
uses = "danger/danger-js@master"
secrets = ["GITHUB_TOKEN"]
args = "--dangerfile .ci/danger/dangerfile.ts"
}
action "Hello World" {
uses = "./ci/action-a"
env = {
MY_NAME = "Mona"
}
args = "\"Hello world, I'm $MY_NAME!\""
workflow "Dangerfile JS Pull" {
on = "pull_request"
resolves = "Danger JS"
}
workflow "Dangerfile JS Label" {
on = "label"
resolves = "Danger JS"
}
# ===
action "Automention" {
uses = "shilman/automention@master"
secrets = ["GITHUB_TOKEN"]
}
workflow "Automention Issues" {
on = "issues"
resolves = "Automention"
}
workflow "Automention PRs" {
on = "pull_request"
resolves = "Automention"
}

3
.gitignore vendored
View File

@ -24,3 +24,6 @@ integration/__image_snapshots__/__diff_output__
/examples/cra-kitchen-sink/src/__image_snapshots__/__diff_output__/
lib/*.jar
lib/**/dll
.expo/packager-info.json
scripts/storage
htpasswd

View File

@ -21,11 +21,8 @@ object Project : Project({
buildType(OpenSourceProjects_Storybook_Bootstrap)
buildType(OpenSourceProjects_Storybook_CliTestLatestCra)
buildType(OpenSourceProjects_Storybook_Examples)
buildType(OpenSourceProjects_Storybook_Danger)
buildType(OpenSourceProjects_Storybook_NativeSmokeTests)
buildType(OpenSourceProjects_Storybook_Docs)
buildType(OpenSourceProjects_Storybook_Build_2)
buildType(OpenSourceProjects_Storybook_CliTest)
buildType(OpenSourceProjects_Storybook_Test)
buildType(OpenSourceProjects_Storybook_Lint)
buildType(OpenSourceProjects_Storybook_Lint_Warnings)

View File

@ -39,6 +39,7 @@ object OpenSourceProjects_Storybook_Build_2 : BuildType({
}
retryBuild {
delaySeconds = 60
enabled = false
}
finishBuildTrigger {
enabled = false
@ -95,11 +96,6 @@ object OpenSourceProjects_Storybook_Build_2 : BuildType({
onDependencyCancel = FailureAction.ADD_PROBLEM
}
}
dependency(OpenSourceProjects_Storybook.buildTypes.OpenSourceProjects_Storybook_NativeSmokeTests) {
snapshot {
onDependencyCancel = FailureAction.ADD_PROBLEM
}
}
dependency(OpenSourceProjects_Storybook.buildTypes.OpenSourceProjects_Storybook_SmokeTests) {
snapshot {
onDependencyCancel = FailureAction.ADD_PROBLEM
@ -115,10 +111,5 @@ object OpenSourceProjects_Storybook_Build_2 : BuildType({
onDependencyCancel = FailureAction.ADD_PROBLEM
}
}
dependency(OpenSourceProjects_Storybook.buildTypes.OpenSourceProjects_Storybook_CliTest) {
snapshot {
onDependencyCancel = FailureAction.ADD_PROBLEM
}
}
}
})

View File

@ -26,10 +26,10 @@ object OpenSourceProjects_Storybook_Chromatic : BuildType({
scriptContent = """
#!/bin/sh
set -e -x
yarn
yarn chromatic
# set -e -x
# yarn
# yarn chromatic
echo "chromatic moved to cirlce CI"
""".trimIndent()
dockerImage = "node:%docker.node.version%"
}

View File

@ -1,63 +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
object OpenSourceProjects_Storybook_CliTest : BuildType({
uuid = "b1db1a3a-a4cf-46ea-8f55-98b86611f92e"
id = "OpenSourceProjects_Storybook_CliTest"
name = "CLI test"
vcs {
root(OpenSourceProjects_Storybook.vcsRoots.OpenSourceProjects_Storybook_HttpsGithubComStorybooksStorybookRefsHeadsMaster)
}
steps {
script {
name = "Test"
scriptContent = """
#!/bin/sh
set -e -x
yarn
yarn test --cli --teamcity
""".trimIndent()
dockerImage = "node:%docker.node.version%"
}
}
features {
commitStatusPublisher {
publisher = github {
githubUrl = "https://api.github.com"
authType = personalToken {
token = "credentialsJSON:5ffe2d7e-531e-4f6f-b1fc-a41bfea26eaa"
}
}
param("github_oauth_user", "Hypnosphi")
}
}
dependencies {
dependency(OpenSourceProjects_Storybook.buildTypes.OpenSourceProjects_Storybook_Bootstrap) {
snapshot {
onDependencyFailure = FailureAction.FAIL_TO_START
}
artifacts {
artifactRules = "dist.zip!**"
}
}
}
requirements {
doesNotContain("env.OS", "Windows")
}
cleanup {
artifacts(days = 1)
}
})

View File

@ -43,7 +43,9 @@ object OpenSourceProjects_Storybook_CliTestLatestCra : BuildType({
+:next
""".trimIndent()
}
retryBuild {}
retryBuild {
enabled = false
}
}
features {

View File

@ -1,79 +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_Danger : BuildType({
uuid = "759f0116-2f7d-4c03-8220-56e4ab03be3a"
id = "OpenSourceProjects_Storybook_Danger"
name = "Danger"
params {
password("env.DANGER_GITHUB_API_TOKEN", "credentialsJSON:9ac87388-d267-4def-a10e-3e596369f644")
param("env.PULL_REQUEST_URL", "https://github.com/storybooks/storybook/%teamcity.build.branch%")
}
vcs {
root(OpenSourceProjects_Storybook.vcsRoots.OpenSourceProjects_Storybook_HttpsGithubComStorybooksStorybookRefsHeadsMaster1)
buildDefaultBranch = false
}
steps {
script {
name = "Danger"
scriptContent = """
#!/bin/sh
set -e -x
yarn
yarn danger ci
""".trimIndent()
dockerImage = "node:%docker.node.version%"
}
}
triggers {
vcs {
quietPeriodMode = VcsTrigger.QuietPeriodMode.USE_DEFAULT
branchFilter = """
+:*
-:master
""".trimIndent()
}
retryBuild {
delaySeconds = 3600
}
}
features {
commitStatusPublisher {
publisher = github {
githubUrl = "https://api.github.com"
authType = personalToken {
token = "credentialsJSON:5ffe2d7e-531e-4f6f-b1fc-a41bfea26eaa"
}
}
param("github_oauth_user", "Hypnosphi")
}
feature {
type = "pullRequests"
param("filterAuthorRole", "EVERYBODY")
param("authenticationType", "token")
param("secure:accessToken", "credentialsJSON:5ffe2d7e-531e-4f6f-b1fc-a41bfea26eaa")
}
}
requirements {
doesNotContain("env.OS", "Windows")
}
cleanup {
artifacts(days = 1)
}
})

View File

@ -1,66 +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
object OpenSourceProjects_Storybook_NativeSmokeTests : BuildType({
uuid = "ac276912-df1a-44f1-8de2-056276193ce8"
id = "OpenSourceProjects_Storybook_NativeSmokeTests"
name = "Native Smoke Tests"
params {
param("env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD", "true")
}
vcs {
root(OpenSourceProjects_Storybook.vcsRoots.OpenSourceProjects_Storybook_HttpsGithubComStorybooksStorybookRefsHeadsMaster)
cleanCheckout = true
}
steps {
script {
name = "crna-kitchen-sink"
scriptContent = """
#!/bin/sh
set -e -x
yarn
yarn bootstrap --reactnativeapp
cd examples-native/crna-kitchen-sink
yarn storybook --smoke-test
""".trimIndent()
dockerImage = "node:%docker.node.version%"
}
}
features {
commitStatusPublisher {
publisher = github {
githubUrl = "https://api.github.com"
authType = personalToken {
token = "credentialsJSON:5ffe2d7e-531e-4f6f-b1fc-a41bfea26eaa"
}
}
param("github_oauth_user", "Hypnosphi")
}
}
dependencies {
dependency(OpenSourceProjects_Storybook.buildTypes.OpenSourceProjects_Storybook_Bootstrap) {
snapshot {
onDependencyFailure = FailureAction.FAIL_TO_START
}
artifacts {
artifactRules = "dist.zip!**"
}
}
}
requirements {
doesNotContain("env.OS", "Windows")
}
})

View File

@ -25,7 +25,7 @@ object OpenSourceProjects_Storybook_Test : BuildType({
set -e -x
yarn
yarn test --core --coverage --runInBand --teamcity
yarn test --core --coverage --teamcity --w2
""".trimIndent()
dockerImage = "node:%docker.node.version%"
}

View File

@ -9,6 +9,7 @@ import jetbrains.buildServer.configs.kotlin.v2017_2.failureConditions.failOnMetr
enum class StorybookApp(val appName: String, val exampleDir: String, val merged: Boolean = true) {
CRA("CRA", "cra-kitchen-sink"),
CRA_TS("CRA TS", "cra-ts-kitchen-sink"),
CRA_REACT15("CRA REACT15", "cra-react15"),
VUE("Vue", "vue-kitchen-sink"),
ANGULAR("Angular", "angular-cli"),
POLYMER("Polymer", "polymer-cli"),

View File

@ -5,13 +5,12 @@ 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 = 'b1db1a3a-a4cf-46ea-8f55-98b86611f92e' (id = 'OpenSourceProjects_Storybook_CliTest')
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("b1db1a3a-a4cf-46ea-8f55-98b86611f92e") {
params {
add {
param("docker.node.version", "latest")
}
changeBuildType("1bda59b5-d08d-4fd8-b317-953e7d79d881") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,11 @@ To apply the patch, change the buildType with uuid = '9f9177e7-9ec9-4e2e-aabb-d3
accordingly, and delete the patch script.
*/
changeBuildType("9f9177e7-9ec9-4e2e-aabb-d304fd667711") {
check(paused == false) {
"Unexpected paused: '$paused'"
}
paused = true
params {
add {
param("docker.node.version", "10.13")

View File

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

View File

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

View File

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

View File

@ -2,10 +2,11 @@
| | [React](app/react)|[React Native](app/react-native)|[Vue](app/vue)|[Angular](app/angular)| [Polymer](app/polymer)| [Mithril](app/mithril)| [HTML](app/html)| [Marko](app/marko)| [Svelte](app/svelte)| [Riot](app/riot)| [Ember](app/ember)| [Preact](app/preact)|
| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|
|[a11y](addons/a11y) |+| |+|+|+|+|+|+| | |+|+|
|[actions](addons/actions) |+|+|+|+|+|+|+|+|+|+|+|+|
|[a11y](addons/a11y) |+| |+|+|+|+|+|+|+|+|+|+|
|[actions](addons/actions) |+|+*|+|+|+|+|+|+|+|+|+|+|
|[backgrounds](addons/backgrounds) |+|*|+|+|+|+|+|+|+|+|+|+|
|[centered](addons/centered) |+| |+|+| |+|+| |+| |+|+|
|[contexts](addons/contexts) |+| |+| | | | | | | | |+|
|[events](addons/events) |+| |+|+|+|+|+|+| | |+|+|
|[graphql](addons/graphql) |+| | | | | | | | | | | |
|[google-analytics](addons/google-analytics) |+|+|+|+|+|+|+|+|+|+|+|+|

File diff suppressed because it is too large Load Diff

View File

@ -28,14 +28,16 @@ To test your project against the current latest version of storybook, you can cl
git clone https://github.com/storybooks/storybook.git
cd storybook
yarn install
yarn bootstrap --core
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:
yarn bootstrap --core
```sh
yarn bootstrap --core
```
#### 2a. Run unit tests
@ -67,7 +69,7 @@ Before the tests are run, the project must be bootstrapped with core. You can ac
This option executes tests from `<rootdir>/examples/official-storybook`
In order for the image snapshots to be correctly generated, you must have a static build of the storybook up-to-date :
```javascript
```sh
cd examples/official-storybook
yarn build-storybook
cd ../..
@ -80,68 +82,118 @@ Puppeteer is used to launch and grab screenshots of example pages, while jest is
If you made any changes to the `lib/cli` package, the easiest way to verify that it doesn't break anything is to run e2e tests:
yarn test --cli
```sh
yarn test --cli
```
This will run a bash script located at `lib/cli/test/run_tests.sh`. It will copy the contents of `fixtures` into a temporary `run` directory, run `getstorybook` in each of the subdirectories, and check that storybook starts successfully using `yarn storybook --smoke-test`.
After that, the `run` directory content will be compared with `snapshots`. You can update the snapshots by passing an `--update` flag:
yarn test --cli --update
```sh
yarn test --cli --update
```
In that case, please check the git diff before committing to make sure it only contains the intended changes.
#### 2c. Link `storybook` and any other required dependencies:
#### 2c. Run Linter
If you want to test your own existing project using the GitHub version of storybook, you need to `link` the packages you use in your project.
We use eslint as a linter for all code (including typescript code).
All you have to run is:
```sh
cd app/react
yarn link
cd <your-project>
yarn link @storybook/react
# repeat with whichever other parts of the monorepo you are using.
yarn lint
```
It can be immensely helpful to get feedback in your editor, if you're using VsCode, you should install the `eslint` plugin and configure it with these settings:
```plaintext
"eslint.autoFixOnSave": true,
"eslint.packageManager": "yarn",
"eslint.options": {
"cache": true,
"cacheLocation": ".cache/eslint",
"extensions": [".js", ".jsx", ".mjs", ".json", ".ts", ".tsx"]
},
"eslint.validate": [
"javascript",
"javascriptreact",
{"language": "typescript", "autoFix": true },
{"language": "typescriptreact", "autoFix": true }
],
"eslint.alwaysShowStatus": true
```
This should enable auto-fix for all source files, and give linting warnings and errors within your editor.
### Reproductions
#### In the monorepo
The best way to help figure out an issue you are having is to produce a minimal reproduction against the `master` branch.
A good way to do that is using the example `cra-kitchen-sink` app embedded in this repository:
```sh
# Download and build this repository:
git clone https://github.com/storybooks/storybook.git
cd storybook
yarn install
yarn bootstrap --core
# Download and build this repository:
git clone https://github.com/storybooks/storybook.git
cd storybook
yarn install
yarn bootstrap --core
# make changes to try and reproduce the problem, such as adding components + stories
cd examples/cra-kitchen-sink
yarn storybook
# make changes to try and reproduce the problem, such as adding components + stories
cd examples/cra-kitchen-sink
yarn storybook
# see if you can see the problem, if so, commit it:
git checkout "branch-describing-issue"
git add -A
git commit -m "reproduction for issue #123"
# see if you can see the problem, if so, commit it:
git checkout "branch-describing-issue"
git add -A
git commit -m "reproduction for issue #123"
# fork the storybook repo to your account, then add the resulting remote
git remote add <your-username> https://github.com/<your-username>/storybook.git
git push -u <your-username> master
# fork the storybook repo to your account, then add the resulting remote
git remote add <your-username> https://github.com/<your-username>/storybook.git
git push -u <your-username> master
```
If you follow that process, you can then link to the GitHub repository in the issue. See <https://github.com/storybooks/storybook/issues/708#issuecomment-290589886> for an example.
**NOTE**: If your issue involves a webpack config, create-react-app will prevent you from modifying the _app's_ webpack config, however, you can still modify storybook's to mirror your app's version of the storybook. Alternatively, use `yarn eject` in the CRA app to get a modifiable webpack config.
#### Outside the monorepo
Sometimes your storybook is deeply ingrained in your own setup and it's hard to create a minimal viable reproduction somewhere else.
Inside the storybook repo we have a script that allows you to test the packages inside this repo in your own seperate project.
You can use `npm link` on all packages, but npm linking is cumbersome and has subtle differences from what happens in a registry-based installation.
So the way our script works is that it:
- sets up a npm registry running on your own local machine
- changes your default registry to this local one
- builds all packages in the storybook repo
- publishes all packages as latest
Our script leaves the local registry running, for **as long as you keep it running** you can install storybook packages from this local registry.
- Navigate to your own project and then change `package.json` so the storybook packages match the version of the one you just published.
- Then just do the normal install procedure using `yarn` or `npm`
- Start using your storybook as normally.
If you've made a change to storybook's codebase and would want this change to be reflected in your app:
- Ensure the storybook packages are transpiled, by either having run `yarn dev` or `yarn bootstrap --core`.
- Go to the terminal where the local regitry is running and press `<Enter>`. This will kick off a new publish.
- Run the install procedure again in your local repo, (you may need to clean out node_modules first).
- Restart your storybook.
### Updating Tests
Before any contributes are submitted in a PR, make sure to add or update meaningful tests. A PR that has failing tests will be regarded as a “Work in Progress” and will not be merged until all tests pass.
When creating new unit test files, the tests should adhere to a particular folder structure and naming convention, as defined below.
```sh
#Proper naming convention and structure for js tests files
# Proper naming convention and structure for js tests files
+-- parentFolder
| +-- [filename].js
| +-- [filename].test.js
@ -149,14 +201,9 @@ When creating new unit test files, the tests should adhere to a particular folde
## Pull Requests (PRs)
We welcome your contributions. There are many ways you can help us. This is few of those ways:
We welcome all contributions. There are many ways you can help us. This is few of those ways:
- Fix typos and add more [documentation](https://github.com/storybooks/storybook/labels/needs%20docs).
- Try to fix some [bugs](https://github.com/storybooks/storybook/labels/bug).
- Work on [API](https://github.com/storybooks/storybook/labels/enhancement%3A%20api), [Addons](https://github.com/storybooks/storybook/labels/enhancement%3A%20addons), [UI](https://github.com/storybooks/storybook/labels/enhancement%3A%20ui) or [Webpack](https://github.com/storybooks/storybook/labels/enhancement%3A%20webpack) use enhancements and new [features](https://github.com/storybooks/storybook/labels/feature%20request).
- Add more [tests](https://codecov.io/gh/storybooks/storybook/tree/master/packages) (especially for the [UI](https://codecov.io/gh/storybooks/storybook/tree/master/packages/storybook-ui/src)).
Before you submit a new PR, make sure you run `yarn test`. Do not submit a PR if tests are failing. If you need any help, create an issue and ask.
Before you submit a new PR, make sure you run `yarn test`. Do not submit a PR if tests are failing. If you need any help, the best way is to [join the discord server and ask in the maintenance channel](https://discord.gg/sMFvFsG).
### Reviewing PRs

View File

@ -1,5 +1,22 @@
# Migration
- [From version 5.0.x to 5.1.x](#from-version-50x-to-51x)
- [React native server](#react-native-server)
- [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)
- [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](#addon-a11y-uses-parameters-decorator-renamed)
- [New keyboard shortcuts defaults](#new-keyboard-shortcuts-defaults)
- [New URL structure](#new-url-structure)
- [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)
@ -37,6 +54,354 @@
- [Packages renaming](#packages-renaming)
- [Deprecated embedded addons](#deprecated-embedded-addons)
## From version 5.0.x to 5.1.x
### React native server
Storybook 5.1 contains a major overhaul of `@storybook/react-native` as compared to 4.1 (we didn't ship a version of RN in 5.0 due to timing constraints). Storybook for RN consists of an an UI for browsing stories on-device or in a simulator, and an optional webserver which can also be used to browse stories and web addons.
5.1 refactors both pieces:
- `@storybook/react-native` no longer depends on the Storybook UI and only contains on-device functionality
- `@storybook/react-native-server` is a new package for those who wish to run a web server alongside their device UI
In addition, both packages share more code with the rest of Storybook, which will reduce bugs and increase compatibility (e.g. with the latest versions of babel, etc.).
As a user with an existing 4.1.x RN setup, no migration should be necessary to your RN app. Simply upgrading the library should be enough.
If you wish to run the optional web server, you will need to do the following migration:
- Add `babel-loader` as a dev dependency
- Add `@storybook/react-native-server` as a dev dependency
- Change your "storybook" `package.json` script from `storybook start [-p ...]` to `start-storybook [-p ...]`
And with that you should be good to go!
## From version 5.0.1 to 5.0.2
### Deprecate webpack extend mode
Exporting an object from your custom webpack config puts storybook in "extend mode".
There was a bad bug in `v5.0.0` involving webpack "extend mode" that caused webpack issues for users migrating from `4.x`. We've fixed this problem in `v5.0.2` but it means that extend-mode has a different behavior if you're migrating from `5.0.0` or `5.0.1`. In short, `4.x` extended a base config with the custom config, whereas `5.0.0-1` extended the base with a richer config object that could conflict with the custom config in different ways from `4.x`.
We've also deprecated "extend mode" because it doesn't add a lot of value over "full control mode", but adds more code paths, documentation, user confusion etc. Starting in SB6.0 we will only support "full control mode" customization.
To migrate from extend-mode to full-control mode, if your extend-mode webpack config looks like this:
```js
module.exports = {
module: {
rules: [
/* ... */
],
},
};
```
In full control mode, you need modify the default config to have the rules of your liking:
```js
module.exports = ({ config }) => ({
...config,
module: {
...config.module,
rules: [
/* your own rules "..." here and/or some subset of config.module.rules */
],
},
});
```
Please refer to the [current custom webpack documentation](https://github.com/storybooks/storybook/blob/next/docs/src/pages/configurations/custom-webpack-config/index.md) for more information on custom webpack config and to [Issue #6081](https://github.com/storybooks/storybook/issues/6081) for more information about the change.
## From version 4.1.x to 5.0.x
Storybook 5.0 includes sweeping UI changes as well as changes to the addon API and custom webpack configuration. We've tried to keep backwards compatibility in most cases, but there are some notable exceptions documented below.
## Webpack config simplification
The API for custom webpack configuration has been simplifed in 5.0, but it's a breaking change. Storybook's "full control mode" for webpack allows you to override the webpack config with a function that returns a configuration object.
In Storybook 5 there is a single signature for full-control mode that takes a parameters object with the fields `config` and `mode`:
```js
module.exports = ({ config, mode }) => { config.module.rules.push(...); return config; }
```
In contrast, the 4.x configuration function accepted either two or three arguments (`(baseConfig, mode)`, or `(baseConfig, mode, defaultConfig)`). The `config` object in the 5.x signature is equivalent to 4.x's `defaultConfig`.
Please see the [current custom webpack documentation](https://github.com/storybooks/storybook/blob/next/docs/src/pages/configurations/custom-webpack-config/index.md) for more information on custom webpack config.
## Theming overhaul
Theming has been rewritten in v5. If you used theming in v4, please consult the [theming docs](https://github.com/storybooks/storybook/blob/next/docs/src/pages/configurations/theming/index.md) to learn about the new API.
## Story hierarchy defaults
Storybook's UI contains a hierarchical tree of stories that can be configured by `hierarchySeparator` and `hierarchyRootSeparator` [options](./addons/options/README.md).
In Storybook 4.x the values defaulted to `null` for both of these options, so that there would be no hierarchy by default.
In 5.0, we now provide recommended defaults:
```js
{
hierarchyRootSeparator: '|',
hierarchySeparator: /\/|\./,
}
```
This means if you use the characters { `|`, `/`, `.` } in your story kinds it will triggger the story hierarchy to appear. For example `storiesOf('UI|Widgets/Basics/Button')` will create a story root called `UI` containing a `Widgets/Basics` group, containing a `Button` component.
If you wish to opt-out of this new behavior and restore the flat UI, simply set them back to `null` in your storybook config, or remove { `|`, `/`, `.` } from your story kinds:
```js
addParameters({
options: {
hierarchyRootSeparator: null,
hierarchySeparator: null,
},
});
```
## Options addon deprecated
In 4.x we added story parameters. In 5.x we've deprecated the options addon in favor of [global parameters](./docs/src/pages/configurations/options-parameter/index.md), and we've also renamed some of the options in the process (though we're maintaining backwards compatibility until 6.0).
Here's an old configuration:
```js
addDecorator(
withOptions({
name: 'Storybook',
url: 'https://storybook.js.org',
goFullScreen: false,
addonPanelInRight: true,
})
);
```
And here's its new counterpart:
```js
import { create } from '@storybook/theming';
addParameters({
options: {
theme: create({
base: 'light',
brandTitle: 'Storybook',
brandUrl: 'https://storybook.js.org',
// To control appearance:
// brandImage: 'http://url.of/some.svg',
}),
isFullscreen: false,
panelPosition: 'right',
isToolshown: true,
},
});
```
Here is the mapping from old options to new:
| Old | New |
| ----------------- | ---------------- |
| name | theme.brandTitle |
| url | theme.brandUrl |
| goFullScreen | isFullscreen |
| showStoriesPanel | showNav |
| showAddonPanel | showPanel |
| addonPanelInRight | panelPosition |
| showSearchBox | |
| | isToolshown |
Storybook v5 removes the search dialog box in favor of a quick search in the navigation view, so `showSearchBox` has been removed.
Storybook v5 introduce a new tool bar above the story view and you can show\hide it with the new `isToolshown` option.
## Individual story decorators
The behavior of adding decorators to a kind has changed in SB5 ([#5781](https://github.com/storybooks/storybook/issues/5781)).
In SB4 it was possible to add decorators to only a subset of the stories of a kind.
```js
storiesOf('Stories', module)
.add('noncentered', () => 'Hello')
.addDecorator(centered)
.add('centered', () => 'Hello');
```
The semantics has changed in SB5 so that calling `addDecorator` on a kind adds a decorator to all its stories, no matter the order. So in the previous example, both stories would be centered.
To allow for a subset of the stories in a kind to be decorated, we've added the ability to add decorators to individual stories using parameters:
```js
storiesOf('Stories', module)
.add('noncentered', () => 'Hello')
.add('centered', () => 'Hello', { decorators: [centered] });
```
## Addon backgrounds uses parameters
Similarly, `@storybook/addon-backgrounds` uses parameters to pass background options. If you previously had:
```js
import { withBackgrounds } from `@storybook/addon-backgrounds`;
storiesOf('Stories', module)
.addDecorator(withBackgrounds(options));
```
You should replace it with:
```js
storiesOf('Stories', module).addParameters({ backgrounds: options });
```
You can pass `backgrounds` parameters at the global level (via `addParameters` imported from `@storybook/react` et al.), and the story level (via the third argument to `.add()`).
## Addon cssresources name attribute renamed
In the options object for `@storybook/addon-cssresources`, the `name` attribute for each resource has been renamed to `id`. If you previously had:
```js
import { withCssResources } from '@storybook/addon-cssresources';
import { addDecorator } from '@storybook/react';
addDecorator(
withCssResources({
cssresources: [
{
name: `bluetheme`, // Previous
code: `<style>body { background-color: lightblue; }</style>`,
picked: false,
},
],
})
);
```
You should replace it with:
```js
import { withCssResources } from '@storybook/addon-cssresources';
import { addDecorator } from '@storybook/react';
addDecorator(
withCssResources({
cssresources: [
{
id: `bluetheme`, // Renamed
code: `<style>body { background-color: lightblue; }</style>`,
picked: false,
},
],
})
);
```
## Addon viewport uses parameters
Similarly, `@storybook/addon-viewport` uses parameters to pass viewport options. If you previously had:
```js
import { configureViewport } from `@storybook/addon-viewport`;
configureViewport(options);
```
You should replace it with:
```js
import { addParameters } from '@storybook/react'; // or others
addParameters({ viewport: options });
```
The `withViewport` decorator is also no longer supported and should be replaced with a parameter based API as above. Also the `onViewportChange` callback is no longer supported.
See the [viewport addon README](https://github.com/storybooks/storybook/blob/master/addons/viewport/README.md) for more information.
## Addon a11y uses parameters, decorator renamed
Similarly, `@storybook/addon-a11y` uses parameters to pass a11y options. If you previously had:
```js
import { configureA11y } from `@storybook/addon-a11y`;
configureA11y(options);
```
You should replace it with:
```js
import { addParameters } from '@storybook/react'; // or others
addParameters({ a11y: options });
```
You can also pass `a11y` parameters at the component level (via `storiesOf(...).addParameters`), and the story level (via the third argument to `.add()`).
Furthermore, the decorator `checkA11y` has been deprecated and renamed to `withA11y` to make it consistent with other Storybook decorators.
See the [a11y addon README](https://github.com/storybooks/storybook/blob/master/addons/a11y/README.md) for more information.
## 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:
| Shorctut | Old | New |
| ---------------------- | ----------- | ----- |
| Toggle sidebar | cmd-shift-X | S |
| Toggle addons panel | cmd-shift-Z | A |
| Toggle addons position | cmd-shift-G | D |
| Toggle fullscreen | cmd-shift-F | F |
| Next story | cmd-shift-→ | alt-→ |
| Prev story | cmd-shift-← | alt-← |
| Next component | | alt-↓ |
| Prev component | | alt-↑ |
| Search | | / |
## New URL structure
We've update Storybook's URL structure in 5.0. The old structure used URL parameters to save the UI state, resulting in long ugly URLs. v5 respects the old URL parameters, but largely does away with them.
The old structure encoded `selectedKind` and `selectedStory` among other parameters. Storybook v5 respects these parameters but will issue a deprecation message in the browser console warning of potential future removal.
The new URL structure looks like:
```
https://url-of-storybook?path=/story/<storyId>
```
The structure of `storyId` is a slugified `<selectedKind>--<selectedStory>` (slugified = lowercase, hyphen-separated). Each `storyId` must be unique. We plan to build more features into Storybook in upcoming versions based on this new structure.
## Rename of the `--secure` cli parameter to `--https`
Storybook for React Native's start commands & the Web versions' start command were a bit different, for no reason.
We've changed the start command for Reactnative to match the other.
This means that when you previously used the `--secure` flag like so:
```sh
start-storybook --secure
# or
start-storybook --s
```
You have to replace it with:
```sh
start-storybook --https
```
## Vue integration
The Vue integration was updated, so that every story returned from a story or decorator function is now being normalized with `Vue.extend` **and** is being wrapped by a functional component. Returning a string from a story or decorator function is still supported and is treated as a component with the returned string as the template.
Currently there is no recommended way of accessing the component options of a story inside a decorator.
## From version 4.0.x to 4.1.x
There are are a few migrations you should be aware of in 4.1, including one unintentionally breaking change for advanced addon usage.
@ -66,17 +431,19 @@ However, if you're developing React components, this means you need to upgrade t
Also, here's the error you'll get if you're running an older version of React:
```
core.browser.esm.js:15 Uncaught TypeError: Object(...) is not a function
at Module../node_modules/@emotion/core/dist/core.browser.esm.js (core.browser.esm.js:15)
at __webpack_require__ (bootstrap:724)
at fn (bootstrap:101)
at Module../node_modules/@emotion/styled-base/dist/styled-base.browser.esm.js (styled-base.browser.esm.js:1)
at __webpack_require__ (bootstrap:724)
at fn (bootstrap:101)
at Module../node_modules/@emotion/styled/dist/styled.esm.js (styled.esm.js:1)
at __webpack_require__ (bootstrap:724)
at fn (bootstrap:101)
at Object../node_modules/@storybook/components/dist/navigation/MenuLink.js (MenuLink.js:12)
at Module../node_modules/@emotion/core/dist/core.browser.esm.js (core.browser.esm.js:15)
at **webpack_require** (bootstrap:724)
at fn (bootstrap:101)
at Module../node_modules/@emotion/styled-base/dist/styled-base.browser.esm.js (styled-base.browser.esm.js:1)
at **webpack_require** (bootstrap:724)
at fn (bootstrap:101)
at Module../node_modules/@emotion/styled/dist/styled.esm.js (styled.esm.js:1)
at **webpack_require** (bootstrap:724)
at fn (bootstrap:101)
at Object../node_modules/@storybook/components/dist/navigation/MenuLink.js (MenuLink.js:12)
```
### Generic addons

View File

@ -85,18 +85,18 @@ For additional help, join us [in our Discord](https://discord.gg/sMFvFsG) or [Sl
| Framework | Demo | |
| -------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [React](app/react) | [v4.0.0](https://storybooks-official.netlify.com) | [![React](https://img.shields.io/npm/dm/@storybook/react.svg)](app/react) |
| [React](app/react) | [v5.0.0](https://storybooks-official.netlify.com) | [![React](https://img.shields.io/npm/dm/@storybook/react.svg)](app/react) |
| [React Native](app/react-native) | - | [![React Native](https://img.shields.io/npm/dm/@storybook/react-native.svg)](app/react-native) |
| [Vue](app/vue) | [v4.0.0](https://storybooks-vue.netlify.com/) | [![Vue](https://img.shields.io/npm/dm/@storybook/vue.svg)](app/vue) |
| [Angular](app/angular) | [v4.0.0](https://storybooks-angular.netlify.com/) | [![Angular](https://img.shields.io/npm/dm/@storybook/angular.svg)](app/angular) |
| [Polymer](app/polymer) | [v4.0.0](https://storybooks-polymer.netlify.com/) | [![Polymer](https://img.shields.io/npm/dm/@storybook/polymer.svg)](app/polymer) |
| [Mithril](app/mithril) | [v4.0.0](https://storybooks-mithril.netlify.com/) | [![Mithril](https://img.shields.io/npm/dm/@storybook/mithril.svg)](app/mithril) |
| [Marko](app/marko) | [v4.0.0](https://storybooks-marko.netlify.com/) | [![Marko](https://img.shields.io/npm/dm/@storybook/marko.svg)](app/marko) |
| [HTML](app/html) | [v4.0.0](https://storybooks-html.netlify.com/) | [![HTML](https://img.shields.io/npm/dm/@storybook/html.svg)](app/html) |
| [Svelte](app/svelte) | [v4.0.0](https://storybooks-svelte.netlify.com/) | [![Svelte](https://img.shields.io/npm/dm/@storybook/svelte.svg)](app/svelte) |
| [Riot](app/riot) | [v4.0.0](https://storybooks-riot.netlify.com/) | [![Riot](https://img.shields.io/npm/dm/@storybook/riot.svg)](app/riot) |
| [Ember](app/ember) | [v4.0.0](https://storybooks-ember.netlify.com/) | [![Ember](https://img.shields.io/npm/dm/@storybook/ember.svg)](app/ember) |
| [Preact](app/preact) | [v4.0.0](https://storybooks-preact.netlify.com/) | [![Preact](https://img.shields.io/npm/dm/@storybook/preact.svg)](app/preact) |
| [Vue](app/vue) | [v5.0.0](https://storybooks-vue.netlify.com/) | [![Vue](https://img.shields.io/npm/dm/@storybook/vue.svg)](app/vue) |
| [Angular](app/angular) | [v5.0.0](https://storybooks-angular.netlify.com/) | [![Angular](https://img.shields.io/npm/dm/@storybook/angular.svg)](app/angular) |
| [Polymer](app/polymer) | [v5.0.0](https://storybooks-polymer.netlify.com/) | [![Polymer](https://img.shields.io/npm/dm/@storybook/polymer.svg)](app/polymer) |
| [Mithril](app/mithril) | [v5.0.0](https://storybooks-mithril.netlify.com/) | [![Mithril](https://img.shields.io/npm/dm/@storybook/mithril.svg)](app/mithril) |
| [Marko](app/marko) | [v5.0.0](https://storybooks-marko.netlify.com/) | [![Marko](https://img.shields.io/npm/dm/@storybook/marko.svg)](app/marko) |
| [HTML](app/html) | [v5.0.0](https://storybooks-html.netlify.com/) | [![HTML](https://img.shields.io/npm/dm/@storybook/html.svg)](app/html) |
| [Svelte](app/svelte) | [v5.0.0](https://storybooks-svelte.netlify.com/) | [![Svelte](https://img.shields.io/npm/dm/@storybook/svelte.svg)](app/svelte) |
| [Riot](app/riot) | [v5.0.0](https://storybooks-riot.netlify.com/) | [![Riot](https://img.shields.io/npm/dm/@storybook/riot.svg)](app/riot) |
| [Ember](app/ember) | [v5.0.0](https://storybooks-ember.netlify.com/) | [![Ember](https://img.shields.io/npm/dm/@storybook/ember.svg)](app/ember) |
| [Preact](app/preact) | [v5.0.0](https://storybooks-preact.netlify.com/) | [![Preact](https://img.shields.io/npm/dm/@storybook/preact.svg)](app/preact) |
### Sub Projects
@ -111,6 +111,7 @@ For additional help, join us [in our Discord](https://discord.gg/sMFvFsG) or [Sl
| [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 |
| [events](addons/events/) | Interactively fire events to components that respond to EventEmitter |
| [graphql](addons/graphql/) | Query a GraphQL server within Storybook stories |
@ -131,10 +132,10 @@ See [Addon / Framework Support Table](ADDONS_SUPPORT.md)
We have a badge! Link it to your live Storybook example.
![Storybook](https://github.com/storybooks/press/blob/master/badges/storybook.svg)
![Storybook](https://cdn.jsdelivr.net/gh/storybooks/brand@master/badge/badge-storybook.svg)
```md
[![Storybook](https://github.com/storybooks/press/blob/master/badges/storybook.svg)](link to site)
[![Storybook](https://cdn.jsdelivr.net/gh/storybooks/brand@master/badge/badge-storybook.svg)](link to site)
```
If you're looking for material to use in your presentation about storybook, like logo's video material and the colors we use etc, you can find all of that at our [press repo](https://github.com/storybooks/press).
@ -153,15 +154,15 @@ We welcome contributions to Storybook!
- 📥 Pull requests and 🌟 Stars are always welcome.
- Read our [contributing guide](CONTRIBUTING.md) to get started.
or find us on [Discord](https://discord.gg/sMFvFsG), we're will take the time to guide you
or find us on [Discord](https://discord.gg/sMFvFsG), we're will take the time to guide you
Looking for a first issue to tackle?
Looking for a first issue to tackle?
- We tag issues with [![Good First Issue](https://img.shields.io/github/issues/storybooks/storybook/good%20first%20issue.svg)](https://github.com/storybooks/storybook/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) when we think they are well suited for people who are new to the codebase or OSS in general.
- [Talk to us](https://discord.gg/sMFvFsG), we'll find something to suits your skills and learning interest.
### Development scripts
Storybook is organized as a monorepo using [Lerna](https://lernajs.io). Useful scripts include:
#### `yarn bootstrap`
> Installs package dependencies and links packages together - using lerna
@ -195,7 +196,6 @@ Become a sponsor and get your logo on our README on Github with a link to your s
<a href="https://opencollective.com/storybook/sponsor/2/website" target="_blank"><img src="https://opencollective.com/storybook/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/storybook/sponsor/3/website" target="_blank"><img src="https://opencollective.com/storybook/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/storybook/sponsor/4/website" target="_blank"><img src="https://opencollective.com/storybook/sponsor/4/avatar.svg"></a>
<a href="https://applitools.com/" target="_blank"><img src="https://file-xvimrfykua.now.sh/"></a>
<a href="https://opencollective.com/storybook/sponsor/5/website" target="_blank"><img src="https://opencollective.com/storybook/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/storybook/sponsor/6/website" target="_blank"><img src="https://opencollective.com/storybook/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/storybook/sponsor/7/website" target="_blank"><img src="https://opencollective.com/storybook/sponsor/7/avatar.svg"></a>
@ -261,4 +261,4 @@ Support us with a monthly donation and help us continue our activities. \[[Becom
[MIT](https://github.com/storybooks/storybook/blob/master/LICENSE)
-the end-
-the end-

View File

@ -4,7 +4,7 @@ This storybook addon can be helpful to make your UI components more accessible.
[Framework Support](https://github.com/storybooks/storybook/blob/master/ADDONS_SUPPORT.md)
![](docs/screenshot.png)
![Screenshot](https://raw.githubusercontent.com/storybooks/storybook/HEAD/addons/a11y/docs/screenshot.png)
## Getting started
@ -20,16 +20,16 @@ Add this line to your `addons.js` file (create this file inside your storybook c
import '@storybook/addon-a11y/register';
```
import the `withA11Y` decorator to check your stories for violations within your components.
import the `withA11y` decorator to check your stories for violations within your components.
```js
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withA11Y } from '@storybook/addon-a11y';
import { storiesOf, addDecorator } from '@storybook/react';
import { withA11y } from '@storybook/addon-a11y';
// should only be added once
// best place is in config.js
addDecorator(withA11Y)
addDecorator(withA11y)
storiesOf('button', module)
.add('Accessible', () => (
@ -51,17 +51,18 @@ You can override these options at story level too.
import React from 'react';
import { storiesOf, addDecorator, addParameters } from '@storybook/react';
import { withA11Y } from '@storybook/addon-a11y';
import { withA11y } from '@storybook/addon-a11y';
addDecorator(withA11Y)
addDecorator(withA11y)
addParameters({
a11y: {
// ... axe options
element: '#root', // optional selector which element to inspect
config: {}, // axe-core configurationOptions (https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#parameters-1)
options: {} // axe-core optionsParameter (https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#options-parameter)
},
});
storiesOf('button', module)
.add('Accessible', () => (
<button style={{ backgroundColor: 'black', color: 'white', }}>

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-a11y",
"version": "5.0.0-alpha.7",
"version": "5.1.0-rc.0",
"description": "a11y addon for storybook",
"keywords": [
"a11y",
@ -16,30 +16,38 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/storybooks/storybook.git"
"url": "git+https://github.com/storybooks/storybook.git",
"directory": "addons/a11y"
},
"license": "MIT",
"main": "dist/index.js",
"jsnext:main": "src/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.0.0-alpha.7",
"@storybook/client-logger": "5.0.0-alpha.7",
"@storybook/components": "5.0.0-alpha.7",
"@storybook/core-events": "5.0.0-alpha.7",
"@storybook/theming": "5.0.0-alpha.7",
"axe-core": "^3.1.2",
"@storybook/addons": "5.1.0-rc.0",
"@storybook/api": "5.1.0-rc.0",
"@storybook/client-logger": "5.1.0-rc.0",
"@storybook/components": "5.1.0-rc.0",
"@storybook/core-events": "5.1.0-rc.0",
"@storybook/theming": "5.1.0-rc.0",
"axe-core": "^3.2.2",
"common-tags": "^1.8.0",
"core-js": "^2.6.2",
"core-js": "^3.0.1",
"global": "^4.3.2",
"hoist-non-react-statics": "^3.3.0",
"memoizerific": "^1.11.3",
"prop-types": "^15.6.2",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react": "^16.8.4",
"react-redux": "^7.0.2",
"react-sizeme": "^2.5.2",
"redux": "^4.0.1",
"util-deprecate": "^1.0.2"
},
"devDependencies": {
"@types/common-tags": "^1.8.0",
"@types/react-redux": "^7.0.6"
},
"publishConfig": {
"access": "public"
}

View File

@ -0,0 +1,222 @@
import React from 'react';
import { mount } from 'enzyme';
import { ThemeProvider, themes, convert } from '@storybook/theming';
import { STORY_RENDERED } from '@storybook/core-events';
import { ScrollArea } from '@storybook/components';
import { A11YPanel } from './A11YPanel';
import { EVENTS } from '../constants';
function createApi() {
return {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
};
}
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 STORY_RENDERED and RESULT updater on mount', () => {
// given
const api = createApi();
expect(api.on).not.toHaveBeenCalled();
// when
mount(<ThemedA11YPanel api={api} />);
// then
expect(api.on.mock.calls.length).toBe(2);
expect(api.on.mock.calls[0][0]).toBe(STORY_RENDERED);
expect(api.on.mock.calls[1][0]).toBe(EVENTS.RESULT);
});
it('should request a run on tab activation', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} />);
expect(api.emit).not.toHaveBeenCalled();
// when
wrapper.setProps({ active: true });
wrapper.update();
// then
expect(api.emit).toHaveBeenCalledWith(EVENTS.REQUEST);
expect(wrapper.find(ScrollArea).length).toBe(0);
});
it('should deregister STORY_RENDERED and RESULT updater on unmount', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} />);
expect(api.off).not.toHaveBeenCalled();
// when
wrapper.unmount();
// then
expect(api.off.mock.calls.length).toBe(2);
expect(api.off.mock.calls[0][0]).toBe(STORY_RENDERED);
expect(api.off.mock.calls[1][0]).toBe(EVENTS.RESULT);
});
it('should update run result', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} active />);
const onUpdate = api.on.mock.calls.find(([event]) => event === EVENTS.RESULT)[1];
expect(
wrapper
.find('button')
.last()
.text()
.trim()
).toBe('Rerun tests');
// when
onUpdate(axeResult);
// then
expect(
wrapper
.find('button')
.last()
.text()
.trim()
).toBe('Tests completed');
});
it('should request run', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} active />);
const request = api.on.mock.calls.find(([event]) => event === STORY_RENDERED)[1];
expect(
wrapper
.find('button')
.last()
.text()
.trim()
).toBe('Rerun tests');
expect(api.emit).not.toHaveBeenCalled();
// when
request();
// then
expect(
wrapper
.find('button')
.last()
.text()
.trim()
).toBe('Running test');
expect(api.emit).toHaveBeenCalledWith(EVENTS.REQUEST);
});
it('should NOT request run on inactive tab', () => {
// given
const api = createApi();
mount(<ThemedA11YPanel api={api} active={false} />);
const request = api.on.mock.calls.find(([event]) => event === STORY_RENDERED)[1];
expect(api.emit).not.toHaveBeenCalled();
// when
request();
// then
expect(api.emit).not.toHaveBeenCalled();
});
it('should render report', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} active />);
const onUpdate = api.on.mock.calls.find(([event]) => event === EVENTS.RESULT)[1];
// when
onUpdate(axeResult);
// then
expect(wrapper.find(A11YPanel)).toMatchSnapshot();
});
it("should render loader when it's running", () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} active />);
const request = api.on.mock.calls.find(([event]) => event === STORY_RENDERED)[1];
// when
request();
wrapper.update();
// then
expect(wrapper.find('ScrollArea').length).toBe(0);
expect(wrapper.find('Loader').length).toBe(1);
expect(wrapper.find('ActionBar').length).toBe(1);
expect(wrapper.find('Loader')).toMatchSnapshot();
});
it('should NOT anything when tab is not active', () => {
// given
const api = createApi();
// when
const wrapper = mount(<ThemedA11YPanel api={api} active={false} />);
// then
expect(wrapper.find('ScrollArea').length).toBe(0);
expect(wrapper.find('ActionBar').length).toBe(0);
});
});

View File

@ -0,0 +1,223 @@
import React, { Component, Fragment } from 'react';
import { styled } from '@storybook/theming';
import { STORY_RENDERED } from '@storybook/core-events';
import { ActionBar, Icons, ScrollArea } from '@storybook/components';
import { AxeResults, Result } from 'axe-core';
import { API } from '@storybook/api';
import { Provider } from 'react-redux';
import { Report } from './Report';
import { Tabs } from './Tabs';
import { EVENTS } from '../constants';
import store, { clearElements } from '../redux-config';
export enum RuleType {
VIOLATION,
PASS,
INCOMPLETION,
}
const Icon = styled(Icons)(
{
height: '12px',
width: '12px',
marginRight: '4px',
},
({ status, theme }: any) =>
status === 'running'
? {
animation: `${theme.animation.rotate360} 1s linear infinite;`,
}
: {}
);
const Passes = styled.span(({ theme }) => ({
color: theme.color.positive,
}));
const Violations = styled.span(({ theme }) => ({
color: theme.color.negative,
}));
const Incomplete = styled.span(({ theme }) => ({
color: theme.color.warning,
}));
const Loader = styled(({ className }) => (
<div className={className}>
<Icon inline icon="sync" status="running" /> Please wait while the accessibility scan is running
...
</div>
))({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
});
Loader.displayName = 'Loader';
interface A11YPanelState {
status: string;
passes: Result[];
violations: Result[];
incomplete: Result[];
}
interface A11YPanelProps {
active: boolean;
api: API;
}
export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
state: A11YPanelState = {
status: 'ready',
passes: [],
violations: [],
incomplete: [],
};
componentDidMount() {
const { api } = this.props;
api.on(STORY_RENDERED, this.request);
api.on(EVENTS.RESULT, this.onUpdate);
}
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());
this.request();
}
}
componentWillUnmount() {
const { api } = this.props;
api.off(STORY_RENDERED, this.request);
api.off(EVENTS.RESULT, this.onUpdate);
}
onUpdate = ({ passes, violations, incomplete }: AxeResults) => {
this.setState(
{
status: 'ran',
passes,
violations,
incomplete,
},
() => {
setTimeout(() => {
const { status } = this.state;
if (status === 'ran') {
this.setState({
status: 'ready',
});
}
}, 900);
}
);
};
request = () => {
const { api, active } = this.props;
if (active) {
this.setState(
{
status: 'running',
},
() => {
api.emit(EVENTS.REQUEST);
// removes all elements from the redux map in store from the previous panel
store.dispatch(clearElements());
}
);
}
};
render() {
const { passes, violations, incomplete, status } = this.state;
const { active } = this.props;
let actionTitle;
if (status === 'ready') {
actionTitle = 'Rerun tests';
} else if (status === 'running') {
actionTitle = (
<Fragment>
<Icon inline icon="sync" status={status} /> Running test
</Fragment>
);
} else if (status === 'ran') {
actionTitle = (
<Fragment>
<Icon inline icon="check" /> Tests completed
</Fragment>
);
}
return active ? (
<Fragment>
<Provider store={store}>
{status === 'running' ? (
<Loader />
) : (
<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>
</Fragment>
) : null;
}
}

View File

@ -1,163 +0,0 @@
import { document } from 'global';
import React, { Component, Fragment } from 'react';
import memoize from 'memoizerific';
import { styled } from '@storybook/theming';
import { logger } from '@storybook/client-logger';
import { Popout, Item, Icons, Icon, IconButton, Title, List } from '@storybook/components';
const getIframe = memoize(1)(() => document.getElementById('storybook-preview-iframe'));
const ColorIcon = styled.span(
{
background: 'linear-gradient(to right, #F44336, #FF9800, #FFEB3B, #8BC34A, #2196F3, #9C27B0)',
},
({ filter }) => ({
filter: filter === 'mono' ? 'grayscale(100%)' : `url('#${filter}')`,
})
);
const Hidden = styled.div(() => ({
display: 'none',
}));
class ColorBlindness extends Component {
state = {
filter: false,
};
setFilter = filter => {
const iframe = getIframe();
if (iframe) {
iframe.style.filter = filter === 'mono' ? 'grayscale(100%)' : `url('#${filter}')`;
this.setState({
filter,
});
} else {
logger.error('Cannot find Storybook iframe');
}
};
render() {
const { filter } = this.state;
return (
<Fragment>
<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>
<Popout key="filters">
<IconButton key="filter" active={!!filter} title="Color Blindness Emulation">
<Icons icon="mirror" />
</IconButton>
{({ hide }) => (
<List>
{[
'protanopia',
'protanomaly',
'deuteranopia',
'deuteranomaly',
'tritanopia',
'tritanomaly',
'achromatopsia',
'achromatomaly',
].map(i => (
<Item
key={i}
onClick={() => {
this.setFilter(filter === i ? null : i);
hide();
}}
>
<Icon type={<ColorIcon filter={i} />} />
<Title>{i}</Title>
</Item>
))}
<Item
onClick={() => {
this.setFilter(filter === 'mono' ? null : 'mono');
hide();
}}
>
<Icon type={<ColorIcon filter="mono" />} />
<Title>mono</Title>
</Item>
<Item
onClick={() => {
this.setFilter(null);
hide();
}}
>
<Icon type={<ColorIcon />} />
<Title>Off</Title>
</Item>
</List>
)}
</Popout>
</Fragment>
);
}
}
export default ColorBlindness;

View File

@ -0,0 +1,126 @@
import { document } from 'global';
import React, { Component } from 'react';
import memoize from 'memoizerific';
import { styled } from '@storybook/theming';
import { logger } from '@storybook/client-logger';
import { Icons, IconButton, WithTooltip, TooltipLinkList } from '@storybook/components';
const getIframe = memoize(1)(() => document.getElementById('storybook-preview-iframe'));
const getFilter = (filter: string | null) => {
if (filter === null) {
return 'none';
}
if (filter === 'mono') {
return 'grayscale(100%)';
}
return `url('#${filter}')`;
};
const ColorIcon = styled.span(
{
background: 'linear-gradient(to right, #F44336, #FF9800, #FFEB3B, #8BC34A, #2196F3, #9C27B0)',
borderRadius: '1rem',
display: 'block',
height: '1rem',
width: '1rem',
},
({ filter }: { filter: string | null }) => ({
filter: getFilter(filter),
}),
({ theme }) => ({
boxShadow: `${theme.appBorderColor} 0 0 0 1px inset`,
})
);
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface ColorBlindnessProps {}
interface ColorBlindnessState {
expanded: boolean;
filter: string | null;
}
export class ColorBlindness extends Component<ColorBlindnessProps, ColorBlindnessState> {
state: ColorBlindnessState = {
expanded: false,
filter: null,
};
setFilter = (filter: string | null) => {
const iframe = getIframe();
if (iframe) {
iframe.style.filter = getFilter(filter);
this.setState({
expanded: false,
filter,
});
} else {
logger.error('Cannot find Storybook iframe');
}
};
onVisibilityChange = (s: boolean) => {
const { expanded } = this.state;
if (expanded !== s) {
this.setState({ expanded: s });
}
};
render() {
const { filter, expanded } = this.state;
let colorList = [
'protanopia',
'protanomaly',
'deuteranopia',
'deuteranomaly',
'tritanopia',
'tritanomaly',
'achromatopsia',
'achromatomaly',
'mono',
].map(i => ({
id: i,
title: i.charAt(0).toUpperCase() + i.slice(1),
onClick: () => {
this.setFilter(i);
},
right: <ColorIcon filter={i} />,
active: filter === i,
}));
if (filter !== null) {
colorList = [
{
id: 'reset',
title: 'Reset color filter',
onClick: () => {
this.setFilter(null);
},
right: undefined,
active: false,
},
...colorList,
];
}
return (
<WithTooltip
placement="top"
trigger="click"
tooltipShown={expanded}
onVisibilityChange={this.onVisibilityChange}
tooltip={<TooltipLinkList links={colorList} />}
closeOnClick
onDoubleClick={() => this.setFilter(null)}
>
<IconButton key="filter" active={!!filter} title="Color Blindness Emulation">
<Icons icon="mirror" />
</IconButton>
</WithTooltip>
);
}
}

View File

@ -1,135 +0,0 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import { STORY_RENDERED } from '@storybook/core-events';
import { ActionBar, ActionButton, Icons } from '@storybook/components';
import EVENTS from '../constants';
import Tabs from './Tabs';
import Report from './Report';
const Passes = styled.span(({ theme }) => ({
color: theme.successColor,
}));
const Violations = styled.span(({ theme }) => ({
color: theme.failColor,
}));
class A11YPanel extends Component {
static propTypes = {
active: PropTypes.bool.isRequired,
api: PropTypes.shape({
on: PropTypes.func,
emit: PropTypes.func,
off: PropTypes.func,
}).isRequired,
};
state = {
status: 'ready',
passes: [],
violations: [],
};
componentDidMount() {
const { api } = this.props;
api.on(STORY_RENDERED, this.request);
api.on(EVENTS.RESULT, this.onUpdate);
}
componentDidUpdate(prevProps) {
// TODO: might be able to remove this
const { active } = this.props;
if (!prevProps.active && active) {
this.request();
}
}
componentWillUnmount() {
const { api } = this.props;
api.off(STORY_RENDERED, this.request);
api.off(EVENTS.RESULT, this.onUpdate);
}
onUpdate = ({ passes, violations }) => {
this.setState(
{
status: 'ran',
passes,
violations,
},
() => {
setTimeout(() => {
const { status } = this.state;
if (status === 'ran') {
this.setState({
status: 'ready',
});
}
}, 900);
}
);
};
request = () => {
const { api, active } = this.props;
if (active) {
this.setState(
{
status: 'running',
},
() => {
api.emit(EVENTS.REQUEST);
}
);
}
};
render() {
const { passes, violations, status } = this.state;
const { active } = this.props;
return active ? (
<Fragment>
<Tabs
key="tabs"
tabs={[
{
label: <Violations>{violations.length} Violations</Violations>,
panel: <Report passes={false} items={violations} empty="No a11y violations found." />,
},
{
label: <Passes>{passes.length} Passes</Passes>,
panel: <Report passes items={passes} empty="No a11y check passed" />,
},
]}
/>
<ActionBar key="actionbar">
<ActionButton onClick={this.request}>
{status === 'ready' ? <span>RERUN TEST</span> : null}
{status === 'running' ? (
<Fragment>
<Icons inline icon="timer" /> <span>Running test</span>
</Fragment>
) : null}
{status === 'ran' ? (
<Fragment>
<Icons inline icon="check" /> <span>Tests completed</span>
</Fragment>
) : null}
</ActionButton>
</ActionBar>
</Fragment>
) : null;
}
}
export default A11YPanel;

View File

@ -1,60 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import Rules from './Rules';
const Item = styled.li({
fontWeight: 600,
});
const ItemTitle = styled.span({
borderBottom: '1px solid rgb(130, 130, 130)',
width: '100%',
display: 'inline-block',
paddingBottom: '4px',
marginBottom: '4px',
});
function Element({ element, passes }) {
const { any, all, none } = element;
const rules = [...any, ...all, ...none];
return (
<Item>
<ItemTitle>{element.target[0]}</ItemTitle>
<Rules rules={rules} passes={passes} />
</Item>
);
}
Element.propTypes = {
element: PropTypes.shape({
any: PropTypes.array.isRequired,
all: PropTypes.array.isRequired,
none: PropTypes.array.isRequired,
}).isRequired,
passes: PropTypes.bool.isRequired,
};
/* eslint-disable react/no-array-index-key */
const Elements = ({ elements, passes }) => (
<ol>
{elements.map((element, index) => (
<Element passes={passes} element={element} key={index} />
))}
</ol>
);
Elements.propTypes = {
elements: PropTypes.arrayOf(
PropTypes.shape({
any: PropTypes.array.isRequired,
all: PropTypes.array.isRequired,
none: PropTypes.array.isRequired,
})
).isRequired,
passes: PropTypes.bool.isRequired,
};
export default Elements;

View File

@ -0,0 +1,71 @@
import React, { FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import { NodeResult } from 'axe-core';
import { Rules } from './Rules';
import { RuleType } from '../A11YPanel';
import HighlightToggle from './HighlightToggle';
const Item = styled.li({
fontWeight: 600,
});
const ItemTitle = styled.span(({ theme }) => ({
borderBottom: `1px solid ${theme.appBorderColor}`,
width: '100%',
display: 'flex',
paddingBottom: '6px',
marginBottom: '6px',
justifyContent: 'space-between',
}));
const HighlightToggleElement = styled.span({
fontWeight: 'normal',
alignSelf: 'center',
paddingRight: '15px',
input: { margin: 0 },
});
interface ElementProps {
element: NodeResult;
type: RuleType;
}
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}
/>
</HighlightToggleElement>
</ItemTitle>
<Rules rules={rules} />
</Item>
);
};
interface ElementsProps {
elements: NodeResult[];
type: RuleType;
}
export const Elements: FunctionComponent<ElementsProps> = ({ elements, type }) => (
<ol>
{elements.map((element, index) => (
// eslint-disable-next-line react/no-array-index-key
<Element element={element} key={index} type={type} />
))}
</ol>
);

View File

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

View File

@ -0,0 +1,191 @@
import { document } from 'global';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { styled, themes, convert } from '@storybook/theming';
import memoize from 'memoizerific';
import { NodeResult } from 'axe-core';
import { RuleType } from '../A11YPanel';
import { addElement } from '../../redux-config';
import { IFRAME } from '../../constants';
export class HighlightedElementData {
originalOutline: string;
isHighlighted: boolean;
}
interface ToggleProps {
elementsToHighlight: NodeResult[];
type: RuleType;
addElement?: (data: any) => void;
highlightedElementsMap?: Map<HTMLElement, HighlightedElementData>;
isToggledOn?: boolean;
toggleId?: string;
indeterminate?: boolean;
}
enum CheckBoxStates {
CHECKED,
UNCHECKED,
INDETERMINATE,
}
const Checkbox = styled.input(({ 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;
}
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>
): CheckBoxStates {
const highlightedCount = elementsToHighlight.filter(item => {
const targetElement = getElementBySelectorPath(item.target[0]);
return (
highlightedElementsMap.has(targetElement) &&
highlightedElementsMap.get(targetElement).isHighlighted
);
}).length;
// eslint-disable-next-line no-nested-ternary
return highlightedCount === 0
? CheckBoxStates.UNCHECKED
: highlightedCount === elementsToHighlight.length
? CheckBoxStates.CHECKED
: CheckBoxStates.INDETERMINATE;
}
function mapDispatchToProps(dispatch: any) {
return {
addElement: (data: { element: HTMLElement; data: HighlightedElementData }) =>
dispatch(addElement(data)),
};
}
const mapStateToProps = (state: any, ownProps: any) => {
const checkBoxState = areAllRequiredElementsHighlighted(
ownProps.elementsToHighlight || [],
state.highlightedElementsMap
);
return {
highlightedElementsMap: state.highlightedElementsMap,
isToggledOn: checkBoxState === CheckBoxStates.CHECKED,
indeterminate: checkBoxState === CheckBoxStates.INDETERMINATE,
};
};
class HighlightToggle extends Component<ToggleProps> {
static defaultProps: Partial<ToggleProps> = {
elementsToHighlight: [],
};
private checkBoxRef = React.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(prevProps: Readonly<ToggleProps>): 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 } = highlightedElementsMap.get(targetElement);
const { isHighlighted } = highlightedElementsMap.get(targetElement);
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 = new HighlightedElementData();
data.isHighlighted = isHighlighted;
data.originalOutline = 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);

View File

@ -1,13 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import { Result } from 'axe-core';
const Wrapper = styled.div(({ theme }) => ({
backgroundColor: theme.barFill,
const Wrapper = styled.div({
padding: '12px',
marginBottom: '10px',
}));
});
const Help = styled.p({
margin: '0 0 12px',
});
@ -18,7 +17,11 @@ const Link = styled.a({
display: 'block',
});
function Info({ item }) {
interface InfoProps {
item: Result;
}
export const Info: FunctionComponent<InfoProps> = ({ item }) => {
return (
<Wrapper>
<Help>{item.help}</Help>
@ -27,13 +30,4 @@ function Info({ item }) {
</Link>
</Wrapper>
);
}
Info.propTypes = {
item: PropTypes.shape({
help: PropTypes.node,
helpUrl: PropTypes.string,
}).isRequired,
};
export default Info;

View File

@ -1,80 +0,0 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import { Icons } from '@storybook/components';
import Info from './Info';
import Tags from './Tags';
import Elements from './Elements';
const Wrapper = styled.div();
const HeaderBar = styled.button(({ theme }) => ({
padding: theme.layoutMargin,
paddingLeft: theme.layoutMargin - 3,
display: 'flex',
width: '100%',
border: 0,
background: 'none',
color: 'inherit',
borderLeft: '3px solid transparent',
'&:focus': {
outline: '0 none',
borderLeft: `3px solid ${theme.highlightColor}`,
},
}));
class Item extends Component {
static propTypes = {
item: PropTypes.shape({
description: PropTypes.string,
nodes: PropTypes.array,
tags: PropTypes.array,
}).isRequired,
passes: PropTypes.bool.isRequired,
};
state = {
open: false,
};
onToggle = () =>
this.setState(prevState => ({
open: !prevState.open,
}));
render() {
const { item, passes } = this.props;
const { open } = this.state;
return (
<Wrapper>
<HeaderBar onClick={this.onToggle}>
<Icons
icon="chevrondown"
size={10}
color="#9DA5AB"
style={{
marginRight: '10px',
transform: `rotate(${open ? 0 : -90}deg)`,
transition: 'transform 0.1s ease-in-out',
}}
/>
{item.description}
</HeaderBar>
{open ? (
<Fragment>
<Info item={item} key="info" />
<Elements elements={item.nodes} passes={passes} key="elements" />
<Tags tags={item.tags} key="tags" />
</Fragment>
) : null}
</Wrapper>
);
}
}
export default Item;

View File

@ -0,0 +1,113 @@
import React, { Component, Fragment } from 'react';
import { styled } from '@storybook/theming';
import { Icons } from '@storybook/components';
import { Result } from 'axe-core';
import { Info } from './Info';
import { Elements } from './Elements';
import { Tags } from './Tags';
import { RuleType } from '../A11YPanel';
import HighlightToggle from './HighlightToggle';
const Wrapper = styled.div(({ theme }) => ({
display: 'flex',
width: '100%',
borderBottom: `1px solid ${theme.appBorderColor}`,
'&:hover': {
background: theme.background.hoverable,
},
}));
const Icon = styled<any, any>(Icons)(({ theme }) => ({
height: 10,
width: 10,
minWidth: 10,
color: theme.color.mediumdark,
marginRight: '10px',
transition: 'transform 0.1s ease-in-out',
alignSelf: 'center',
display: 'inline-flex',
}));
const HeaderBar = styled.div(({ theme }) => ({
padding: theme.layoutMargin,
paddingLeft: theme.layoutMargin - 3,
background: 'none',
color: 'inherit',
textAlign: 'left',
cursor: 'pointer',
borderLeft: '3px solid transparent',
width: '100%',
'&:focus': {
outline: '0 none',
borderLeft: `3px solid ${theme.color.secondary}`,
},
}));
const HighlightToggleElement = styled.span({
fontWeight: 'normal',
float: 'right',
marginRight: '15px',
alignSelf: 'center',
input: { margin: 0 },
});
interface ItemProps {
item: Result;
type: RuleType;
}
interface ItemState {
open: boolean;
}
export class Item extends Component<ItemProps, ItemState> {
state = {
open: false,
};
onToggle = () =>
this.setState(prevState => ({
open: !prevState.open,
}));
render() {
const { item, type } = this.props;
const { open } = this.state;
const highlightToggleId = `${type}-${item.id}`;
return (
<Fragment>
<Wrapper>
<HeaderBar onClick={this.onToggle} role="button">
<Icon
icon="chevrondown"
size={10}
color="#9DA5AB"
style={{
transform: `rotate(${open ? 0 : -90}deg)`,
}}
/>
{item.description}
</HeaderBar>
<HighlightToggleElement>
<HighlightToggle
toggleId={highlightToggleId}
type={type}
elementsToHighlight={item ? item.nodes : null}
/>
</HighlightToggleElement>
</Wrapper>
{open ? (
<Fragment>
<Info item={item} key="info" />
<Elements elements={item.nodes} type={type} key="elements" />
<Tags tags={item.tags} key="tags" />
</Fragment>
) : null}
</Fragment>
);
}
}

View File

@ -1,80 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import { Icons } from '@storybook/components';
const impactColors = {
minor: '#f1c40f',
moderate: '#e67e22',
serious: '#e74c3c',
critical: '#c0392b',
success: '#2ecc71',
};
const List = styled.div({
display: 'flex',
flexDirection: 'column',
padding: '4px',
fontWeight: '400',
});
const Item = styled.div({
display: 'flex',
flexDirection: 'row',
marginBottom: '6px',
});
const Message = styled.div({
paddingLeft: '6px',
});
const Status = styled.div(({ passes, impact }) => ({
height: '16px',
width: '16px',
borderRadius: '8px',
fontSize: '10px',
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
flex: '0 0 16px',
color: passes ? impactColors.success : impactColors[impact],
}));
const Rule = ({ rule, passes }) => (
<Item>
<Status passes={passes || undefined} impact={rule.impact}>
{passes ? <Icons icon="check" /> : <Icons icon="cross" />}
</Status>
<Message>{rule.message}</Message>
</Item>
);
Rule.propTypes = {
rule: PropTypes.shape({
message: PropTypes.node,
}).isRequired,
passes: PropTypes.bool.isRequired,
};
/* eslint-disable react/no-array-index-key */
function Rules({ rules, passes }) {
return (
<List>
{rules.map((rule, index) => (
<Rule passes={passes} rule={rule} key={index} />
))}
</List>
);
}
Rules.propTypes = {
rules: PropTypes.arrayOf(
PropTypes.shape({
message: PropTypes.node,
})
).isRequired,
passes: PropTypes.bool.isRequired,
};
export default Rules;

View File

@ -0,0 +1,120 @@
import React, { FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import { Badge, Icons } from '@storybook/components';
import { CheckResult } from 'axe-core';
import { SizeMe } from 'react-sizeme';
import { RuleType } from '../A11YPanel';
const impactColors = {
minor: '#f1c40f',
moderate: '#e67e22',
serious: '#e74c3c',
critical: '#c0392b',
success: '#2ecc71',
};
const List = styled.div({
display: 'flex',
flexDirection: 'column',
paddingBottom: '4px',
paddingRight: '4px',
paddingTop: '4px',
fontWeight: '400',
} as any);
const Item = styled.div(({ elementWidth }: { elementWidth: number }) => {
const maxWidthBeforeBreak = 407;
return {
flexDirection: elementWidth > maxWidthBeforeBreak ? 'row' : 'inherit',
marginBottom: elementWidth > maxWidthBeforeBreak ? '6px' : '12px',
display: elementWidth > maxWidthBeforeBreak ? 'flex' : 'block',
};
});
const StyledBadge = styled(Badge)(({ status }: { status: string }) => ({
padding: '2px 8px',
marginBottom: '3px',
minWidth: '65px',
maxWidth: 'fit-content',
width: '100%',
textAlign: 'center',
}));
const Message = styled.div({
paddingLeft: '6px',
paddingRight: '23px',
});
const Status = styled.div(({ passes, impact }: { passes: boolean; impact: string }) => ({
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
color: passes ? impactColors.success : (impactColors as any)[impact],
'& > svg': {
height: '16px',
width: '16px',
},
}));
export enum ImpactValue {
MINOR = 'minor',
MODERATE = 'moderate',
SERIOUS = 'serious',
CRITICAL = 'critical',
}
interface RuleProps {
rule: CheckResult;
}
const formatSeverityText = (severity: string) => {
return severity
.charAt(0)
.toUpperCase()
.concat(severity.slice(1));
};
const Rule: FunctionComponent<RuleProps> = ({ rule }) => {
let badgeType: any = null;
switch (rule.impact) {
case ImpactValue.CRITICAL:
badgeType = 'critical';
break;
case ImpactValue.SERIOUS:
badgeType = 'negative';
break;
case ImpactValue.MODERATE:
badgeType = 'warning';
break;
case ImpactValue.MINOR:
badgeType = 'neutral';
break;
default:
break;
}
return (
<SizeMe refreshMode="debounce">
{({ size }: { size: any }) => (
<Item elementWidth={size.width}>
<StyledBadge status={badgeType}>{formatSeverityText(rule.impact)}</StyledBadge>
<Message>{rule.message}</Message>
</Item>
)}
</SizeMe>
);
};
interface RulesProps {
rules: CheckResult[];
}
export const Rules: FunctionComponent<RulesProps> = ({ rules }) => {
return (
<List>
{rules.map((rule, index) => (
// eslint-disable-next-line react/no-array-index-key
<Rule rule={rule} key={index} />
))}
</List>
);
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import { TagValue } from 'axe-core';
const Wrapper = styled.div({
display: 'flex',
@ -12,11 +12,15 @@ const Wrapper = styled.div({
const Item = styled.div(({ theme }) => ({
margin: '0 6px',
padding: '5px',
border: theme.mainBorder,
borderRadius: theme.mainBorderRadius,
border: `1px solid ${theme.appBorderColor}`,
borderRadius: theme.appBorderRadius,
}));
function Tags({ tags }) {
interface TagsProps {
tags: TagValue[];
}
export const Tags: FunctionComponent<TagsProps> = ({ tags }) => {
return (
<Wrapper>
{tags.map(tag => (
@ -24,9 +28,4 @@ function Tags({ tags }) {
))}
</Wrapper>
);
}
Tags.propTypes = {
tags: PropTypes.arrayOf(PropTypes.node).isRequired,
};
export default Tags;

View File

@ -0,0 +1,353 @@
// 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": "1023qba-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;",
},
"inlineGlow": Object {
"map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9hbmltYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBaUNzQiIsImZpbGUiOiIuLi9zcmMvYW5pbWF0aW9uLnRzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9jb3JlJztcblxuZXhwb3J0IGNvbnN0IGVhc2luZyA9IHtcbiAgcnViYmVyOiAnY3ViaWMtYmV6aWVyKDAuMTc1LCAwLjg4NSwgMC4zMzUsIDEuMDUpJyxcbn07XG5cbmNvbnN0IHJvdGF0ZTM2MCA9IGtleWZyYW1lc2Bcblx0ZnJvbSB7XG5cdFx0dHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG5cdH1cblx0dG8ge1xuXHRcdHRyYW5zZm9ybTogcm90YXRlKDM2MGRlZyk7XG5cdH1cbmA7XG5cbmNvbnN0IGdsb3cgPSBrZXlmcmFtZXNgXG4gIDAlLCAxMDAlIHsgb3BhY2l0eTogMTsgfVxuICA1MCUgeyBvcGFjaXR5OiAuNDsgfVxuYDtcblxuY29uc3QgZmxvYXQgPSBrZXlmcmFtZXNgXG4gIDAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDFweCk7IH1cbiAgMjUlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDBweCk7IH1cbiAgNTAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKC0zcHgpOyB9XG4gIDEwMCUgeyB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoMXB4KTsgfVxuYDtcblxuY29uc3QgamlnZ2xlID0ga2V5ZnJhbWVzYFxuICAwJSwgMTAwJSB7IHRyYW5zZm9ybTp0cmFuc2xhdGUzZCgwLDAsMCk7IH1cbiAgMTIuNSUsIDYyLjUlIHsgdHJhbnNmb3JtOnRyYW5zbGF0ZTNkKC00cHgsMCwwKTsgfVxuICAzNy41JSwgODcuNSUgeyAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCg0cHgsMCwwKTsgIH1cbmA7XG5cbmNvbnN0IGlubGluZUdsb3cgPSBjc3NgXG4gIGFuaW1hdGlvbjogJHtnbG93fSAxLjVzIGVhc2UtaW4tb3V0IGluZmluaXRlO1xuICBjb2xvcjogdHJhbnNwYXJlbnQ7XG4gIGN1cnNvcjogcHJvZ3Jlc3M7XG5gO1xuXG4vLyBob3ZlciAmIGFjdGl2ZSBzdGF0ZSBmb3IgbGlua3MgYW5kIGJ1dHRvbnNcbmNvbnN0IGhvdmVyYWJsZSA9IGNzc2BcbiAgdHJhbnNpdGlvbjogYWxsIDE1MG1zIGVhc2Utb3V0O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDAsIDAsIDApO1xuXG4gICY6aG92ZXIge1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlM2QoMCwgLTJweCwgMCk7XG4gIH1cblxuICAmOmFjdGl2ZSB7XG4gICAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCgwLCAwLCAwKTtcbiAgfVxuYDtcblxuZXhwb3J0IGNvbnN0IGFuaW1hdGlvbiA9IHtcbiAgcm90YXRlMzYwLFxuICBnbG93LFxuICBmbG9hdCxcbiAgamlnZ2xlLFxuICBpbmxpbmVHbG93LFxuICBob3ZlcmFibGUsXG59O1xuIl19 */",
"name": "1euta6d-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;",
},
"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,
},
},
}
}
>
<ConnectFunction>
<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>
</ConnectFunction>
</ThemeProvider>
</ThemedHighlightToggle>
</Provider>
`;

View File

@ -1,29 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Placeholder } from '@storybook/components';
import Item from './Item';
const Report = ({ items, empty, passes }) => (
<Fragment>
{items.length ? (
items.map(item => <Item passes={passes} item={item} key={item.id} />)
) : (
<Placeholder key="placeholder">{empty}</Placeholder>
)}
</Fragment>
);
Report.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
description: PropTypes.string,
nodes: PropTypes.array,
tags: PropTypes.array,
})
).isRequired,
empty: PropTypes.string.isRequired,
passes: PropTypes.bool.isRequired,
};
export default Report;

View File

@ -0,0 +1,21 @@
import React, { Fragment, FunctionComponent } from 'react';
import { Placeholder } from '@storybook/components';
import { Result } from 'axe-core';
import { Item } from './Item';
import { RuleType } from '../A11YPanel';
export interface ReportProps {
items: Result[];
empty: string;
type: RuleType;
}
export const Report: FunctionComponent<ReportProps> = ({ items, empty, type }) => (
<Fragment>
{items && items.length ? (
items.map(item => <Item item={item} key={`${type}:${item.id}`} type={type} />)
) : (
<Placeholder key="placeholder">{empty}</Placeholder>
)}
</Fragment>
);

View File

@ -1,91 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
const Container = styled.div({
width: '100%',
position: 'relative',
minHeight: '100%',
});
const List = styled.div(({ theme }) => ({
borderBottom: theme.mainBorder,
flexWrap: 'wrap',
display: 'flex',
}));
const Item = styled.button(
({ active }) =>
active
? {
opacity: 1,
fontWeight: 600,
}
: {},
({ theme }) => ({
textDecoration: 'none',
textTransform: 'uppercase',
padding: '10px 15px',
letterSpacing: '1px',
cursor: 'pointer',
fontWeight: 500,
opacity: 0.7,
border: 'none',
borderTop: '3px solid transparent',
borderBottom: '3px solid transparent',
background: 'none',
flex: 1,
'&:focus': {
outline: '0 none',
borderBottom: `3px solid ${theme.highlightColor}`,
},
})
);
class Tabs extends Component {
state = {
active: 0,
};
onToggle = index => {
this.setState({
active: index,
});
};
render() {
const { tabs } = this.props;
const { active } = this.state;
return (
<Container>
<List>
{tabs.map((tab, index) => (
<Item
// eslint-disable-next-line react/no-array-index-key
key={index}
active={active === index ? true : undefined}
onClick={() => this.onToggle(index)}
>
{tab.label}
</Item>
))}
</List>
{tabs[active].panel}
</Container>
);
}
}
Tabs.propTypes = {
tabs: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.node,
panel: PropTypes.node,
})
).isRequired,
};
export default Tabs;

View File

@ -0,0 +1,161 @@
import React, { Component, SyntheticEvent } from 'react';
import { styled, themes } 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';
// TODO: reuse the Tabs component from @storybook/theming instead of re-building identical functionality
const Container = styled.div({
width: '100%',
position: 'relative',
minHeight: '100%',
});
const HighlightToggleLabel = styled.label(({ theme }) => ({
cursor: 'pointer',
userSelect: 'none',
marginBottom: '3px',
marginRight: '3px',
color: theme.color.dark,
}));
const GlobalToggle = styled.div(({ elementWidth }: { elementWidth: number }) => {
const maxWidthBeforeBreak = 450;
return {
cursor: 'pointer',
fontSize: '14px',
padding: elementWidth > maxWidthBeforeBreak ? '12px 15px 10px 0' : '12px 0px 3px 12px',
height: '40px',
border: 'none',
marginTop: elementWidth > maxWidthBeforeBreak ? '-40px' : '0px',
float: elementWidth > maxWidthBeforeBreak ? 'right' : 'left',
display: elementWidth > maxWidthBeforeBreak ? 'flex' : 'block',
alignItems: 'center',
width: elementWidth > maxWidthBeforeBreak ? 'auto' : '100%',
borderBottom: elementWidth > maxWidthBeforeBreak ? 'none' : '1px solid rgba(0,0,0,.1)',
input: {
marginLeft: '10',
marginRight: '0',
marginTop: '0',
marginBottom: '0',
},
};
});
const Item = styled.button(
({ theme }) => ({
textDecoration: 'none',
padding: '10px 15px',
cursor: 'pointer',
fontWeight: theme.typography.weight.bold,
fontSize: theme.typography.size.s2 - 1,
lineHeight: 1,
height: 40,
border: 'none',
borderTop: '3px solid transparent',
borderBottom: '3px solid transparent',
background: 'transparent',
'&:focus': {
outline: '0 none',
borderBottom: `3px solid ${theme.color.secondary}`,
},
}),
({ active, theme }: any) =>
active
? {
opacity: 1,
borderBottom: `3px solid ${theme.color.secondary}`,
}
: {}
);
const TabsWrapper = styled.div({});
const List = styled.div(({ theme }) => ({
boxShadow: `${theme.appBorderColor} 0 -1px 0 0 inset`,
background: 'rgba(0, 0, 0, .05)',
display: 'flex',
justifyContent: 'space-between',
whiteSpace: 'nowrap',
}));
interface TabsProps {
tabs: {
label: JSX.Element;
panel: JSX.Element;
items: Result[];
type: RuleType;
}[];
}
interface TabsState {
active: number;
}
function retrieveAllNodesFromResults(items: Result[]): NodeResult[] {
return items.reduce((acc, item) => acc.concat(item.nodes), []);
}
export class Tabs extends Component<TabsProps, TabsState> {
state: TabsState = {
active: 0,
};
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());
};
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>
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,10 @@
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';
const RESULT = `${ADDON_ID}/result`;
const REQUEST = `${ADDON_ID}/request`;
export default { RESULT, REQUEST };
export const EVENTS = { RESULT, REQUEST };

View File

@ -1,78 +0,0 @@
import { document } from 'global';
import axe from 'axe-core';
import deprecate from 'util-deprecate';
import { stripIndents } from 'common-tags';
import addons, { makeDecorator } from '@storybook/addons';
import { STORY_RENDERED } from '@storybook/core-events';
import EVENTS, { PARAM_KEY } from './constants';
const channel = addons.getChannel();
let progress = Promise.resolve();
let options;
const getElement = () => {
const storyRoot = document.getElementById('story-root');
if (storyRoot) {
return storyRoot.children;
}
return document.getElementById('root');
};
const report = input => {
channel.emit(EVENTS.RESULT, input);
};
const run = o => {
progress = progress.then(() => {
axe.reset();
if (o) {
axe.configure(o);
}
return axe.run(getElement()).then(report);
});
};
export const withA11Y = makeDecorator({
name: 'withA11Y',
parameterName: PARAM_KEY,
skipIfNoParametersOrOptions: false,
allowDeprecatedUsage: false,
wrapper: (getStory, context, opt) => {
options = opt.parameters || opt.options;
return getStory(context);
},
});
channel.on(STORY_RENDERED, () => run(options));
channel.on(EVENTS.REQUEST, () => run(options));
if (module && module.hot && module.hot.decline) {
module.hot.decline();
}
// TODO: REMOVE at v6.0.0
export const checkA11y = deprecate(
(...args) => withA11Y(...args),
'checkA11y has been replaced with withA11Y'
);
// TODO: REMOVE at v6.0.0
export const configureA11y = deprecate(
config => {
options = config;
},
stripIndents`
configureA11y is deprecated, please configure addon-a11y using the addParameter api:
addParameters({
a11y: {
// ... axe options
element: '#root', // optional selector which element to inspect
},
});
`
);

93
addons/a11y/src/index.ts Normal file
View File

@ -0,0 +1,93 @@
/* eslint-disable @typescript-eslint/no-object-literal-type-assertion */
import { document } from 'global';
import axe, { AxeResults, ElementContext, RunOptions, Spec } from 'axe-core';
import deprecate from 'util-deprecate';
import { stripIndents } from 'common-tags';
import addons, { makeDecorator } from '@storybook/addons';
import { EVENTS, PARAM_KEY } from './constants';
let progress = Promise.resolve();
interface Setup {
element?: ElementContext;
config: Spec;
options: RunOptions;
}
let setup: Setup = { element: null, config: {}, options: {} };
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);
});
};
if (module && module.hot && module.hot.decline) {
module.hot.decline();
}
export const withA11y = makeDecorator({
name: 'withA11Y',
parameterName: PARAM_KEY,
wrapper: (getStory, context, { parameters }) => {
if (parameters) {
setup = parameters as Setup;
}
addons.getChannel().on(EVENTS.REQUEST, () => run(setup.element, setup.config, setup.options));
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;
},
stripIndents`
configureA11y is deprecated, please configure addon-a11y using the addParameter api:
addParameters({
a11y: {
// ... axe options
element: '#root', // optional selector which element to inspect
},
});
`
);

View File

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

View File

@ -1,21 +0,0 @@
import React from 'react';
import addons, { types } from '@storybook/addons';
import Panel from './components/Panel';
import ColorBlindness from './components/ColorBlindness';
import { ADDON_ID, PANEL_ID } from './constants';
addons.register(ADDON_ID, api => {
addons.add(PANEL_ID, {
type: types.TOOL,
render: () => <ColorBlindness />,
});
addons.add(PANEL_ID, {
type: types.PANEL,
title: 'Accessibility',
// eslint-disable-next-line react/prop-types
render: ({ active, key }) => <Panel key={key} api={api} active={active} />,
});
});

View File

@ -0,0 +1,104 @@
import React, { Fragment, FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import { addons, types } from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from './constants';
import { ColorBlindness } from './components/ColorBlindness';
import { A11YPanel } from './components/A11YPanel';
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.add(PANEL_ID, {
title: '',
type: types.TOOL,
match: ({ viewMode }) => viewMode === 'story',
render: () => <ColorBlindness />,
});
addons.add(PANEL_ID, {
title: 'Accessibility',
type: types.PANEL,
render: ({ active, key }) => <A11YPanel key={key} api={api} active={active} />,
});
addons.add(PANEL_ID, {
title: '',
type: types.PREVIEW,
render: PreviewWrapper as any,
});
});

2
addons/a11y/src/typings.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
declare module 'global';
declare module 'react-sizeme';

13
addons/a11y/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env"]
},
"include": [
"src/**/*"
],
"exclude": [
"src/__tests__/**/*"
]
}

View File

@ -4,7 +4,7 @@ Storybook Addon Actions can be used to display data received by event handlers i
[Framework Support](https://github.com/storybooks/storybook/blob/master/ADDONS_SUPPORT.md)
![Screenshot](docs/screenshot.png)
![Screenshot](https://raw.githubusercontent.com/storybooks/storybook/HEAD/addons/actions/docs/screenshot.png)
## Getting Started

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-actions",
"version": "5.0.0-alpha.7",
"version": "5.1.0-rc.0",
"description": "Action Logger addon for storybook",
"keywords": [
"storybook"
@ -11,29 +11,35 @@
},
"repository": {
"type": "git",
"url": "https://github.com/storybooks/storybook.git"
"url": "https://github.com/storybooks/storybook.git",
"directory": "addons/actions"
},
"license": "MIT",
"main": "dist/index.js",
"jsnext:main": "src/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.0.0-alpha.7",
"@storybook/components": "5.0.0-alpha.7",
"@storybook/core-events": "5.0.0-alpha.7",
"@storybook/theming": "5.0.0-alpha.7",
"core-js": "^2.6.2",
"@storybook/addons": "5.1.0-rc.0",
"@storybook/api": "5.1.0-rc.0",
"@storybook/components": "5.1.0-rc.0",
"@storybook/core-events": "5.1.0-rc.0",
"@storybook/theming": "5.1.0-rc.0",
"core-js": "^3.0.1",
"fast-deep-equal": "^2.0.1",
"global": "^4.3.2",
"lodash": "^4.17.11",
"make-error": "^1.3.5",
"prop-types": "^15.6.2",
"react": "^16.7.0",
"react-inspector": "^2.3.0",
"polished": "^3.3.1",
"prop-types": "^15.7.2",
"react": "^16.8.4",
"react-inspector": "^3.0.2",
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/lodash": "^4.14.129",
"@types/uuid": "^3.4.4"
},
"publishConfig": {
"access": "public"
}

View File

@ -1,48 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Inspector from 'react-inspector';
import { withTheme } from '@storybook/theming';
import { ActionBar, ActionButton } from '@storybook/components';
import { Actions, Action, Wrapper, InspectorContainer, Countwrap, Counter } from './style';
const ActionLogger = withTheme(({ actions, onClear, theme }) => (
<Wrapper>
<Actions>
{actions.map(action => (
<Action key={action.id}>
<Countwrap>{action.count > 1 && <Counter>{action.count}</Counter>}</Countwrap>
<InspectorContainer>
<Inspector
theme={theme.addonActionsTheme || 'chromeLight'}
sortObjectKeys
showNonenumerable={false}
name={action.data.name}
data={action.data.args || action.data}
/>
</InspectorContainer>
</Action>
))}
</Actions>
<ActionBar>
<ActionButton onClick={onClear}>CLEAR</ActionButton>
</ActionBar>
</Wrapper>
));
ActionLogger.propTypes = {
onClear: PropTypes.func.isRequired,
actions: PropTypes.arrayOf(
PropTypes.shape({
count: PropTypes.node,
data: PropTypes.shape({
name: PropTypes.node.isRequired,
args: PropTypes.any,
}),
})
).isRequired,
};
export default ActionLogger;

View File

@ -0,0 +1,56 @@
import React, { Fragment } from 'react';
import { styled, withTheme, Theme } from '@storybook/theming';
import Inspector from 'react-inspector';
import { ActionBar, ScrollArea } from '@storybook/components';
import { Action, InspectorContainer, Counter } from './style';
import { ActionDisplay } from '../../models';
export const Wrapper = styled(({ children, className }) => (
<ScrollArea horizontal vertical className={className}>
{children}
</ScrollArea>
))({
margin: 0,
padding: '10px 5px 20px',
});
interface InspectorProps {
theme: Theme;
sortObjectKeys: boolean;
showNonenumerable: boolean;
name: any;
data: any;
}
const ThemedInspector = withTheme(({ theme, ...props }: InspectorProps) => (
<Inspector theme={theme.addonActionsTheme || 'chromeLight'} {...props} />
));
interface ActionLoggerProps {
actions: ActionDisplay[];
onClear: () => void;
}
export const ActionLogger = ({ actions, onClear }: ActionLoggerProps) => (
<Fragment>
<Wrapper title="actionslogger">
{actions.map((action: ActionDisplay) => (
<Action key={action.id}>
{action.count > 1 && <Counter>{action.count}</Counter>}
<InspectorContainer>
<ThemedInspector
sortObjectKeys
showNonenumerable={false}
name={action.data.name}
data={action.data.args || action.data}
/>
</InspectorContainer>
</Action>
))}
</Wrapper>
<ActionBar actionItems={[{ title: 'Clear', onClick: onClear }]} />
</Fragment>
);

View File

@ -1,42 +0,0 @@
import { styled } from '@storybook/theming';
export const Actions = styled.pre({
flex: 1,
margin: 0,
padding: '8px 2px 20px 0',
overflowY: 'auto',
color: '#666',
});
export const Action = styled.div({
display: 'flex',
padding: '3px 3px 3px 0',
borderLeft: '5px solid transparent',
borderBottom: '1px solid transparent',
transition: 'all 0.1s',
alignItems: 'start',
});
export const Counter = styled.div({
margin: '0 5px 0 5px',
backgroundColor: '#777777',
color: '#ffffff',
padding: '1px 5px',
borderRadius: '20px',
});
export const Countwrap = styled.div({
paddingBottom: 2,
});
export const InspectorContainer = styled.div({
flex: 1,
padding: '0 0 0 5px',
});
export const Wrapper = styled.div({
flex: 1,
display: 'flex',
position: 'relative',
height: '100%',
});

View File

@ -0,0 +1,27 @@
import { styled } from '@storybook/theming';
import { opacify } from 'polished';
export const Action = styled.div({
display: 'flex',
padding: '0',
borderLeft: '5px solid transparent',
borderBottom: '1px solid transparent',
transition: 'all 0.1s',
alignItems: 'flex-start',
});
export const Counter = styled.div(({ theme }) => ({
backgroundColor: opacify(0.5, theme.appBorderColor),
color: theme.color.inverseText,
fontSize: theme.typography.size.s1,
fontWeight: theme.typography.weight.bold,
lineHeight: 1,
padding: '1px 5px',
borderRadius: '20px',
margin: '2px 0px',
}));
export const InspectorContainer = styled.div({
flex: 1,
padding: '0 0 0 5px',
});

View File

@ -1,78 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import deepEqual from 'fast-deep-equal';
import { STORY_RENDERED } from '@storybook/core-events';
import ActionLoggerComponent from '../../components/ActionLogger';
import { EVENT_ID } from '../..';
export default class ActionLogger extends React.Component {
state = { actions: [] };
componentDidMount() {
this.mounted = true;
const { api } = this.props;
api.on(EVENT_ID, this.addAction);
api.on(STORY_RENDERED, this.handleStoryChange);
}
componentWillUnmount() {
this.mounted = false;
const { api } = this.props;
api.off(STORY_RENDERED, this.handleStoryChange);
api.off(EVENT_ID, this.addAction);
}
handleStoryChange = () => {
const { actions } = this.state;
if (actions.length > 0 && actions[0].options.clearOnStoryChange) {
this.clearActions();
}
};
addAction = action => {
let { actions = [] } = this.state;
actions = [...actions];
const previous = actions.length && actions[0];
if (previous && deepEqual(previous.data, action.data, { strict: true })) {
previous.count++; // eslint-disable-line
} else {
action.count = 1; // eslint-disable-line
actions.unshift(action);
}
this.setState({ actions: actions.slice(0, action.options.limit) });
};
clearActions = () => {
this.setState({ actions: [] });
};
render() {
const { actions = [] } = this.state;
const { active } = this.props;
const props = {
actions,
onClear: this.clearActions,
};
return active ? <ActionLoggerComponent {...props} /> : null;
}
}
ActionLogger.propTypes = {
active: PropTypes.bool.isRequired,
channel: PropTypes.shape({
emit: PropTypes.func,
on: PropTypes.func,
removeListener: PropTypes.func,
}).isRequired,
api: PropTypes.shape({
on: PropTypes.func,
getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func,
}).isRequired,
};

View File

@ -0,0 +1,87 @@
import React, { Component } from 'react';
import deepEqual from 'fast-deep-equal';
import { API } from '@storybook/api';
import { STORY_RENDERED } from '@storybook/core-events';
import { ActionLogger as ActionLoggerComponent } from '../../components/ActionLogger';
import { EVENT_ID } from '../..';
import { ActionDisplay } from '../../models';
interface ActionLoggerProps {
active: boolean;
api: API;
}
interface ActionLoggerState {
actions: ActionDisplay[];
}
const safeDeepEqual = (a: any, b: any): boolean => {
try {
return deepEqual(a, b);
} catch (e) {
return false;
}
};
export default class ActionLogger extends Component<ActionLoggerProps, ActionLoggerState> {
private mounted: boolean;
constructor(props: ActionLoggerProps) {
super(props);
this.state = { actions: [] };
}
componentDidMount() {
this.mounted = true;
const { api } = this.props;
api.on(EVENT_ID, this.addAction);
api.on(STORY_RENDERED, this.handleStoryChange);
}
componentWillUnmount() {
this.mounted = false;
const { api } = this.props;
api.off(STORY_RENDERED, this.handleStoryChange);
api.off(EVENT_ID, this.addAction);
}
handleStoryChange = () => {
const { actions } = this.state;
if (actions.length > 0 && actions[0].options.clearOnStoryChange) {
this.clearActions();
}
};
addAction = (action: ActionDisplay) => {
this.setState((prevState: ActionLoggerState) => {
const actions = [...prevState.actions];
const previous = actions.length && actions[0];
if (previous && safeDeepEqual(previous.data, action.data)) {
previous.count++; // eslint-disable-line
} else {
action.count = 1; // eslint-disable-line
actions.unshift(action);
}
return { actions: actions.slice(0, action.options.limit) };
});
};
clearActions = () => {
this.setState({ actions: [] });
};
render() {
const { actions = [] } = this.state;
const { active } = this.props;
const props = {
actions,
onClear: this.clearActions,
};
return active ? <ActionLoggerComponent {...props} /> : null;
}
}

View File

@ -1,27 +0,0 @@
import {
action,
actions,
decorate,
configureActions,
decorateAction,
withActions,
} from './preview';
// addons, panels and events get unique names using a prefix
import { ADDON_ID, PANEL_ID, EVENT_ID } from './constants';
export {
action,
actions,
decorate,
configureActions,
decorateAction,
withActions,
ADDON_ID,
PANEL_ID,
EVENT_ID,
};
if (module && module.hot && module.hot.decline) {
module.hot.decline();
}

View File

@ -0,0 +1,7 @@
export * from './constants';
export * from './models';
export * from './preview';
if (module && module.hot && module.hot.decline) {
module.hot.decline();
}

View File

@ -5,13 +5,9 @@ import { ADDON_ID, PANEL_ID } from '.';
export function register() {
addons.register(ADDON_ID, api => {
const channel = addons.getChannel();
addons.addPanel(PANEL_ID, {
title: 'Action Logger',
// eslint-disable-next-line react/prop-types
render: ({ active, key }) => (
<ActionLogger key={key} channel={channel} api={api} active={active} />
),
title: 'Actions',
render: ({ active, key }) => <ActionLogger key={key} api={api} active={active} />,
});
});
}

View File

@ -0,0 +1,11 @@
import { ActionOptions } from './ActionOptions';
export interface ActionDisplay {
id: string;
data: {
name: string;
args: any[];
};
count: number;
options: ActionOptions;
}

View File

@ -0,0 +1,5 @@
export interface ActionOptions {
depth?: number;
clearOnStoryChange?: boolean;
limit?: number;
}

View File

@ -0,0 +1,5 @@
import { HandlerFunction } from './HandlerFunction';
export interface ActionsMap {
[key: string]: HandlerFunction;
}

View File

@ -0,0 +1 @@
export type DecoratorFunction = (args: any[]) => any[];

View File

@ -0,0 +1 @@
export type HandlerFunction = (...args: any[]) => void;

View File

@ -0,0 +1,5 @@
export * from './ActionDisplay';
export * from './ActionOptions';
export * from './ActionsMap';
export * from './DecoratorFunction';
export * from './HandlerFunction';

View File

@ -1,22 +0,0 @@
import uuid from 'uuid/v1';
import addons from '@storybook/addons';
import { EVENT_ID } from '../constants';
export default function action(name, options = {}) {
const actionOptions = {
...options,
};
// eslint-disable-next-line no-shadow
const handler = function action(...args) {
const channel = addons.getChannel();
const id = uuid();
channel.emit(EVENT_ID, {
id,
data: { name, args },
options: actionOptions,
});
};
return handler;
}

View File

@ -0,0 +1,29 @@
import uuid from 'uuid/v1';
import { addons } from '@storybook/addons';
import { EVENT_ID } from '../constants';
import { ActionDisplay, ActionOptions, HandlerFunction } from '../models';
export function action(name: string, options: ActionOptions = {}): HandlerFunction {
const actionOptions = {
...options,
};
const handler = function actionHandler(...args: any[]) {
const channel = addons.getChannel();
const id = uuid();
const minDepth = 5; // anything less is really just storybook internals
const actionDisplayToEmit: ActionDisplay = {
id,
count: 0,
data: { name, args },
options: {
...actionOptions,
depth: minDepth + (actionOptions.depth || 3),
},
};
channel.emit(EVENT_ID, actionDisplayToEmit);
};
return handler;
}

View File

@ -1,7 +1,8 @@
import action from './action';
import { action } from './action';
import { ActionOptions, ActionsMap } from '../models';
export default function actions(...args) {
let options = {};
export function actions(...args: any[]): ActionsMap {
let options: ActionOptions = {};
const names = args;
// last argument can be options
if (names.length !== 1 && typeof args[args.length - 1] !== 'string') {
@ -16,7 +17,7 @@ export default function actions(...args) {
});
}
const actionsObject = {};
const actionsObject: ActionsMap = {};
Object.keys(namesObject).forEach(name => {
actionsObject[name] = action(namesObject[name], options);
});

View File

@ -1,9 +0,0 @@
export const config = {
depth: 10,
clearOnStoryChange: true,
limit: 50,
};
export function configureActions(options = {}) {
Object.assign(config, options);
}

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