diff --git a/addons/a11y/README.md b/addons/a11y/README.md
index d4d2638a9e6c..ec9d59c3a475 100755
--- a/addons/a11y/README.md
+++ b/addons/a11y/README.md
@@ -58,7 +58,8 @@ addParameters({
a11y: {
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)
+ options: {}, // axe-core optionsParameter (https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#options-parameter)
+ manual: true // optional flag to prevent the automatic check
},
});
diff --git a/addons/a11y/src/components/A11YPanel.test.js b/addons/a11y/src/components/A11YPanel.test.js
index 2f84b31afd39..9947bcc52f90 100644
--- a/addons/a11y/src/components/A11YPanel.test.js
+++ b/addons/a11y/src/components/A11YPanel.test.js
@@ -2,8 +2,6 @@ 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';
@@ -63,7 +61,7 @@ function ThemedA11YPanel(props) {
}
describe('A11YPanel', () => {
- it('should register STORY_RENDERED, RESULT and ERROR updater on mount', () => {
+ it('should register event listener on mount', () => {
// given
const api = createApi();
expect(api.on).not.toHaveBeenCalled();
@@ -73,12 +71,12 @@ describe('A11YPanel', () => {
// then
expect(api.on.mock.calls.length).toBe(3);
- expect(api.on.mock.calls[0][0]).toBe(STORY_RENDERED);
- expect(api.on.mock.calls[1][0]).toBe(EVENTS.RESULT);
- expect(api.on.mock.calls[2][0]).toBe(EVENTS.ERROR);
+ expect(api.on.mock.calls[0][0]).toBe(EVENTS.RESULT);
+ expect(api.on.mock.calls[1][0]).toBe(EVENTS.ERROR);
+ expect(api.on.mock.calls[2][0]).toBe(EVENTS.MANUAL);
});
- it('should request a run on tab activation', () => {
+ it('should show initial state on tab activation', () => {
// given
const api = createApi();
@@ -90,11 +88,10 @@ describe('A11YPanel', () => {
wrapper.update();
// then
- expect(api.emit).toHaveBeenCalledWith(EVENTS.REQUEST);
- expect(wrapper.find(ScrollArea).length).toBe(0);
+ expect(wrapper.find(A11YPanel)).toMatchSnapshot();
});
- it('should deregister STORY_RENDERED, RESULT and ERROR updater on unmount', () => {
+ it('should deregister event listener on unmount', () => {
// given
const api = createApi();
const wrapper = mount();
@@ -105,9 +102,25 @@ describe('A11YPanel', () => {
// then
expect(api.off.mock.calls.length).toBe(3);
- expect(api.off.mock.calls[0][0]).toBe(STORY_RENDERED);
- expect(api.off.mock.calls[1][0]).toBe(EVENTS.RESULT);
- expect(api.off.mock.calls[2][0]).toBe(EVENTS.ERROR);
+ expect(api.off.mock.calls[0][0]).toBe(EVENTS.RESULT);
+ expect(api.off.mock.calls[1][0]).toBe(EVENTS.ERROR);
+ expect(api.off.mock.calls[2][0]).toBe(EVENTS.MANUAL);
+ });
+
+ it('should show manual state depending on config', () => {
+ // given
+ const api = createApi();
+
+ const wrapper = mount();
+ expect(api.emit).not.toHaveBeenCalled();
+
+ // when
+ wrapper.setProps({ active: true });
+ api.emit(EVENTS.MANUAL, true);
+ wrapper.update();
+
+ // then
+ expect(wrapper.find(A11YPanel)).toMatchSnapshot();
});
it('should update run result', () => {
@@ -141,7 +154,7 @@ describe('A11YPanel', () => {
// given
const api = createApi();
const wrapper = mount();
- const request = api.on.mock.calls.find(([event]) => event === STORY_RENDERED)[1];
+ const request = api.on.mock.calls.find(([event]) => event === EVENTS.MANUAL)[1];
expect(
wrapper
@@ -170,7 +183,7 @@ describe('A11YPanel', () => {
// given
const api = createApi();
mount();
- const request = api.on.mock.calls.find(([event]) => event === STORY_RENDERED)[1];
+ const request = api.on.mock.calls.find(([event]) => event === EVENTS.MANUAL)[1];
expect(api.emit).not.toHaveBeenCalled();
// when
@@ -197,7 +210,7 @@ describe('A11YPanel', () => {
// given
const api = createApi();
const wrapper = mount();
- const request = api.on.mock.calls.find(([event]) => event === STORY_RENDERED)[1];
+ const request = api.on.mock.calls.find(([event]) => event === EVENTS.MANUAL)[1];
// when
request();
diff --git a/addons/a11y/src/components/A11YPanel.tsx b/addons/a11y/src/components/A11YPanel.tsx
index 194acca7be6d..89d916a71f57 100644
--- a/addons/a11y/src/components/A11YPanel.tsx
+++ b/addons/a11y/src/components/A11YPanel.tsx
@@ -1,8 +1,8 @@
+/* eslint-disable react/destructuring-assignment,default-case,consistent-return,no-case-declarations */
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';
@@ -20,60 +20,70 @@ export enum RuleType {
INCOMPLETION,
}
-const Icon = styled(Icons)(
- {
- height: '12px',
- width: '12px',
- marginRight: '4px',
- },
- ({ status, theme }: any) =>
- status === 'running'
- ? {
- animation: `${theme.animation.rotate360} 1s linear infinite;`,
- }
- : {}
-);
+const RotatingIcons = styled(Icons)(({ theme }) => ({
+ height: '12px',
+ width: '12px',
+ marginRight: '4px',
+ animation: `${theme.animation.rotate360} 1s linear infinite;`,
+}));
-const Passes = styled.span<{}>(({ theme }) => ({
+const Passes = styled.span(({ theme }) => ({
color: theme.color.positive,
}));
-const Violations = styled.span<{}>(({ theme }) => ({
+const Violations = styled.span(({ theme }) => ({
color: theme.color.negative,
}));
-const Incomplete = styled.span<{}>(({ theme }) => ({
+const Incomplete = styled.span(({ theme }) => ({
color: theme.color.warning,
}));
-const centeredStyle = {
+const Centered = styled.span({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
-};
-
-const Loader = styled(({ className }) => (
-
- Please wait while the accessibility scan is running
- ...
-
-))(centeredStyle);
-Loader.displayName = 'Loader';
-
-interface A11YPanelNormalState {
- status: 'ready' | 'ran' | 'running';
+});
+
+interface InitialState {
+ status: 'initial';
+}
+
+interface ManualState {
+ status: 'manual';
+}
+
+interface RunningState {
+ status: 'running';
+}
+
+interface RanState {
+ status: 'ran';
+ passes: Result[];
+ violations: Result[];
+ incomplete: Result[];
+}
+
+interface ReadyState {
+ status: 'ready';
passes: Result[];
violations: Result[];
incomplete: Result[];
}
-interface A11YPanelErrorState {
+interface ErrorState {
status: 'error';
error: unknown;
}
-type A11YPanelState = A11YPanelNormalState | A11YPanelErrorState;
+type A11YPanelState =
+ | InitialState
+ | ManualState
+ | RunningState
+ | RanState
+ | ReadyState
+ | ErrorState;
interface A11YPanelProps {
active: boolean;
@@ -82,18 +92,15 @@ interface A11YPanelProps {
export class A11YPanel extends Component {
state: A11YPanelState = {
- status: 'ready',
- passes: [],
- violations: [],
- incomplete: [],
+ status: 'initial',
};
componentDidMount() {
const { api } = this.props;
- api.on(STORY_RENDERED, this.request);
- api.on(EVENTS.RESULT, this.onUpdate);
+ api.on(EVENTS.RESULT, this.onResult);
api.on(EVENTS.ERROR, this.onError);
+ api.on(EVENTS.MANUAL, this.onManual);
}
componentDidUpdate(prevProps: A11YPanelProps) {
@@ -103,18 +110,18 @@ export class A11YPanel extends Component {
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);
+
+ api.off(EVENTS.RESULT, this.onResult);
api.off(EVENTS.ERROR, this.onError);
+ api.off(EVENTS.MANUAL, this.onManual);
}
- onUpdate = ({ passes, violations, incomplete }: AxeResults) => {
+ onResult = ({ passes, violations, incomplete }: AxeResults) => {
this.setState(
{
status: 'ran',
@@ -142,9 +149,18 @@ export class A11YPanel extends Component {
});
};
+ onManual = (manual: boolean) => {
+ if (manual) {
+ this.setState({
+ status: 'manual',
+ });
+ } else {
+ this.request();
+ }
+ };
+
request = () => {
const { api, active } = this.props;
-
if (active) {
this.setState(
{
@@ -163,43 +179,39 @@ export class A11YPanel extends Component {
const { active } = this.props;
if (!active) return null;
- // eslint-disable-next-line react/destructuring-assignment
- if (this.state.status === 'error') {
- const { error } = this.state;
- return (
-
- The accessibility scan encountered an error.
-
- {error}
-
- );
- }
-
- const { passes, violations, incomplete, status } = this.state;
-
- let actionTitle;
- if (status === 'ready') {
- actionTitle = 'Rerun tests';
- } else if (status === 'running') {
- actionTitle = (
-
- Running test
-
- );
- } else if (status === 'ran') {
- actionTitle = (
-
- Tests completed
-
- );
- }
-
- return (
-
-
- {status === 'running' ? (
-
+ switch (this.state.status) {
+ case 'initial':
+ return Initializing...;
+ case 'manual':
+ return (
+
+ Manually run the accessibility scan.
+
+
+ );
+ case 'running':
+ return (
+
+ Please wait while the accessibility scan is running
+ ...
+
+ );
+ case 'ready':
+ case 'ran':
+ const { passes, violations, incomplete, status } = this.state;
+ const actionTitle =
+ status === 'ready' ? (
+ 'Rerun tests'
) : (
+
+ Tests completed
+
+ );
+ return (
+
{
]}
/>
- )}
-
-
-
- );
+
+
+ );
+ case 'error':
+ const { error } = this.state;
+ return (
+
+ The accessibility scan encountered an error.
+
+ {error}
+
+ );
+ }
}
}
diff --git a/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap b/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap
index 2ac873d47536..0ab1b7ebe993 100644
--- a/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap
+++ b/addons/a11y/src/components/__snapshots__/A11YPanel.test.js.snap
@@ -1118,3 +1118,148 @@ exports[`A11YPanel should render report 1`] = `
`;
+
+exports[`A11YPanel should show initial state on tab activation 1`] = `
+.emotion-0 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ height: 100%;
+}
+
+
+
+
+ Initializing...
+
+
+
+`;
+
+exports[`A11YPanel should show manual state depending on config 1`] = `
+.emotion-0 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ height: 100%;
+}
+
+
+
+
+ Initializing...
+
+
+
+`;
diff --git a/addons/a11y/src/constants.ts b/addons/a11y/src/constants.ts
index efabac7a268b..79ef7446a159 100755
--- a/addons/a11y/src/constants.ts
+++ b/addons/a11y/src/constants.ts
@@ -7,5 +7,6 @@ export const CLEAR_ELEMENTS = 'CLEAR_ELEMENTS';
const RESULT = `${ADDON_ID}/result`;
const REQUEST = `${ADDON_ID}/request`;
const ERROR = `${ADDON_ID}/error`;
+const MANUAL = `${ADDON_ID}/manual`;
-export const EVENTS = { RESULT, REQUEST, ERROR };
+export const EVENTS = { RESULT, REQUEST, ERROR, MANUAL };
diff --git a/addons/a11y/src/index.ts b/addons/a11y/src/index.ts
index a7f53ec29a73..ef6b7d546d61 100644
--- a/addons/a11y/src/index.ts
+++ b/addons/a11y/src/index.ts
@@ -11,8 +11,10 @@ interface Setup {
element?: ElementContext;
config: Spec;
options: RunOptions;
+ manual: boolean;
}
-let setup: Setup = { element: null, config: {}, options: {} };
+
+let setup: Setup = { element: null, config: {}, options: {}, manual: false };
const getElement = () => {
const storyRoot = document.getElementById('story-root');
@@ -58,12 +60,14 @@ export const withA11y = makeDecorator({
if (storedDefaultSetup === null) {
storedDefaultSetup = { ...setup };
}
- Object.assign(setup, parameters as Setup);
+ Object.assign(setup, parameters as Partial);
} else if (storedDefaultSetup !== null) {
Object.assign(setup, storedDefaultSetup);
storedDefaultSetup = null;
}
+
addons.getChannel().on(EVENTS.REQUEST, () => run(setup.element, setup.config, setup.options));
+ addons.getChannel().emit(EVENTS.MANUAL, setup.manual);
return getStory(context);
},
diff --git a/lib/components/src/icon/icon.tsx b/lib/components/src/icon/icon.tsx
index fc8f1b1a0202..e421548de521 100644
--- a/lib/components/src/icon/icon.tsx
+++ b/lib/components/src/icon/icon.tsx
@@ -2,13 +2,13 @@ import React, { FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import icons, { IconKey } from './icons';
-import Svg from './svg';
+import Svg, { SvgProps } from './svg';
const Path = styled.path({
fill: 'currentColor',
});
-export interface IconsProps {
+export interface IconsProps extends SvgProps {
icon: IconKey;
}