├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── .eslintrc
├── App.css
├── App.js
├── AppMain.js
├── AppNavigation.js
├── Contact.js
├── Todo.js
├── Todos.js
├── components
│ ├── LabelledInput.js
│ └── PageFocusSection.js
├── data
│ └── todo.js
├── id24.jpg
├── index.js
└── registerServiceWorker.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | .idea
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Companion application for React Accessibility Patterns Talk
2 |
3 | ## Application features
4 |
5 | - Accessible form elements
6 | - Accessible React Router transitions
7 | - ARIA live regions
8 | - Skiplinks
9 |
10 | ## Running the application
11 |
12 | Clone the repository and install the `npm` packages and then start it.
13 |
14 | With `npm`:
15 | ```
16 | npm install
17 | npm run start
18 | ```
19 |
20 | With `yarn`:
21 | ```
22 | yarn
23 | yarn start
24 | ```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-a11y-patterns",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "bootstrap": "3.3.7",
7 | "prettier": "1.10.2",
8 | "react": "16.2.0",
9 | "react-aria-live": "1.0.4",
10 | "react-axe": "2.1.9",
11 | "react-document-title": "2.0.3",
12 | "react-dom": "16.2.0",
13 | "react-router-dom": "4.2.0",
14 | "react-scripts": "1.1.0",
15 | "uuid": "3.1.0"
16 | },
17 | "scripts": {
18 | "start": "react-scripts start",
19 | "build": "react-scripts build",
20 | "test": "react-scripts test --env=jsdom",
21 | "eject": "react-scripts eject",
22 | "format": "prettier --write --single-quote --tab-width=2 --trailing-comma=es5 --jsx-bracket-same-line src/**/*.js"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlmeroSteyn/react-a11y-patterns/01f14ffb2764f835703573567940a3346f447a68/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | #ID24-Demo
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["react-app", "plugin:jsx-a11y/recommended"],
3 | "plugins": ["jsx-a11y"]
4 | }
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 80px;
7 | float: left;
8 | margin-right: 11px;
9 | }
10 |
11 | .jumbotron {
12 | padding-top: 10px;
13 | padding-bottom: 15px;
14 | }
15 |
16 | .App-header {
17 | background-color: #222;
18 | height: 150px;
19 | padding: 20px;
20 | color: white;
21 | }
22 |
23 | .App-intro {
24 | font-size: large;
25 | }
26 |
27 | @keyframes App-logo-spin {
28 | from {
29 | transform: rotate(0deg);
30 | }
31 | to {
32 | transform: rotate(360deg);
33 | }
34 | }
35 |
36 | .nav > li > a {
37 | text-decoration: underline;
38 | }
39 |
40 | .nav > li > a.active {
41 | color: white;
42 | cursor: default;
43 | background-color: darkblue;
44 | border: 1px solid #ddd;
45 | border-bottom-color: transparent;
46 | }
47 |
48 | .nav > li > a.active:focus, .nav > li > a.active:hover {
49 | color: #23527c;
50 | cursor: default;
51 | background-color: #eeeeee;
52 | border: 1px solid #ddd;
53 | border-bottom-color: transparent;
54 | }
55 |
56 | .border-devide {
57 | border-left-width: 1px;
58 | border-left-color: black;
59 | border-left-style: solid;
60 | }
61 |
62 | .skip {
63 | position: absolute;
64 | top: -1000px;
65 | left: -1000px;
66 | height: 1px;
67 | width: 1px;
68 | text-align: left;
69 | overflow: hidden;
70 | }
71 | .skip:focus, .skip:active {
72 | left: 30px;
73 | top: 30px;
74 | width: auto;
75 | height: auto;
76 | overflow: visible;
77 | z-index: 999;
78 | padding: 15px 30px;
79 | background-color: #e5e5e5;
80 | }
81 |
82 | div:focus, section:focus, h2:focus {
83 | outline: none;
84 | }
85 |
86 | .btn-primary {
87 | margin-top: 10px;
88 | margin-left: 10px;
89 | }
90 |
91 | .btn-primary:focus, .btn-primary:hover {
92 | color: white;
93 | background-color: darkblue;
94 | }
95 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { LiveAnnouncer } from 'react-aria-live';
3 | import { BrowserRouter as Router } from 'react-router-dom';
4 | import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
5 | import './App.css';
6 | import AppMain from './AppMain';
7 | import AppNavigation from './AppNavigation';
8 |
9 | class App extends Component {
10 | render() {
11 | return (
12 |
13 |
14 |
43 |
44 |
45 | );
46 | }
47 | }
48 |
49 | export default App;
50 |
--------------------------------------------------------------------------------
/src/AppMain.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Switch, Redirect } from 'react-router-dom';
3 | import Todo from './Todo';
4 | import Todos from './Todos';
5 | import Contact from './Contact';
6 |
7 | const AppMain = () => (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 |
18 | export default AppMain;
19 |
--------------------------------------------------------------------------------
/src/AppNavigation.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | const AppNavigation = () => (
5 |
32 | );
33 |
34 | export default AppNavigation;
35 |
--------------------------------------------------------------------------------
/src/Contact.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PageFocusSection from './components/PageFocusSection';
3 |
4 | const Contact = () => (
5 |
9 |
10 | -
11 | Email: demo@todo.nl
12 |
13 | - Phone: +31 6 00 00 00 01
14 |
15 |
16 | );
17 |
18 | export default Contact;
19 |
--------------------------------------------------------------------------------
/src/Todo.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import LabelledInput from './components/LabelledInput';
4 | import PageFocusSection from './components/PageFocusSection';
5 | import { addTodo } from './data/todo';
6 |
7 | class Todo extends Component {
8 | state = {
9 | todoName: '',
10 | todoDescription: '',
11 | showErrors: false,
12 | liveMessage: 'Add todo page loaded.',
13 | };
14 |
15 | onSubmitHandler = e => {
16 | const { todoName, todoDescription } = this.state;
17 | const canSubmit = !!todoName && !!todoDescription;
18 |
19 | e.preventDefault();
20 | this.setState({ showErrors: true });
21 |
22 | if (canSubmit) {
23 | addTodo({ todoName, todoDescription });
24 | this.setState(
25 | {
26 | liveMessage: `Todo ${todoName} successfully added.`,
27 | },
28 | () => {
29 | setTimeout(() => {
30 | this.props.history.replace({
31 | pathname: '/todos',
32 | state: { setFocus: true },
33 | });
34 | }, 50);
35 | }
36 | );
37 | }
38 | };
39 |
40 | onChangeHandler = e => {
41 | this.setState({ [e.target.name]: e.target.value });
42 | };
43 |
44 | render() {
45 | const { todoName, todoDescription, showErrors, liveMessage } = this.state;
46 | const canSubmit = !!todoName && !!todoDescription;
47 |
48 | return (
49 |
53 |
89 |
90 | );
91 | }
92 | }
93 |
94 | export default Todo;
95 |
--------------------------------------------------------------------------------
/src/Todos.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import PageFocusSection from './components/PageFocusSection';
4 | import { getTodos } from './data/todo';
5 |
6 | class Todos extends Component {
7 | state = {
8 | todos: [],
9 | };
10 |
11 | componentDidMount() {
12 | this.setState({ todos: getTodos() });
13 | }
14 |
15 | render() {
16 | const { todos } = this.state;
17 | return (
18 |
22 |
23 |
24 |
25 | {todos.map(todo => (
26 | - {`${todo.name} - ${todo.description}`}
27 | ))}
28 |
29 |
30 |
31 |
34 | Add new todo
35 |
36 |
37 |
38 |
39 | );
40 | }
41 | }
42 |
43 | export default Todos;
44 |
--------------------------------------------------------------------------------
/src/components/LabelledInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import uuid from 'uuid';
3 |
4 | class LabelledInput extends Component {
5 | inputId = uuid.v4();
6 | describedById = uuid.v4();
7 |
8 | onChangeHandler = e => {
9 | this.props.onChange(e);
10 | };
11 |
12 | render() {
13 | const { value, labelText, name, errorText, showErrors } = this.props;
14 | const hasValue = !!value;
15 |
16 | return (
17 |
18 |
19 |
30 |
35 | {!hasValue && showErrors ? (
36 |
37 |
41 | {errorText}
42 |
43 | ) : null}
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | export default LabelledInput;
51 |
--------------------------------------------------------------------------------
/src/components/PageFocusSection.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { LiveMessage } from 'react-aria-live';
3 | import DocumentTitle from 'react-document-title';
4 | import { withRouter } from 'react-router-dom';
5 |
6 | class PageFocusSection extends Component {
7 | componentDidMount() {
8 | const { location } = this.props;
9 | if (location.state && location.state.setFocus) {
10 | this.header.focus();
11 | }
12 | }
13 |
14 | render() {
15 | const {
16 | docTitle,
17 | liveMessage,
18 | children,
19 | headingText,
20 | headingLevel,
21 | } = this.props;
22 | const HeaderLevel = `h${headingLevel ? headingLevel : '2'}`;
23 | return (
24 |
25 |
26 |
27 |
28 | (this.header = header)}>
29 | {headingText}
30 |
31 | {children}
32 |
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | export default withRouter(PageFocusSection);
40 |
--------------------------------------------------------------------------------
/src/data/todo.js:
--------------------------------------------------------------------------------
1 | let todos = [
2 | {
3 | id: 1,
4 | name: 'Eggs',
5 | description: 'Buy at Tesco',
6 | },
7 | {
8 | id: 2,
9 | name: 'Dog',
10 | description: 'Remember to walk Miffles',
11 | },
12 | ];
13 |
14 | export const getTodos = () => todos;
15 |
16 | export const addTodo = newTodo => {
17 | const newId = todos.reduce((maxId, todo) => {
18 | return todo.id > maxId ? todo.id : maxId;
19 | }, 0);
20 | todos.push({
21 | id: newId + 1,
22 | name: newTodo.todoName,
23 | description: newTodo.todoDescription,
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/src/id24.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlmeroSteyn/react-a11y-patterns/01f14ffb2764f835703573567940a3346f447a68/src/id24.jpg
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import registerServiceWorker from './registerServiceWorker';
5 |
6 | if (process.env.NODE_ENV !== 'production') {
7 | const axe = require('react-axe');
8 | axe(React, ReactDOM, 1000);
9 | }
10 |
11 | ReactDOM.render(, document.getElementById('root'));
12 | registerServiceWorker();
13 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (!isLocalhost) {
36 | // Is not local host. Just register service worker
37 | registerValidSW(swUrl);
38 | } else {
39 | // This is running on localhost. Lets check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------