diff --git a/addons/a11y/package.json b/addons/a11y/package.json index 1c4009ef112b..2a40ed4f95f7 100644 --- a/addons/a11y/package.json +++ b/addons/a11y/package.json @@ -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" } diff --git a/addons/a11y/src/components/Panel.js b/addons/a11y/src/components/A11YPanel.tsx similarity index 79% rename from addons/a11y/src/components/Panel.js rename to addons/a11y/src/components/A11YPanel.tsx index 2898eb45def6..482cceda5915 100644 --- a/addons/a11y/src/components/Panel.js +++ b/addons/a11y/src/components/A11YPanel.tsx @@ -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[]; +} + +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; }; +} - state = { +export class A11YPanel extends Component { + 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; diff --git a/addons/a11y/src/components/ColorBlindness.js b/addons/a11y/src/components/ColorBlindness.tsx similarity index 79% rename from addons/a11y/src/components/ColorBlindness.js rename to addons/a11y/src/components/ColorBlindness.tsx index 375b9a723d45..1e037ffe5912 100644 --- a/addons/a11y/src/components/ColorBlindness.js +++ b/addons/a11y/src/components/ColorBlindness.tsx @@ -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 { + 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={} closeOnClick > @@ -94,5 +106,3 @@ class ColorBlindness extends Component { ); } } - -export default ColorBlindness; diff --git a/addons/a11y/src/components/Report/Elements.js b/addons/a11y/src/components/Report/Elements.js deleted file mode 100644 index c8ba249e775a..000000000000 --- a/addons/a11y/src/components/Report/Elements.js +++ /dev/null @@ -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 ( - - {element.target[0]} - - - ); -} -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 }) => ( -
    - {elements.map((element, index) => ( - - ))} -
