Initial commit

This commit is contained in:
Jaco Bovenschen 2016-10-02 18:20:27 +02:00 committed by Norbert de Langen
parent 38f2f49a95
commit 2a434ddef4
28 changed files with 719 additions and 0 deletions

3
addons/a11y/.babelrc Executable file
View File

@ -0,0 +1,3 @@
{
"presets": ["react-app"]
}

3
addons/a11y/.gitignore vendored Executable file
View File

@ -0,0 +1,3 @@
node_modules
coverage
dist

2
addons/a11y/.npmignore Executable file
View File

@ -0,0 +1,2 @@
node_modules
.babelrc

View File

@ -0,0 +1,9 @@
var path = require('path');
var shell = require('shelljs');
var babel = ['node_modules', '.bin', 'babel'].join(path.sep);
// required for react-app preset
process.env.NODE_ENV = 'production';
shell.rm('-rf', 'dist')
shell.exec(babel + ' --ignore __tests__ src --out-dir dist')

View File

@ -0,0 +1 @@
import '../register';

View File

@ -0,0 +1,2 @@
import * as storybook from '@kadira/storybook';
storybook.configure(() => require('./stories'), module);

View File

@ -0,0 +1,24 @@
import React from 'react';
import { storiesOf } from '@kadira/storybook';
import { checkA11y } from './../src';
storiesOf('Button', module)
.addDecorator(checkA11y)
.add('Default', () => (
<button>
Correct Button
</button>
))
.add('Red button', () => (
<div>
<button style={{ backgroundColor: 'red', color: 'darkRed', }}>
Incorrect Button
</button>
<button>
Correct Button
</button>
<button style={{ backgroundColor: 'blue', color: 'lightBlue', height: '20px', width: '20px' }}>
</button>
</div>
));

6
addons/a11y/CHANGELOG.md Executable file
View File

