Merge branch 'next' of github.com:storybooks/storybook into next

This commit is contained in:
Michael Shilman 2019-03-08 11:23:01 +08:00
commit 8445452644
19 changed files with 219 additions and 222 deletions

View File

@ -21,7 +21,7 @@
},
"license": "MIT",
"main": "dist/index.js",
"jsnext:main": "src/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
@ -36,10 +36,12 @@
"core-js": "^2.6.5",
"global": "^4.3.2",
"memoizerific": "^1.11.3",
"prop-types": "^15.7.2",
"react": "^16.8.3",
"util-deprecate": "^1.0.2"
},
"devDependencies": {
"@types/common-tags": "^1.8.0"
},
"publishConfig": {
"access": "public"
}

View File

@ -1,16 +1,14 @@
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, Icons } from '@storybook/components';
import { ActionBar, Icons, ScrollArea } from '@storybook/components';
import { ScrollArea } from '@storybook/components/dist/ScrollArea/ScrollArea';
import EVENTS from '../constants';
import Tabs from './Tabs';
import Report from './Report';
import { AxeResults, Result } from 'axe-core';
import { Report } from './Report';
import { Tabs } from './Tabs';
import { EVENTS } from '../constants';
const Icon = styled(Icons)(
{
@ -18,7 +16,7 @@ const Icon = styled(Icons)(
width: '12px',
marginRight: '4px',
},
({ status, theme }) =>
({ status, theme }: any) =>
status === 'running'
? {
animation: `${theme.animation.rotate360} 1s linear infinite;`,
@ -34,17 +32,23 @@ const Violations = styled.span(({ theme }) => ({
color: theme.color.negative,
}));
class A11YPanel extends Component {
static propTypes = {
active: PropTypes.bool.isRequired,
api: PropTypes.shape({
on: PropTypes.func,
emit: PropTypes.func,
off: PropTypes.func,
}).isRequired,
};
interface A11YPanelState {
status: string;
passes: Result[];
violations: Result[];
}
state = {
interface A11YPanelProps {
active: boolean;
api: {
on(event: string, callback: (data: any) => void): void;
off(event: string, callback: (data: any) => void): void;
emit(event: string): void;
};
}
export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
state: A11YPanelState = {
status: 'ready',
passes: [],
violations: [],
@ -57,7 +61,7 @@ class A11YPanel extends Component {
api.on(EVENTS.RESULT, this.onUpdate);
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: A11YPanelProps) {
// TODO: might be able to remove this
const { active } = this.props;
@ -73,7 +77,7 @@ class A11YPanel extends Component {
api.off(EVENTS.RESULT, this.onUpdate);
}
onUpdate = ({ passes, violations }) => {
onUpdate = ({ passes, violations }: AxeResults) => {
this.setState(
{
status: 'ran',
@ -153,5 +157,3 @@ class A11YPanel extends Component {
) : null;
}
}
export default A11YPanel;

View File

@ -16,7 +16,7 @@ const ColorIcon = styled.span(
height: '1rem',
width: '1rem',
},
({ filter }) => ({
({ filter }: { filter: string | null }) => ({
filter: filter === 'mono' ? 'grayscale(100%)' : `url('#${filter}')`,
}),
({ theme }) => ({
@ -24,13 +24,21 @@ const ColorIcon = styled.span(
})
);
class ColorBlindness extends Component {
state = {
// tslint:disable-next-line: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 => {
setFilter = (filter: string | null) => {
const iframe = getIframe();
if (iframe) {
@ -44,6 +52,8 @@ class ColorBlindness extends Component {
}
};
onVisibilityChange = (s: boolean) => this.setState({ expanded: s });
render() {
const { filter, expanded } = this.state;
@ -69,10 +79,12 @@ class ColorBlindness extends Component {
if (filter !== null) {
colorList = [
{
id: 'reset',
title: 'Reset color filter',
onClick: () => {
this.setFilter(null);
},
right: undefined,
},
...colorList,
];
@ -83,7 +95,7 @@ class ColorBlindness extends Component {
placement="top"
trigger="click"
tooltipShown={expanded}
onVisibilityChange={s => this.setState({ expanded: s })}
onVisibilityChange={this.onVisibilityChange}
tooltip={<TooltipLinkList links={colorList} />}
closeOnClick
>
@ -94,5 +106,3 @@ class ColorBlindness extends Component {
);
}
}
export default ColorBlindness;

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,48 @@
import React, { FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import { NodeResult } from 'axe-core';
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',
});
interface ElementProps {
element: NodeResult;
passes: boolean;
}
const Element: FunctionComponent<ElementProps> = ({ 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>
);
};
interface ElementsProps {
elements: NodeResult[];
passes: boolean;
}
export const Elements: FunctionComponent<ElementsProps> = ({ elements, passes }) => (
<ol>
{elements.map((element, index) => (
<Element passes={passes} element={element} key={index} />
))}
</ol>
);

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 { Result } from 'axe-core';
const Wrapper = styled.div(({ theme }) => ({
backgroundColor: theme.background.bar,
@ -18,7 +18,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 +31,4 @@ function Info({ item }) {
</Link>
</Wrapper>
);
}
Info.propTypes = {
item: PropTypes.shape({
help: PropTypes.node,
helpUrl: PropTypes.string,
}).isRequired,
};
export default Info;

View File

@ -1,16 +1,16 @@
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';
import { Result } from 'axe-core';
import { Info } from './Info';
import { Elements } from './Elements';
import { Tags } from './Tags';
const Wrapper = styled.div();
const Icon = styled(Icons)(({ theme }) => ({
const Icon = styled<any, any>(Icons)(({ theme }) => ({
height: 10,
width: 10,
color: theme.color.mediumdark,
@ -37,16 +37,16 @@ const HeaderBar = styled.button(({ theme }) => ({
},
}));
class Item extends Component {
static propTypes = {
item: PropTypes.shape({
description: PropTypes.string,
nodes: PropTypes.array,
tags: PropTypes.array,
}).isRequired,
passes: PropTypes.bool.isRequired,
};
interface ItemProps {
item: Result;
passes: boolean;
}
interface ItemState {
open: boolean;
}
export class Item extends Component<ItemProps, ItemState> {
state = {
open: false,
};
@ -84,5 +84,3 @@ class Item extends Component {
);
}
}
export default Item;

View File

@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import { Icons } from '@storybook/components';
import { CheckResult } from 'axe-core';
const impactColors = {
minor: '#f1c40f',
@ -17,7 +17,7 @@ const List = styled.div({
flexDirection: 'column',
padding: '4px',
fontWeight: '400',
});
} as any);
const Item = styled.div({
display: 'flex',
@ -29,7 +29,7 @@ const Message = styled.div({
paddingLeft: '6px',
});
const Status = styled.div(({ passes, impact }) => ({
const Status = styled.div(({ passes, impact }: { passes: boolean; impact: string }) => ({
height: '16px',
width: '16px',
borderRadius: '8px',
@ -39,10 +39,15 @@ const Status = styled.div(({ passes, impact }) => ({
alignItems: 'center',
textAlign: 'center',
flex: '0 0 16px',
color: passes ? impactColors.success : impactColors[impact],
color: passes ? impactColors.success : (impactColors as any)[impact],
}));
const Rule = ({ rule, passes }) => (
interface RuleProps {
rule: CheckResult;
passes: boolean;
}
const Rule: FunctionComponent<RuleProps> = ({ rule, passes }) => (
<Item>
<Status passes={passes || undefined} impact={rule.impact}>
{passes ? <Icons icon="check" /> : <Icons icon="cross" />}
@ -51,15 +56,12 @@ const Rule = ({ rule, passes }) => (
</Item>
);
Rule.propTypes = {
rule: PropTypes.shape({
message: PropTypes.node,
}).isRequired,
passes: PropTypes.bool.isRequired,
};
interface RulesProps {
rules: CheckResult[];
passes: boolean;
}
/* eslint-disable react/no-array-index-key */
function Rules({ rules, passes }) {
export const Rules: FunctionComponent<RulesProps> = ({ rules, passes }) => {
return (
<List>
{rules.map((rule, index) => (
@ -67,14 +69,4 @@ function Rules({ rules, passes }) {
))}
</List>
);
}
Rules.propTypes = {
rules: PropTypes.arrayOf(
PropTypes.shape({
message: PropTypes.node,
})
).isRequired,
passes: PropTypes.bool.isRequired,
};
export default Rules;

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',
@ -16,7 +16,11 @@ const Item = styled.div(({ theme }) => ({
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

@ -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';
export interface ReportProps {
items: Result[];
empty: string;
passes: boolean;
}
export const Report: FunctionComponent<ReportProps> = ({ items, empty, passes }) => (
<Fragment>
{items.length ? (
items.map(item => <Item passes={passes} item={item} key={item.id} />)
) : (
<Placeholder key="placeholder">{empty}</Placeholder>
)}
</Fragment>
);

View File

@ -1,5 +1,4 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
// TODO: reuse the Tabs component from @storybook/theming instead
@ -37,7 +36,7 @@ const Item = styled.button(
borderBottom: `3px solid ${theme.color.secondary}`,
},
}),
({ active, theme }) =>
({ active, theme }: any) =>
active
? {
opacity: 1,
@ -46,12 +45,23 @@ const Item = styled.button(
: {}
);
class Tabs extends Component {
state = {
interface TabsProps {
tabs: Array<{
label: JSX.Element;
panel: JSX.Element;
}>;
}
interface TabsState {
active: number;
}
export class Tabs extends Component<TabsProps, TabsState> {
state: TabsState = {
active: 0,
};
onToggle = index => {
onToggle = (index: number) => {
this.setState({
active: index,
});
@ -80,14 +90,3 @@ class Tabs extends Component {
);
}
}
Tabs.propTypes = {
tabs: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.node,
panel: PropTypes.node,
})
).isRequired,
};
export default Tabs;

View File

@ -5,4 +5,4 @@ export const PARAM_KEY = `a11y`;
const RESULT = `${ADDON_ID}/result`;
const REQUEST = `${ADDON_ID}/request`;
export default { RESULT, REQUEST };
export const EVENTS = { RESULT, REQUEST };

View File

@ -1,15 +1,15 @@
import { document } from 'global';
import axe from 'axe-core';
import axe, { AxeResults, RunOptions, Spec } from 'axe-core';
import deprecate from 'util-deprecate';
import { stripIndents } from 'common-tags';
import addons from '@storybook/addons';
import addons, { StoryWrapper } from '@storybook/addons';
import { STORY_RENDERED } from '@storybook/core-events';
import EVENTS, { PARAM_KEY } from './constants';
import { EVENTS, PARAM_KEY } from './constants';
const channel = addons.getChannel();
let progress = Promise.resolve();
let setup = {};
let setup: { config: Spec; options: RunOptions } = { config: {}, options: {} };
const getElement = () => {
const storyRoot = document.getElementById('story-root');
@ -20,11 +20,11 @@ const getElement = () => {
return document.getElementById('root');
};
const report = input => {
const report = (input: AxeResults) => {
channel.emit(EVENTS.RESULT, input);
};
const run = (config, options) => {
const run = (config: Spec, options: RunOptions) => {
progress = progress.then(() => {
axe.reset();
if (config) {
@ -33,16 +33,18 @@ const run = (config, options) => {
return axe
.run(
getElement(),
options || {
restoreScroll: true,
}
options ||
// tslint:disable-next-line:no-object-literal-type-assertion
({
restoreScroll: true,
} as RunOptions) // cast to RunOptions is necessary because axe types are not up to date
)
.then(report);
});
};
// NOTE: we should add paramaters to the STORY_RENDERED event and deprecate this
export const withA11y = (getStory, context) => {
export const withA11y: StoryWrapper = (getStory, context) => {
const params = context.parameters[PARAM_KEY];
if (params) {
setup = params;
@ -59,19 +61,21 @@ if (module && module.hot && module.hot.decline) {
// TODO: REMOVE at v6.0.0
export const withA11Y = deprecate(
(...args) => withA11y(...args),
// @ts-ignore
(...args: any[]) => withA11y(...args),
'withA11Y has been renamed withA11y'
);
// TODO: REMOVE at v6.0.0
export const checkA11y = deprecate(
(...args) => withA11y(...args),
// @ts-ignore
(...args: any[]) => withA11y(...args),
'checkA11y has been renamed withA11y'
);
// TODO: REMOVE at v6.0.0
export const configureA11y = deprecate(
config => {
(config: any) => {
setup = config;
},
stripIndents`

View File

@ -1,17 +1,16 @@
import React, { Fragment } from 'react';
import addons, { types } from '@storybook/addons';
import React, { Fragment, FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import Panel from './components/Panel';
import ColorBlindness from './components/ColorBlindness';
import { ADDON_ID, PANEL_ID } from './constants';
import { ColorBlindness } from './components/ColorBlindness';
import { A11YPanel } from './components/A11YPanel';
import { addons, types } from '@storybook/addons';
const Hidden = styled.div(() => ({
display: 'none',
}));
const PreviewWrapper = p => (
const PreviewWrapper: FunctionComponent<{}> = p => (
<Fragment>
{p.children}
<Hidden>
@ -81,20 +80,21 @@ const PreviewWrapper = p => (
addons.register(ADDON_ID, api => {
addons.add(PANEL_ID, {
title: '',
type: types.TOOL,
match: ({ viewMode }) => viewMode === 'story',
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} />,
type: types.PANEL,
render: ({ active, key }) => <A11YPanel key={key} api={api} active={active} />,
});
addons.add(PANEL_ID, {
title: '',
type: types.PREVIEW,
render: PreviewWrapper,
render: PreviewWrapper as any,
});
});

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

@ -0,0 +1,3 @@
// TODO: following packages need definition files or a TS migration
declare module '@storybook/components';
declare module 'global';

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

@ -40,7 +40,7 @@ addDecorator(storyFn => (
addParameters({
a11y: {
configure: {},
config: {},
options: {
checks: { 'color-contrast': { options: { noScroll: true } } },
restoreScroll: true,

View File

@ -5196,7 +5196,7 @@ exports[`Storyshots Core|Parameters passed to story 1`] = `
<pre>
Parameters are {
"a11y": {
"configure": {},
"config": {},
"options": {
"checks": {
"color-contrast": {