-); - -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; diff --git a/addons/a11y/src/components/Report/Elements.tsx b/addons/a11y/src/components/Report/Elements.tsx new file mode 100644 index 000000000000..cce537cba04c --- /dev/null +++ b/addons/a11y/src/components/Report/Elements.tsx @@ -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 = ({ element, passes }) => { + const { any, all, none } = element; + + const rules = [...any, ...all, ...none]; + + return ( + + {element.target[0]} + + + ); +}; + +interface ElementsProps { + elements: NodeResult[]; + passes: boolean; +} + +export const Elements: FunctionComponent = ({ elements, passes }) => ( +
    + {elements.map((element, index) => ( + + ))} +
+); diff --git a/addons/a11y/src/components/Report/Info.js b/addons/a11y/src/components/Report/Info.tsx similarity index 69% rename from addons/a11y/src/components/Report/Info.js rename to addons/a11y/src/components/Report/Info.tsx index 56054a38286b..678eb57d2628 100644 --- a/addons/a11y/src/components/Report/Info.js +++ b/addons/a11y/src/components/Report/Info.tsx @@ -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 = ({ item }) => { return ( {item.help} @@ -27,13 +31,4 @@ function Info({ item }) { ); -} - -Info.propTypes = { - item: PropTypes.shape({ - help: PropTypes.node, - helpUrl: PropTypes.string, - }).isRequired, }; - -export default Info; diff --git a/addons/a11y/src/components/Report/Item.js b/addons/a11y/src/components/Report/Item.tsx similarity index 78% rename from addons/a11y/src/components/Report/Item.js rename to addons/a11y/src/components/Report/Item.tsx index 69dbddaea69e..6a493173a4d9 100644 --- a/addons/a11y/src/components/Report/Item.js +++ b/addons/a11y/src/components/Report/Item.tsx @@ -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(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 { state = { open: false, }; @@ -84,5 +84,3 @@ class Item extends Component { ); } } - -export default Item; diff --git a/addons/a11y/src/components/Report/Rules.js b/addons/a11y/src/components/Report/Rules.tsx similarity index 63% rename from addons/a11y/src/components/Report/Rules.js rename to addons/a11y/src/components/Report/Rules.tsx index 10989e555643..0a142eb98ee9 100644 --- a/addons/a11y/src/components/Report/Rules.js +++ b/addons/a11y/src/components/Report/Rules.tsx @@ -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 = ({ rule, passes }) => ( {passes ? : } @@ -51,15 +56,12 @@ const Rule = ({ rule, passes }) => ( ); -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 = ({ rules, passes }) => { return ( {rules.map((rule, index) => ( @@ -67,14 +69,4 @@ function Rules({ rules, passes }) { ))} ); -} -Rules.propTypes = { - rules: PropTypes.arrayOf( - PropTypes.shape({ - message: PropTypes.node, - }) - ).isRequired, - passes: PropTypes.bool.isRequired, }; - -export default Rules; diff --git a/addons/a11y/src/components/Report/Tags.js b/addons/a11y/src/components/Report/Tags.tsx similarity index 68% rename from addons/a11y/src/components/Report/Tags.js rename to addons/a11y/src/components/Report/Tags.tsx index 0ef1d2b2e32d..559a31ee79fa 100644 --- a/addons/a11y/src/components/Report/Tags.js +++ b/addons/a11y/src/components/Report/Tags.tsx @@ -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 = ({ tags }) => { return ( {tags.map(tag => ( @@ -24,9 +28,4 @@ function Tags({ tags }) { ))} ); -} -Tags.propTypes = { - tags: PropTypes.arrayOf(PropTypes.node).isRequired, }; - -export default Tags; diff --git a/addons/a11y/src/components/Report/index.js b/addons/a11y/src/components/Report/index.js deleted file mode 100644 index 3a9bdb9fbe2c..000000000000 --- a/addons/a11y/src/components/Report/index.js +++ /dev/null @@ -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 }) => ( - - {items.length ? ( - items.map(item => ) - ) : ( - {empty} - )} - -); - -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; diff --git a/addons/a11y/src/components/Report/index.tsx b/addons/a11y/src/components/Report/index.tsx new file mode 100644 index 000000000000..2498962ccfd9 --- /dev/null +++ b/addons/a11y/src/components/Report/index.tsx @@ -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 = ({ items, empty, passes }) => ( + + {items.length ? ( + items.map(item => ) + ) : ( + {empty} + )} + +); diff --git a/addons/a11y/src/components/Tabs.js b/addons/a11y/src/components/Tabs.tsx similarity index 85% rename from addons/a11y/src/components/Tabs.js rename to addons/a11y/src/components/Tabs.tsx index 43568a96ce2e..ca1aae619006 100644 --- a/addons/a11y/src/components/Tabs.js +++ b/addons/a11y/src/components/Tabs.tsx @@ -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 { + 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; diff --git a/addons/a11y/src/constants.js b/addons/a11y/src/constants.ts similarity index 82% rename from addons/a11y/src/constants.js rename to addons/a11y/src/constants.ts index 060abd6d2d32..a3563f5ed4bf 100755 --- a/addons/a11y/src/constants.js +++ b/addons/a11y/src/constants.ts @@ -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 }; diff --git a/addons/a11y/src/index.js b/addons/a11y/src/index.ts similarity index 68% rename from addons/a11y/src/index.js rename to addons/a11y/src/index.ts index 13428ae784d7..5c834cdc9007 100644 --- a/addons/a11y/src/index.js +++ b/addons/a11y/src/index.ts @@ -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` diff --git a/addons/a11y/src/register.js b/addons/a11y/src/register.tsx similarity index 86% rename from addons/a11y/src/register.js rename to addons/a11y/src/register.tsx index 5e48441705fa..4b9c9d22ffee 100644 --- a/addons/a11y/src/register.js +++ b/addons/a11y/src/register.tsx @@ -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 => ( {p.children} @@ -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: () => , }); addons.add(PANEL_ID, { - type: types.PANEL, title: 'Accessibility', - // eslint-disable-next-line react/prop-types - render: ({ active, key }) => , + type: types.PANEL, + render: ({ active, key }) => , }); addons.add(PANEL_ID, { + title: '', type: types.PREVIEW, - render: PreviewWrapper, + render: PreviewWrapper as any, }); }); diff --git a/addons/a11y/src/typings.d.ts b/addons/a11y/src/typings.d.ts new file mode 100644 index 000000000000..1cebcd2c971a --- /dev/null +++ b/addons/a11y/src/typings.d.ts @@ -0,0 +1,3 @@ +// TODO: following packages need definition files or a TS migration +declare module '@storybook/components'; +declare module 'global'; diff --git a/addons/a11y/tsconfig.json b/addons/a11y/tsconfig.json new file mode 100644 index 000000000000..8876bb6737a1 --- /dev/null +++ b/addons/a11y/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "types": ["webpack-env"] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "src/__tests__/**/*" + ] +} diff --git a/examples/official-storybook/config.js b/examples/official-storybook/config.js index a3e716b72f2e..cd15c87b136e 100644 --- a/examples/official-storybook/config.js +++ b/examples/official-storybook/config.js @@ -40,7 +40,7 @@ addDecorator(storyFn => ( addParameters({ a11y: { - configure: {}, + config: {}, options: { checks: { 'color-contrast': { options: { noScroll: true } } }, restoreScroll: true, diff --git a/examples/official-storybook/tests/__snapshots__/storyshots.test.js.snap b/examples/official-storybook/tests/__snapshots__/storyshots.test.js.snap index da4c2f46e456..86df522f604a 100644 --- a/examples/official-storybook/tests/__snapshots__/storyshots.test.js.snap +++ b/examples/official-storybook/tests/__snapshots__/storyshots.test.js.snap @@ -5196,7 +5196,7 @@ exports[`Storyshots Core|Parameters passed to story 1`] = `
   Parameters are {
   "a11y": {
-    "configure": {},
+    "config": {},
     "options": {
       "checks": {
         "color-contrast": {