@ -0,0 +1,6 @@
## Changelog
### v0.0.1
* Initial release
* Implemented [axe-core](https://github.com/dequelabs/axe-core) as the Accessibility Engine.

41
addons/a11y/README.md Executable file
View File

@ -0,0 +1,41 @@
# storybook-addon-a11y
This storybook addon can be helpfull to make you're UI components more accessibile.
![](docs/screenshot.png)
## Getting started
First, install the addon.
```shell
$ npm install -D storybook-addon-a11y
```
Add this line to your `addons.js` file (create this file inside your storybook config directory if needed).
```js
import 'storybook-addon-a11y/register';
```
import the `'checkA11y'` decorator to check you're stories for violations within your components.
```js
import React from 'react';
import { storiesOf } from '@kadira/storybook';
import { checkA11y } from 'storybook-addon-a11y';
storiesOf('button', module)
.addDecorator(checkA11y)
.add('Accessible', () => (
<button>
Accessible button
</button>
))
.add('Inaccessible', () => (
<button style={{ backgroundColor: 'red', color: 'darkRed', }}>
Inaccessible button
</button>
));
```

7
addons/a11y/ROADMAP.md Normal file
View File

@ -0,0 +1,7 @@
* Make UI accessibile
* Add color blindness filters ([Example](http://lowvision.support/))
* Show in story where violations are.
* Make it configurable
* Add more example tests
* Add tests
* Make CI integration possible

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

2
addons/a11y/manager.js Executable file
View File

@ -0,0 +1,2 @@
const manager = require('./src/register');
manager.init();

44
addons/a11y/package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "storybook-addon-a11y",
"version": "0.0.1",
"description": "a11y addon for storybook",
"main": "preview.js",
"scripts": {
"prepublish": "node .scripts/npm-prepublish.js",
"storybook": "start-storybook -p 9001",
"test": "jest --coverage --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/jbovenschen/storybook-addon-a11y.git"
},
"keywords": [
"storybook"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/jbovenschen/storybook-addon-a11y/issues"
},
"homepage": "https://github.com/jbovenschen/storybook-addon-a11y#readme",
"devDependencies": {
"@kadira/storybook": "^2.20.1",
"babel-cli": "^6.14.0",
"babel-jest": "^15.0.0",
"babel-polyfill": "^6.13.0",
"babel-preset-react-app": "^0.2.1",
"jest": "^15.1.1",
"lodash": "^4.16.2",
"react": "^15.3.1",
"react-addons-test-utils": "^15.3.2",
"react-dom": "^15.3.2",
"shelljs": "^0.7.4"
},
"peerDependencies": {
"@kadira/storybook-addons": "^1.5.0",
"react": "^0.14.7 || ^15.0.0",
"react-dom": "^0.14.7 || ^15.0.0"
},
"dependencies": {
"axe-core": "^2.0.7"
}
}

2
addons/a11y/preview.js Executable file
View File

@ -0,0 +1,2 @@
const preview = require('./dist/preview');
preview.init();

3
addons/a11y/register.js Executable file
View File

@ -0,0 +1,3 @@
// NOTE: loading addons using this file is deprecated!
// please use manager.js and preview.js files instead
require('./manager');

View File

@ -0,0 +1,14 @@
import React from 'react';
import WrapStory from './components/WrapStory';
// Run all a11y checks inside
class A11yManager {
wrapStory(channel, storyFn, context) {
const props = { context, storyFn, channel };
return (<WrapStory {...props} />);
}
}
export default A11yManager;

View File

@ -0,0 +1,83 @@
import React, { Component } from 'react';
import addons from '@kadira/storybook-addons';
import { EVENT_ID } from './../shared';
import Tabs from './Tabs';
import Report from './Report';
const styles = {
passes: {
color: '#2ecc71',
},
violations: {
color: '#e74c3c',
},
}
class Panel extends Component {
constructor(props, ...args) {
super(props, ...args);
this.state = {
passes: [],
violations: [],
};
this.channel = addons.getChannel();
this.onUpdate = this.onUpdate.bind(this);
}
componentDidMount() {
this.channel.on('addon:a11y:check', this.onUpdate);
}
componentWillUnmount() {
this.channel.removeListener('addon:a11y:check', this.onUpdate);
}
onUpdate({ passes, violations }) {
this.setState({
passes,
violations,
})
}
render() {
const { passes, violations } = this.state;
return (
<Tabs
tabs={[{
label: (
<span style={styles.violations}>
Violations
</span>
),
panel: (
<Report
passes={false}
items={violations}
empty="No a11y violations found."
/>
)
}, {
label: (
<span style={styles.passes}>
Passes
</span>
),
panel: (
<Report
passes
items={passes}
empty="No a11y check passed"
/>
)
}]}
/>
)
return <div>{this.state.text}</div>;
}
}
export default Panel;

View File

@ -0,0 +1,50 @@
import React from 'react';
import Rules from './Rules';
const styles = {
element: {
fontWeight: 600,
},
target: {
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 (
<li style={styles.element}>
<span style={styles.target}>
{element.target[0]}
</span>
<Rules
rules={rules}
passes={passes}
/>
</li>
)
}
function Elements({ elements, passes }) {
return (
<ol style={styles.element}>
{elements.map((element, index) => (
<Element
passes={passes}
element={element}
key={index}
/>
))}
</ol>
);
}
export default Elements;

View File

@ -0,0 +1,41 @@
import React, { PropTypes } from 'react';
const styles = {
info: {
backgroundColor: 'rgb(234, 234, 234)',
padding: '12px',
marginBottom: '10px',
},
help: {
margin: '0 0 12px',
},
helpUrl: {
marginTop: '12px',
textDecoration: 'underline',
color: 'rgb(130, 130, 130)',
display: 'block',
},
}
function Info({ item }) {
return (
<div style={styles.info}>
<p style={styles.help}>
{item.help}
</p>
<a
style={styles.helpUrl}
href={item.helpUrl}
target="_blank"
>
More info...
</a>
</div>
)
}
Info.propTypes = {
item: PropTypes.object,
};
export default Info;

View File

@ -0,0 +1,63 @@
import React, { Component, PropTypes } from 'react';
import Info from './Info';
import Tags from './Tags';
import Elements from './Elements';
const styles = {
item: {
padding: '0 14px',
cursor: 'pointer',
borderBottom: '1px solid rgb(234, 234, 234)',
},
headerBar: {
margin: '12px 0',
display: 'block',
width: '100%',
},
}
class Item extends Component {
static propTypes = {
item: PropTypes.object,
passes: PropTypes.bool,
}
constructor() {
super();
this.state = {
open: false,
}
}
onToggle = () => this.setState((prevState) => ({
open: !prevState.open,
}))
render() {
const { item, passes } = this.props;
const { open } = this.state;
return (
<div style={styles.item}>
<div
style={styles.headerBar}
onClick={() => this.onToggle()}
>
{item.description}
</div>
{ open && (<Info item={item} />) }
{ open && (
<Elements
elements={item.nodes}
passes={passes}
/>
) }
{ open && (<Tags tags={item.tags} />) }
</div>
)
}
}
export default Item;

View File

@ -0,0 +1,77 @@
import React from 'react';
const impactColors = {
minor: '#f1c40f',
moderate: '#e67e22',
serious: '#e74c3c',
critical: '#c0392b',
success: '#2ecc71',
};
const styles = {
rules: {
display: 'flex',
flexDirection: 'column',
padding: '4px',
fontWeight: '400',
},
rule: {
display: 'flex',
flexDirection: 'row',
marginBottom: '6px',
},
status: {
height: '16px',
width: '16px',
borderRadius: '8px',
fontSize: '10px',
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
color: '#fff',
textAlign: 'center',
flex: '0 0 16px',
},
message: {
paddingLeft: '6px',
}
}
function Rule({ rule, passes }) {
const color = ( passes ?
impactColors.success :
impactColors[rule.impact]
)
return (
<div style={styles.rule}>
<div
style={{
...styles.status,
backgroundColor: color,
}}
>
{ passes ? '✔' : '✘' }
</div>
<span style={styles.message}>
{rule.message}
</span>
</div>
)
}
function Rules({ rules, passes }) {
return (
<div style={styles.rules}>
{rules.map((rule, index) => (
<Rule
passes={passes}
rule={rule}
key={index}
/>
))}
</div>
)
}
export default Rules;

View File

@ -0,0 +1,34 @@
import React from 'react';
const styles = {
tags: {
display: 'flex',
flexWrap: 'wrap',
margin: '12px 0',
},
tag: {
margin: '0 6px',
padding: '5px',
border: '1px solid rgb(234, 234, 234)',
borderRadius: '2px',
color: 'rgb(130, 130, 130)',
fontSize: '12px',
}
}
function Tags({ tags }) {
return (
<div style={styles.tags}>
{tags.map((tag) => (
<div
key={tag}
style={styles.tag}
>
{tag}
</div>
))}
</div>
);
}
export default Tags;

View File

@ -0,0 +1,44 @@
import React, { Component, PropTypes } from 'react';
import Item from './Item';
const styles = {
container: {
fontFamily: '-apple-system, ".SFNSText-Regular", "San Francisco", Roboto, "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif',
fontSize: '12px',
},
empty: {
fontFamily: '-apple-system, ".SFNSText-Regular", "San Francisco", Roboto, "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif',
fontSize: '11px',
padding: '20px 12px',
width: '100%',
display: 'block',
textAlign: 'center',
textTransform: 'uppercase',
}
}
function Report({ items, empty, passes }) {
if (items.length) {
return (
<div style={styles.container}>
{items.map((item) => (
<Item
passes={passes}
item={item}
key={item.id}
/>
))}
</div>
);
}
return (<span style={styles.empty}>{empty}</span>)
}
Report.propTypes = {
items: PropTypes.array,
empty: PropTypes.string,
}
export default Report;

View File

@ -0,0 +1,101 @@
import React, { Component, PropTypes } from 'react';
const styles = {
container: {
width: '100%',
},
tabs: {
borderBottom: '1px solid rgb(234, 234, 234)',
flexWrap: 'wrap',
display: 'flex',
},
tab: {
fontFamily: '-apple-system, ".SFNSText-Regular", "San Francisco", Roboto, "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif',
color: 'rgb(68, 68, 68)',
fontSize: '11px',
textDecoration: 'none',
textTransform: 'uppercase',
padding: '10px 15px',
letterSpacing: '1px',
cursor: 'pointer',
fontWeight: 500,
opacity: 0.7,
},
tabActive: {
opacity: 1,
fontWeight: 600,
}
}
class Tabs extends Component {
static propTypes = {
tabs: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.element,
panel: PropTypes.element,
})),
}
constructor(props) {
super(props);
this.state = {
active: 0,
}
this.onToggle = this.onToggle.bind(this);
this.renderPanel = this.renderPanel.bind(this);
this.renderTabs = this.renderTabs.bind(this);
}
onToggle(index) {
this.setState({
active: index,
})
}
renderPanel() {
const { tabs } = this.props;
const { active } = this.state;
return (
<div style={styles.panel}>
{tabs[active].panel}
</div>
)
}
renderTabs() {
const { tabs } = this.props;
const { active } = this.state;
return (
<div style={styles.tabs}>
{tabs.map((tab, index) => (
<div
key={index}
style={{
...styles.tab,
...(index === active ? styles.tabActive : undefined)
}}
onClick={() => this.onToggle(index)}
>
{ tab.label }
</div>
))}
</div>
)
}
render() {
const { tabs } = this.props;
return (
<div style={styles.container}>
{this.renderTabs()}
{this.renderPanel()}
</div>
);
}
}
export default Tabs;

View File

@ -0,0 +1,30 @@
import React, { Component, PropTypes } from 'react';
import axe from 'axe-core';
class WrapStory extends Component {
static propTypes = {
context: PropTypes.object,
storyFn: PropTypes.func,
channel: PropTypes.object,
}
componentDidMount() {
const { channel } = this.props;
axe.a11yCheck(this.wrapper, {}, (results) => {
channel.emit('addon:a11y:check', results);
});
}
render() {
const { storyFn, context } = this.props;
return (<span
ref={ (container) => { this.wrapper = container; } }
>
{storyFn(context)}
</span>)
}
}
export default WrapStory;

11
addons/a11y/src/index.js Normal file
View File

@ -0,0 +1,11 @@
import addons from '@kadira/storybook-addons';
import A11yManager from './A11yManager';
const manager = new A11yManager();
function checkA11y(storyFn, context) {
const channel = addons.getChannel();
return manager.wrapStory(channel, storyFn, context);
}
export { checkA11y };

View File

@ -0,0 +1,18 @@
import React from 'react';
import addons from '@kadira/storybook-addons';
import Panel from './components/Panel';
import { ADDON_ID, PANEL_ID } from './shared';
function init() {
addons.register(ADDON_ID, api => {
addons.addPanel(PANEL_ID, {
title: 'Accessibility',
render() {
return <Panel />;
}
});
});
}
export { init }

View File

@ -0,0 +1,4 @@
// addons, panels and events get unique names using a prefix
export const ADDON_ID = 'jbovenschen/storybook-addon-a11y';
export const PANEL_ID = `${ADDON_ID}/addon-panel`;
export const EVENT_ID = `${ADDON_ID}/addon-event`;