├── .gitignore
├── LICENSE
├── README.md
├── cypress.json
├── cypress
├── fixtures
│ └── example.json
├── integration
│ └── index.js
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ └── index.js
├── examples
├── client-side-routing
│ ├── components
│ │ ├── header.js
│ │ └── route-target-heading.js
│ ├── gatsby-browser.js
│ └── pages
│ │ └── page.jsx
├── dropdown
│ └── dropdown.js
├── integration-testing
│ ├── nav.spec.js
│ └── routing.js
├── live-region
│ └── live-region.js
├── progressive-enhancement
│ └── tab-list.js
├── reduced-motion
│ ├── animation.js
│ └── card-flip.scss
└── unit-testing
│ └── dropdown.test.js
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── jest-preprocess.js
├── jest.config.js
├── jest.setup.js
├── loadershim.js
├── outline.yml
├── package-lock.json
├── package.json
├── src
├── __mocks__
│ └── file-mock.js
├── components
│ ├── bad
│ │ ├── animation.js
│ │ ├── async-form.js
│ │ ├── card-flip.js
│ │ ├── card-flip.scss
│ │ ├── dropdown.js
│ │ └── dropdown.scss
│ ├── better
│ │ ├── __tests__
│ │ │ └── dropdown.test.js
│ │ ├── animation.js
│ │ ├── card-flip.js
│ │ ├── card-flip.scss
│ │ ├── downshift-autocomplete.js
│ │ ├── downshift-utils.js
│ │ ├── dropdown.js
│ │ ├── dropdown.scss
│ │ ├── live-region.js
│ │ ├── reduced-motion.js
│ │ ├── route-target-heading.js
│ │ └── tab-list.js
│ └── site-chrome
│ │ ├── code-figure.jsx
│ │ ├── header.js
│ │ ├── image.js
│ │ ├── layout.js
│ │ ├── layout.scss
│ │ ├── navigation.js
│ │ ├── navigation.module.scss
│ │ └── seo.js
├── css
│ └── reset.css
├── images
│ ├── bagley.jpg
│ ├── blueicon.jpg
│ └── rainier-headshot.jpg
├── pages
│ ├── 404.js
│ ├── animation.jsx
│ ├── announcer.jsx
│ ├── dropdown.jsx
│ ├── enhanced-tablist.jsx
│ ├── index.jsx
│ ├── layout.jsx
│ └── semantics.jsx
├── slides
│ └── index.mdx
├── templates
│ ├── image-slide.jsx
│ └── slide.jsx
└── theme.js
└── static
├── a11y-octopus.png
├── aea-grid-inspect.png
├── angular-logo.png
├── announcement.webp
├── axe-logo.png
├── chrome-a11y-inspector.png
├── chrome-accessibility-tree.png
├── ci-bandits.jpg
├── elmo-shrug.gif
├── finally.webp
├── focus-diagram.png
├── gatsby-icon.png
├── keyboard.gif
├── magnification-a11y.png
├── mail.gif
├── marcysutton-sketch.png
├── mythbusters.png
├── paul-adam-aria.png
├── react-logo.png
└── safari-settings.png
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # dotenv environment variables file
55 | .env
56 |
57 | # gatsby files
58 | .cache/
59 | public
60 |
61 | # Mac files
62 | .DS_Store
63 |
64 | # Yarn
65 | yarn-error.log
66 | .pnp/
67 | .pnp.js
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 gatsbyjs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Accessibility in JavaScript Applications
2 |
3 | Learn the necessary techniques and tools for building inclusive web applications with JavaScript from Marcy Sutton.
4 |
5 | Presented at Frontend Masters! You can view the videos that go along with the workshop material in the [Accessibility in JavaScript Application course](https://frontendmasters.com/courses/javascript-accessibility/).
6 |
7 | Some key takeaways:
8 |
9 | - Understand how to incorporate accessibility into your web development workflow.
10 | - Debug your sites and applications for accessibility using the latest tools.
11 | - Apply accessibility to React web applications with Gatsby, while learning how accessibility applies to other stacks.
12 | - Learn the benefits of manual and automated testing to grow web accessibility superpowers!
13 | - Integrate focus management into your web applications, gracefully handling keyboard and screen reader interactions.
14 | - Practice announcing view changes with your code and keeping screen reader users up to date.
15 | - Achieve wins with semantic markup, unobtrusive animation, and progressive enhancement.
16 |
17 | App URL: https://marcysutton.github.io/js-a11y-workshop
18 |
19 | Workshop slides: https://marcysutton.github.io/js-a11y-workshop/slides/
20 |
21 | ---
22 |
23 | This project was started with [gatsby-starter-mdx-basic](https://github.com/christopherbiscardi/gatsby-starter-mdx-basic) and [@mdx-deck/theme](https://github.com/jxnblk/mdx-deck/tree/master/packages/gatsby-theme).
24 |
25 | _Note: This repo requires [Node 12 and npm](https://nodejs.org) to be installed._
26 |
27 | 1. Create a new Gatsby site and slide deck using this starter
28 |
29 | ```sh
30 | git clone https://github.com/marcysutton/js-a11y-workshop
31 | ```
32 |
33 | 2. Go into the directory, install dependencies, and start the development server
34 |
35 | ```sh
36 | cd js-a11y-workshop
37 | npm install
38 | npm start
39 | ```
40 |
41 | View in a browser: http://localhost:8000
42 |
43 | 3. Edit files:
44 |
45 | - Site pages: [`src/pages/*`](https://github.com/marcysutton/js-a11y-workshop/blob/master/src/pages)
46 | - Site components: [`src/components/*`](https://github.com/marcysutton/js-a11y-workshop/blob/master/src/components)
47 | - Templates: [`src/templates/*`](https://github.com/marcysutton/js-a11y-workshop/blob/master/src/templates)
48 | - Slide content: [`src/slides/index.mdx`](https://github.com/marcysutton/js-a11y-workshop/blob/master/src/slides/index.mdx)
49 |
50 | 4. To look at the answers from the exercises, check out the [`/examples`](https://github.com/marcysutton/js-a11y-workshop/blob/master/examples) directory in the `master` branch
51 |
52 | You can also check out the `solutions` branch to see everything in action: https://github.com/marcysutton/js-a11y-workshop/tree/solutions
53 |
54 | ### Prerequisites
55 |
56 | 1. Have a text editor installed, i.e. VSCode
57 | 2. Have the Gatsby CLI (gatsby-cli) installed globally by running ```npm install -g gatsby-cli```
58 | 3. Use Node 12 ([nvm](https://github.com/nvm-sh/nvm) or [n](https://github.com/tj/n) will help you maintain Node versions)
59 |
60 | ### Troubleshooting
61 |
62 | If you run into errors on the install, check your Node version for version 12. Sharp in particular likes to complain a lot.
63 |
64 | To get Gatsby to install after errors, try deleting files and doing another reinstall:
65 |
66 | ```
67 | rm -rf node_modules && rm package-lock.json
68 | npm install
69 | ```
70 |
71 | If you still run into problems, let me know by [opening an issue](https://github.com/marcysutton/js-a11y-workshop/issues/new) and including details on any errors, Node version, platform, etc.
72 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
--------------------------------------------------------------------------------
/cypress/integration/index.js:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | module.exports = (on, config) => {
15 | // `on` is used to hook into various events Cypress emits
16 | // `config` is the resolved Cypress config
17 | }
18 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 |
11 | import "cypress-axe"
12 | import "@testing-library/cypress/add-commands"
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/examples/client-side-routing/components/header.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'gatsby'
2 | import PropTypes from 'prop-types'
3 | import React from 'react'
4 |
5 | const Header = ({ siteTitle }) => (
6 |
7 |
10 |
11 |
14 | {siteTitle}
15 |
16 |
17 |
18 | )
19 |
20 | Header.propTypes = {
21 | siteTitle: PropTypes.string,
22 | }
23 |
24 | Header.defaultProps = {
25 | siteTitle: '',
26 | }
27 |
28 | export default Header
29 |
--------------------------------------------------------------------------------
/examples/client-side-routing/components/route-target-heading.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { css } from "@emotion/core"
3 |
4 | const styles = css`
5 | .routeSkipHeading {
6 | position: relative;
7 | }
8 | .routeSkipLink {
9 | display: inline-block;
10 | margin-left: -0.75em;
11 | opacity: 0;
12 | position: absolute;
13 | text-decoration: none;
14 | }
15 | .routeSkipLink:before {
16 | content: '⇽';
17 | display: block;
18 | }
19 | .routeSkipLink:focus,
20 | .routeSkipLink:hover {
21 | opacity: 1;
22 | }
23 | `
24 | const RouteHeading = ({level = 1, targetID, children}) => {
25 | const Heading = `h${level}`;
26 | return (
27 |
28 |
32 |
33 | {children}
34 |
35 | )
36 | }
37 | export default RouteHeading
--------------------------------------------------------------------------------
/examples/client-side-routing/gatsby-browser.js:
--------------------------------------------------------------------------------
1 | exports.onRouteUpdate = ({ location, prevLocation }) => {
2 | if (prevLocation !== null) {
3 | const skipLink = document.querySelector('.routeSkipLink')
4 | if (skipLink) {
5 | skipLink.focus()
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/examples/client-side-routing/pages/page.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import Layout from '../components/site-chrome/layout'
4 | import SEO from '../components/site-chrome/seo'
5 |
6 | import RouteTargetHeading from "./route-target-heading.js"
7 |
8 | const HeadingDemoPage = () => {
9 | return (
10 |
11 |
12 |
13 |
17 | Heading Demo
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | export default HeadingDemoPage
25 |
26 |
27 |
--------------------------------------------------------------------------------
/examples/dropdown/dropdown.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from "react"
2 | import uuid from "uuid"
3 |
4 | import "./dropdown.scss"
5 |
6 | const Dropdown = ({ activatorText = 'Dropdown', items = [] }) => {
7 | const [isOpen, setIsOpen] = useState(false)
8 | const activatorRef = useRef(null)
9 | const dropdownListRef = useRef(null)
10 |
11 | const wrapKeyHandler = (event) => {
12 | if (event.key === 'Escape' && isOpen) {
13 | // escape key
14 | setIsOpen(false)
15 | activatorRef.current.focus()
16 | }
17 | }
18 | const clickHandler = () => {
19 | setIsOpen(!isOpen)
20 | }
21 | const clickOutsideHandler = (event) => {
22 | if (dropdownListRef.current.contains(event.target) || activatorRef.current.contains(event.target)) {
23 | return
24 | }
25 | setIsOpen()
26 | }
27 | useEffect(() => {
28 | if (isOpen) {
29 | document.addEventListener('mouseup', clickOutsideHandler)
30 |
31 | dropdownListRef.current.querySelector('a').focus()
32 | } else {
33 | document.removeEventListener('mouseup', clickOutsideHandler)
34 | }
35 |
36 | return () => {
37 | document.removeEventListener('mouseup', clickOutsideHandler)
38 | }
39 | }, [isOpen])
40 | return (
41 |
45 |
52 | {activatorText}
53 |
54 |
60 | { items.map((item, index) => {
61 | return
62 | {item.text}
63 |
64 | })}
65 | { items.length === 0 ? No items : null }
66 |
67 |
68 | )
69 | }
70 | export default Dropdown
71 |
--------------------------------------------------------------------------------
/examples/integration-testing/nav.spec.js:
--------------------------------------------------------------------------------
1 | context("Nav menu", () => {
2 | beforeEach(() => {
3 | cy.visit(`https://marcysutton.github.io/js-a11y-workshop`)
4 | cy.injectAxe()
5 | cy.wait(100)
6 | })
7 | it("has no accessibility violations on load", () => {
8 | cy.checkA11y()
9 | })
10 | it("has a focusable, labeled button", () => {
11 | cy.get("[aria-label='Open menu']").focus()
12 | cy.focused().should("have.attr", "aria-label")
13 | })
14 | })
--------------------------------------------------------------------------------
/examples/integration-testing/routing.js:
--------------------------------------------------------------------------------
1 | ///
2 | describe("Accessibility checks", () => {
3 | beforeEach(() => {
4 | })
5 | it("Has no detectable a11y violations on load", () => {
6 | cy.visit("http://localhost:8000")
7 | cy.injectAxe()
8 | cy.wait(500)
9 | cy.checkA11y()
10 | })
11 | it("Handles focus on route change via click", () => {
12 | cy.visit("http://localhost:8000")
13 | cy.focused()
14 | .should("not.have.class", "routeSkipLink")
15 |
16 | cy.get('#page-navigation').find('a').eq(0).click()
17 |
18 | cy.focused()
19 | .should("have.class", "routeSkipLink")
20 | })
21 | })
--------------------------------------------------------------------------------
/examples/live-region/live-region.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useRef} from "react"
2 |
3 | const LiveRegion = () => {
4 | const [message, setMessage] = useState('')
5 | const inputRef = useRef(null)
6 |
7 | const start = () => {
8 | let typedMessage = inputRef.current.value
9 | if (typedMessage.trim().length) {
10 | setMessage(typedMessage)
11 | }
12 | }
13 | const submitHandler = (event) => {
14 | event.preventDefault()
15 | start()
16 | }
17 | return (
18 |
30 | )
31 | }
32 |
33 | export default LiveRegion
--------------------------------------------------------------------------------
/examples/progressive-enhancement/tab-list.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 |
3 | const TabList = ({ items = [] }) => {
4 | const [isClient, setClient] = useState(false)
5 | /*
6 | * Think of this as componentDidMount
7 | * on the client, it will run once and update state
8 | */
9 | useEffect(() => {
10 | setClient(true)
11 | }, [])
12 | return (
13 |
14 | {items.map(item => (
15 |
18 | {item.label}
19 |
20 | ))}
21 |
22 | )
23 | }
24 |
25 | export default TabList
--------------------------------------------------------------------------------
/examples/reduced-motion/animation.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useEffect } from "react"
2 | import { css } from "@emotion/core"
3 | import styled from "@emotion/styled"
4 |
5 | const noAnimate = css`
6 | .pulse, circle {
7 | animation: none !important;
8 | }
9 | .pulse circle {
10 | opacity: 0.75;
11 | transform: scale(0.25);
12 | }
13 | .pulse circle:nth-child(2) {
14 | opacity: 0.25;
15 | transform: scale(0.75);
16 | }
17 | .pulse circle:nth-child(3) {
18 | opacity: 0.5;
19 | transform: scale(0.5);
20 | }
21 | `
22 | const Demo = styled.div`
23 | .animationToggle {
24 | background-color: black;
25 | border: none;
26 | border-radius: 0;
27 | color: white;
28 | font-size: 1rem;
29 | padding: 0.5em 1em;
30 | }
31 | .animationTarget {
32 | height: 400px;
33 | margin: 1em auto;
34 | overflow: hidden;
35 | position: relative;
36 | width: 400px;
37 | }
38 | .pulse {
39 | z-index: 1;
40 | position: absolute;
41 | top: 50%;
42 | left: 50%;
43 | transform: translate(-50%, -50%);
44 | max-width: 30rem;
45 |
46 | circle {
47 | fill: #ff5154;
48 | transform: scale(0);
49 | opacity: 0.4;
50 | transform-origin: 50% 50%;
51 | animation: pulse 2.5s cubic-bezier(.5,.5,0,1) infinite;
52 |
53 | &:nth-child(2) {
54 | fill: #7fc6a4;
55 | animation: pulse 2.5s 0.75s cubic-bezier(.5,.5,0,1) infinite;
56 | }
57 |
58 | &:nth-child(3) {
59 | fill: #e5f77d;
60 | animation: pulse 2.5s 1.5s cubic-bezier(.5,.5,0,1) infinite;
61 | }
62 |
63 | }
64 |
65 | }
66 |
67 | @keyframes pulse {
68 | 25% {
69 | opacity: 0.4;
70 | }
71 |
72 | 100% {
73 | transform: scale(1);
74 | opacity: 0;
75 | }
76 | }
77 |
78 |
79 | .no-animate {
80 | ${noAnimate}
81 | }
82 |
83 | /* if reduced-motion is selected on OSX/iOS */
84 | @media (prefers-reduced-motion) {
85 | /* hide toggle button */
86 | .animationToggle {
87 | display: none;
88 | }
89 | /* make sure animations actually stop */
90 | ${noAnimate}
91 | }
92 | `
93 |
94 | const AccessibleAnimationDemo = () => {
95 | let [animating, setAnimating] = useState(false)
96 | let [toggleText, setToggleText] = useState('on')
97 |
98 | const animationTarget = useRef()
99 | const animationToggle = useRef()
100 |
101 | useEffect(() => {
102 | if (localStorage.getItem('animating') === 'false') {
103 | disableAnimation()
104 | }
105 | }, [animating])
106 |
107 | function toggleBtnHandler(event) {
108 | if (animating) {
109 | disableAnimation()
110 | } else {
111 | enableAnimation()
112 | }
113 | }
114 | function disableAnimation() {
115 | setToggleText('on')
116 | setAnimating(false)
117 | localStorage.setItem('animating', 'false')
118 | }
119 | function enableAnimation() {
120 | setToggleText('off')
121 | setAnimating(true)
122 | localStorage.setItem('animating', 'true')
123 | }
124 | return (
125 |
126 |
131 |
132 | Animating circles
133 |
134 |
135 |
136 |
137 |
138 |
139 |
144 | Turn {toggleText} animation
145 |
146 |
147 |
148 | )
149 | }
150 |
151 | export default AccessibleAnimationDemo
152 |
--------------------------------------------------------------------------------
/examples/reduced-motion/card-flip.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 | .team-member {
5 | display: inline-block;
6 | position: relative;
7 | }
8 | .team-content {
9 | padding: 0;
10 | position: relative;
11 | z-index: 1;
12 | margin-top: 0;
13 | margin-bottom: 0px;
14 | width: 320px;
15 | text-align: center;
16 | }
17 | .team-content button {
18 | background-color: transparent;
19 | border: none;
20 | color: currentColor;
21 | -webkit-appearance: none;
22 | }
23 | .team-content .toggle-button * {
24 | text-decoration: underline;
25 | }
26 | .team-image {
27 | display: block;
28 | width: 190px;
29 | height: 190px;
30 | overflow: hidden;
31 | margin: auto;
32 | margin-top: 60px;
33 | margin-bottom: 20px;
34 | border-radius: 90px;
35 | box-shadow: 0 6px 20px rgba(0, 0, 0, 0.07);
36 | text-align: center;
37 | }
38 | .team-content .team-name {
39 | display: block;
40 | font-size: 18px;
41 | font-weight: 700;
42 | line-height: 1;
43 | margin: 0;
44 | margin-bottom: 10px;
45 | }
46 | .team-content .team-subtitle {
47 | margin-bottom: 0px;
48 | display: block;
49 | font-size: 14px;
50 | font-style: italic;
51 | line-height: 1;
52 | color: inherit;
53 | }
54 | .team-member .team-image img {
55 | margin: 0 auto;
56 | border: none;
57 | height: auto;
58 | width: 100%;
59 | }
60 | .team-content-overlay {
61 | background-color: #1c6300;
62 | color: #fff;
63 | top: 0;
64 | padding: 40px 35px;
65 | position: absolute;
66 | width: 190px;
67 | overflow: hidden;
68 | min-height: 190px;
69 | max-height: 190px;
70 | height: auto;
71 | left: 60px;
72 | opacity: 0.5;
73 | visibility: hidden;
74 | -webkit-transition: 0.25s ease-in;
75 | -o-transition: 0.25s ease-in;
76 | transition: 0.25s ease-in;
77 | z-index: 100;
78 | border-radius: 90px;
79 | }
80 | .active .team-content-overlay {
81 | opacity: 1;
82 | visibility: visible;
83 | margin-left: calc(95px - 50%);
84 | margin-top: 30px;
85 | max-height: 400px;
86 | width: 100%;
87 | height: auto;
88 | border-radius: 10px;
89 | }
90 | .team-content-overlay .gradient-overlay {
91 | position: absolute;
92 | width: 100%;
93 | height: 100%;
94 | z-index: 0;
95 | top: 0;
96 | left: 0;
97 | }
98 | .team-content .team-content-overlay .team-name {
99 | font-size: 18px;
100 | font-weight: 700;
101 | position: relative;
102 | }
103 | .team-content .team-content-overlay .team-subtitle {
104 | position: relative;
105 | }
106 | .team-close-button {
107 | border: none;
108 | font-weight: bold;
109 | position: absolute;
110 | top: 10px;
111 | right: 10px;
112 |
113 | &:focus {
114 | outline: 3px solid lightblue;
115 | }
116 | }
117 | .team-content-overlay p {
118 | color: #fff;
119 | opacity: 0;
120 | font-size: 8px;
121 | font-weight: 400;
122 | line-height: 22px;
123 | position: relative;
124 | font-size: 12px;
125 | // -webkit-transition: 0.25s ease-in;
126 | // -o-transition: 0.25s ease-in;
127 | // transition: 0.25s ease-in;
128 | }
129 | .active .team-content-overlay p {
130 | opacity: 1;
131 | font-size: 12px;
132 | }
133 | .team-socials {
134 | position: relative;
135 | margin: auto;
136 | margin-top: 45px;
137 | width: 100%;
138 | z-index: 101;
139 | text-align: center;
140 | line-height: 12px;
141 | }
142 | .team-socials a {
143 | color: #225165;
144 | outline: 0;
145 | text-decoration: none;
146 | }
147 | .team-socials svg {
148 | margin: 0 10px;
149 | width: 25px;
150 | }
151 | @media (prefers-reduced-motion) {
152 | .team-content-overlay {
153 | transition: opacity 0.25s ease-out;
154 | }
155 | .team-content-overlay p {
156 | transition: none;
157 | }
158 | }
--------------------------------------------------------------------------------
/examples/unit-testing/dropdown.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, fireEvent } from '@testing-library/react'
3 |
4 | import Dropdown from '../dropdown'
5 |
6 | describe(`Dropdown`, () => {
7 | it(`renders activatorText`, () => {
8 | const activatorText = `Hamburgers`
9 | const { getByText } = render( )
10 |
11 | const text = getByText(activatorText)
12 |
13 | expect(text).toBeInTheDocument()
14 | })
15 | it(`renders a focusable button that activates the dropdown`, () => {
16 | const activatorText = `Dogs`
17 | const items = [{
18 | text: 'item 1',
19 | url: '#'
20 | }, {
21 | text: 'item 2',
22 | url: '#'
23 | }]
24 | const dropdown = render( )
25 |
26 | const activator = dropdown.getByTestId('dropdown-activator')
27 | activator.focus()
28 |
29 | expect(activator).toHaveFocus()
30 |
31 | fireEvent.click(activator)
32 |
33 | const dropdownList = dropdown.getByTestId('dropdown-itemList')
34 |
35 | const firstAnchor = dropdownList.querySelector('a')
36 | expect(firstAnchor).toHaveFocus()
37 |
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/gatsby-browser.js:
--------------------------------------------------------------------------------
1 | exports.onRouteUpdate = ({ location, prevLocation }) => {
2 |
3 | }
--------------------------------------------------------------------------------
/gatsby-config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | siteMetadata: {
5 | title: `Accessibility in JavaScript Applications`,
6 | description:
7 | `Learn the necessary techniques and tools for building inclusive web applications with JavaScript.`,
8 | author: `@marcysutton`,
9 | },
10 | pathPrefix: "/js-a11y-workshop",
11 | plugins: [
12 | {
13 | resolve: `gatsby-theme-mdx-deck`,
14 | options: {
15 | // disable gatsby-mdx plugin – use this when your site already uses gatsby-mdx
16 | mdx: true,
17 | // source directory for decks
18 | contentPath: `src/slides`,
19 | basePath: '/slides',
20 | },
21 | },
22 | `gatsby-plugin-sass`,
23 | `gatsby-plugin-react-helmet`,
24 | {
25 | resolve: `gatsby-source-filesystem`,
26 | options: {
27 | name: `images`,
28 | path: `${__dirname}/src/images`,
29 | },
30 | },
31 | {
32 | resolve: `gatsby-source-filesystem`,
33 | options: {
34 | name: `pages`,
35 | path: `${__dirname}/src/pages`
36 | }
37 | },
38 | `gatsby-transformer-sharp`,
39 | `gatsby-plugin-sharp`,
40 | {
41 | resolve: `gatsby-plugin-manifest`,
42 | options: {
43 | name: `js-a11y-workshop`,
44 | short_name: `jsa11y`,
45 | start_url: `/`,
46 | background_color: `#663399`,
47 | theme_color: `#663399`,
48 | display: `minimal-ui`,
49 | icon: `src/images/blueicon.jpg`, // This path is relative to the root of the site.
50 | },
51 | },
52 | // this (optional) plugin enables Progressive Web App + Offline functionality
53 | // To learn more, visit: https://gatsby.app/offline
54 | // `gatsby-plugin-offline`,
55 | ],
56 | }
57 |
--------------------------------------------------------------------------------
/gatsby-node.js:
--------------------------------------------------------------------------------
1 | exports.onCreateBabelConfig = function onCreateBabelConfig({ actions }) {
2 | actions.setBabelPlugin({
3 | name: `@babel/plugin-proposal-export-default-from`
4 | })
5 | }
6 |
--------------------------------------------------------------------------------
/jest-preprocess.js:
--------------------------------------------------------------------------------
1 | const babelOptions = {
2 | presets: ["babel-preset-gatsby"],
3 | }
4 |
5 | module.exports = require("babel-jest").createTransformer(babelOptions)
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: {
3 | "^.+\\.jsx?$": `/jest-preprocess.js`,
4 | ".+\\.(css|styl|less|sass|scss)$": "jest-transform-css"
5 | },
6 | moduleNameMapper: {
7 | ".+\\.(css|styl|less|sass|scss)$": `identity-obj-proxy`,
8 | ".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": `/__mocks__/file-mock.js`,
9 | },
10 | testPathIgnorePatterns: [`node_modules`, `.cache`, `examples`],
11 | transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`],
12 | globals: {
13 | __PATH_PREFIX__: ``,
14 | },
15 | testURL: `http://localhost`,
16 | setupFiles: [`/loadershim.js`],
17 | setupFilesAfterEnv: ["/jest.setup.js"],
18 |
19 | }
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | require(`@testing-library/jest-dom/extend-expect`)
2 | require(`@testing-library/react/cleanup-after-each`)
--------------------------------------------------------------------------------
/loadershim.js:
--------------------------------------------------------------------------------
1 | global.___loader = {
2 | enqueue: jest.fn(),
3 | }
--------------------------------------------------------------------------------
/outline.yml:
--------------------------------------------------------------------------------
1 |
2 | Outline:
3 | - Introduction
4 | - Who is Marcy Sutton?
5 | - Overview of what we’ll cover
6 | - Examples of inaccessible web applications: show the pain
7 | - Examples of better web applications
8 | - Built HTML pages in Gatsby
9 | - Install demos and tools
10 | - Get people downloading the Gatsby demo app and NVDA if on Windows
11 | - Gatsby Demo app
12 | - Make sure everyone can install it and run the tests
13 | - Do a quick tour of the source code
14 | - Browser extensions / common testing tools
15 | - Accessibility debugging: using some of the intro examples
16 | - Rendering code in a browser for basic accessibility testing (quick explanation of web context / DOM vs. Node.js tooling)
17 | - Prototyping: budo
18 | - Gatsby demo app: starting a server with a bigger project
19 | - Discuss other rendering/localhost options, like http-server and Vue/Angular-specific things
20 | - Using the TAB key to check for keyboard support.
21 | - Apple OS setting (a common gotcha)
22 | - Visible focus outlines
23 | - What shouldn’t be interactive/operable with TAB?
24 | - EXERCISE: TAB through some webpages. Can you see where you are on the screen? Can you reach everything?
25 | - Known patterns / ARIA Authoring Practices Guide
26 | - Promote user testing with people with disabilities early and often
27 | - Chrome devtools color contrast debugger
28 | - Testing with the axe browser extensions for Chrome and Firefox
29 | - EXERCISE: Run an extension on a site
30 | - Screen reader testing overview
31 | - Recommended combinations: Voiceover and Safari, NVDA and Firefox, JAWS and IE11, iOS Safari + Voiceover, Android Firefox + Talkback, Narrator and Edge
32 | - Provide resources to learn more (cheat-sheets, training, etc.)
33 | - EXERCISE: use a screen reader (Mac Voiceover, iOS or Android, NVDA or Narrator for Windows)
34 | - Introduction to accessibility in JavaScript applications
35 | - Myth busting: accessibility + JavaScript can be BFFs.
36 | - Focus management requirements + patterns
37 | - Discuss focus manager APIs vs. components handling their own focus
38 | - Tabindex=”-1” vs. tabindex=”0”
39 | - React refs / other framework APIs
40 | - Use cases:
41 | - View changes
42 | - Focus in new layers, replacing on close
43 | - Disabling background layers
44 | - Handling removal of DOM nodes
45 | - Note known issues with iOS and other assistive technologies that can sometimes require workarounds (focus on buttons instead of headings, etc.)
46 | - Announcements using ARIA Live Regions
47 | - Use cases vary, there’s often more than one way to achieve something (discuss focus management vs. wrapping updating content in a live region, when you might not want to move focus, etc.)
48 | - Talk about Live Region requirements: must be rendered on page, but can be visually hidden
49 | - Sometimes two regions per “politeness” level are necessary to get everything to announce
50 | - A site-level announcement API might be the way to go
51 | - Async save / update / etc.
52 | - Title changes
53 | - Combobox usage / list filtering
54 | - Semantic HTML
55 | - Using semantics with JSX; verifying output
56 | - Putting landmarks into your app
57 | - Unintrusive Animation + prefers-reduced-motion
58 | - toggle button
59 | - OS-level setting
60 | - Progressive enhancement: accessible baselines, fallbacks, challenges with JS frameworks
61 | - Accessibility units: component-level API concerns
62 | - Automated unit testing approach
63 | - Test in isolation
64 | - Stub inputs/fixture data
65 | - Often headless
66 | - Fast changing of state
67 | - React Component examples with failing tests: combobox.
68 | - Accessible names (buttons, links, form controls)
69 | - TIPS: don’t use “click here” for links, consider adding visible labels for buttons
70 | - Keyboard interactions: escape key, arrow keys
71 | - Toggling ARIA states
72 | - EXERCISE: Choose a couple failing tests and make them pass
73 | - SOLUTION: Fix them together
74 | - Bonus info: configurable heading levels, passing in ARIA attributes with component DSLs (don’t make up ARIA attributes; use data attributes instead) etc.
75 | - Accessible pages
76 | - Integration/end-to-end testing approach
77 | - Real-world browser testing
78 | - Cypress vs. Selenium Webdriver
79 | - Document/page-level accessibility rules: page title, HTML lang, color contrast, heading hierarchy
80 | - Component interoperability
81 | - Framework testing flexibility
82 | - Accessibility testing APIs make more sense here as you can run an entire ruleset against each page
83 | - Gatsby example with failing tests
84 | - EXERCISE: Color contrast debugging with axe and Chrome devtools
85 | - EXERCISE: Fix failing focus management tests (showing component interop)
86 | - EXERCISE: get Cypress tests passing on a page
87 | - Q&A
88 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "js-a11y-workshop",
3 | "description": "Accessibility in JavaScript Applications",
4 | "version": "1.0.0",
5 | "author": "Marcy Sutton ",
6 | "dependencies": {
7 | "@babel/plugin-proposal-export-default-from": "^7.2.0",
8 | "@emotion/core": "^10.0.14",
9 | "@emotion/styled": "^10.0.14",
10 | "autoprefixer": "^9.6.1",
11 | "downshift": "^3.2.10",
12 | "emotion": "^10.0.14",
13 | "extract-text-webpack-plugin": "^3.0.2",
14 | "focus-visible": "^5.0.2",
15 | "gatsby": "^2.13.25",
16 | "gatsby-image": "^2.2.6",
17 | "gatsby-plugin-manifest": "^2.2.1",
18 | "gatsby-plugin-mdx": "^1.0.7",
19 | "gatsby-plugin-offline": "^2.2.1",
20 | "gatsby-plugin-react-helmet": "^3.0.12",
21 | "gatsby-plugin-sass": "^2.1.3",
22 | "gatsby-plugin-sharp": "^2.2.7",
23 | "gatsby-remark-prismjs": "^3.3.3",
24 | "gatsby-source-filesystem": "^2.1.5",
25 | "gatsby-theme-mdx-deck": "^3.0.8",
26 | "gatsby-transformer-sharp": "^2.2.3",
27 | "match-sorter": "^4.0.1",
28 | "node-sass": "^4.12.0",
29 | "prop-types": "^15.7.2",
30 | "react": "^16.8.6",
31 | "react-debounce-input": "^3.2.0",
32 | "react-dom": "^16.8.6",
33 | "react-helmet": "^5.2.0",
34 | "sass-loader": "^7.1.0",
35 | "starwars-names": "^1.6.0"
36 | },
37 | "keywords": [
38 | "gatsby"
39 | ],
40 | "license": "MIT",
41 | "scripts": {
42 | "build": "gatsby build",
43 | "serve": "gatsby serve",
44 | "develop": "gatsby develop",
45 | "deploy": "gatsby clean && gatsby build --prefix-paths && gh-pages -d public",
46 | "start": "npm run develop",
47 | "format": "prettier --write \"src/**/*.js\"",
48 | "test": "jest",
49 | "cy:open": "cypress open",
50 | "cy:run": "cypress run",
51 | "test:e2e": "CYPRESS_SUPPORT=y start-server-and-test develop http://localhost:8000 cy:open",
52 | "test:e2e:ci": "start-server-and-test develop http://localhost:8000 cy:run"
53 | },
54 | "devDependencies": {
55 | "@testing-library/cypress": "^4.0.5",
56 | "@testing-library/jest-dom": "^4.0.0",
57 | "@testing-library/react": "^8.0.6",
58 | "axe-core": "^3.3.1",
59 | "babel-jest": "^24.8.0",
60 | "babel-preset-gatsby": "^0.2.7",
61 | "cypress": "^3.4.1",
62 | "cypress-axe": "^0.5.1",
63 | "gh-pages": "^2.0.1",
64 | "identity-obj-proxy": "^3.0.0",
65 | "jest": "^24.8.0",
66 | "jest-transform-css": "^2.0.0",
67 | "prettier": "^1.17.0",
68 | "react-test-renderer": "^16.8.6",
69 | "start-server-and-test": "^1.9.1"
70 | },
71 | "repository": {
72 | "type": "git",
73 | "url": "https://github.com/marcysutton/js-a11y-workshop"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/__mocks__/file-mock.js:
--------------------------------------------------------------------------------
1 | module.exports = "test-file-stub"
--------------------------------------------------------------------------------
/src/components/bad/animation.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useEffect } from "react"
2 | import styled from "@emotion/styled"
3 |
4 | const Demo = styled.div`
5 | .animationToggle {
6 | background-color: black;
7 | border: none;
8 | border-radius: 0;
9 | color: white;
10 | font-size: 1rem;
11 | padding: 0.5em 1em;
12 | }
13 | .animationTarget {
14 | height: 100%;
15 | margin: 1em auto;
16 | min-width: 400px;
17 | min-height: 400px;
18 | overflow: hidden;
19 | position: relative;
20 | width: 100%;
21 | }
22 | .pulse {
23 | z-index: 1;
24 | position: absolute;
25 | top: 50%;
26 | left: 50%;
27 | transform: translate(-50%, -50%);
28 | max-width: 30rem;
29 |
30 | circle {
31 | fill: #ff5154;
32 | transform: scale(0);
33 | opacity: 0.4;
34 | transform-origin: 50% 50%;
35 | animation: pulse 2.5s cubic-bezier(.5,.5,0,1) infinite;
36 |
37 | &:nth-child(2) {
38 | fill: #7fc6a4;
39 | animation: pulse 2.5s 0.75s cubic-bezier(.5,.5,0,1) infinite;
40 | }
41 |
42 | &:nth-child(3) {
43 | fill: #e5f77d;
44 | animation: pulse 2.5s 1.5s cubic-bezier(.5,.5,0,1) infinite;
45 | }
46 |
47 | }
48 |
49 | }
50 |
51 | @keyframes pulse {
52 | 25% {
53 | opacity: 0.4;
54 | }
55 |
56 | 100% {
57 | transform: scale(1);
58 | opacity: 0;
59 | }
60 | }
61 | `
62 |
63 | const InaccessibleAnimationDemo = () => {
64 | return (
65 |
66 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | )
78 | }
79 |
80 | export default InaccessibleAnimationDemo
81 |
--------------------------------------------------------------------------------
/src/components/bad/async-form.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from "react"
2 | import {DebounceInput} from 'react-debounce-input'
3 |
4 | const InaccessibleAsyncFormDemo = () => {
5 | const [updating, setUpdating] = useState(false)
6 |
7 | const handleSubmit = (event) => {
8 | event.preventDefault()
9 | }
10 | const handleTextChange = (value) => {
11 | setUpdating(true)
12 |
13 | setTimeout(() => {
14 | setUpdating(false)
15 | }, 3000
16 | )
17 | }
18 | return (
19 |
30 | )
31 | }
32 |
33 | const Textarea = ({handleUpdateFunc}) => (
34 | handleUpdateFunc(event.target.value)} />
40 | )
41 |
42 | export default InaccessibleAsyncFormDemo
--------------------------------------------------------------------------------
/src/components/bad/card-flip.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | import './card-flip.scss'
4 |
5 | class CardFlip extends Component {
6 | constructor (props) {
7 | super(props)
8 | this.state = {
9 | isActive: false
10 | }
11 | }
12 | handleHover() {
13 | this.setState({
14 | isActive: !this.state.isActive
15 | })
16 | }
17 | render() {
18 | const activeClass = this.state.isActive ? "active" : ""
19 | return (
20 |
21 |
22 |
23 |
26 |
27 |
28 |
29 |
{this.props.memberName}
30 |
{this.props.subtitle}
31 |
{this.props.bio}
32 |
44 |
45 |
46 |
{this.props.memberName}
47 |
{this.props.subtitle}
48 |
49 |
50 |
51 | )
52 | }
53 | }
54 |
55 | export default CardFlip
56 |
--------------------------------------------------------------------------------
/src/components/bad/card-flip.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 | .card-flip-mouse-demo {
5 | .team-member {
6 | display: inline-block;
7 | position: relative;
8 | }
9 | .team-content {
10 | padding: 0;
11 | position: relative;
12 | z-index: 1;
13 | margin-top: 0;
14 | margin-bottom: 0px;
15 | width: 320px;
16 | text-align: center;
17 | }
18 | .team-image {
19 | width: 190px;
20 | height: 190px;
21 | overflow: hidden;
22 | margin: auto;
23 | margin-top: 60px;
24 | margin-bottom: 20px;
25 | border-radius: 90px;
26 | box-shadow: 0 6px 20px rgba(0, 0, 0, 0.07);
27 | text-align: center;
28 | }
29 | .team-content h5 {
30 | font-size: 18px;
31 | font-weight: 700;
32 | line-height: 1;
33 | margin: 0;
34 | margin-bottom: 10px;
35 | }
36 | .team-content .team-subtitle {
37 | margin-bottom: 30px;
38 | display: block;
39 | font-size: 14px;
40 | font-style: italic;
41 | line-height: 1;
42 | color: inherit;
43 | }
44 | .team-member .team-image img {
45 | margin: 0 auto;
46 | border: none;
47 | height: auto;
48 | width: 100%;
49 | }
50 | .team-content-hover {
51 | top: 0;
52 | padding: 40px 35px;
53 | position: absolute;
54 | width: 190px;
55 | overflow: hidden;
56 | max-height: 190px;
57 | height: auto;
58 | opacity: 0.5;
59 | visibility: hidden;
60 | -webkit-transition: 0.25s ease-in;
61 | -o-transition: 0.25s ease-in;
62 | transition: 0.25s ease-in;
63 | z-index: 100;
64 | border-radius: 90px;
65 | }
66 | .team-image.active .team-content-hover {
67 | opacity: 1;
68 | visibility: visible;
69 | margin-left: calc(95px - 50%);
70 | margin-top: -50px;
71 | max-height: 400px;
72 | width: 100%;
73 | height: auto;
74 | border-radius: 10px;
75 | }
76 | .team-content-hover .gradient-overlay {
77 | position: absolute;
78 | width: 100%;
79 | height: 100%;
80 | z-index: 0;
81 | top: 0;
82 | left: 0;
83 | }
84 | .team-content .team-content-hover h5 {
85 | color: #fff;
86 | font-size: 18px;
87 | font-weight: 700;
88 | position: relative;
89 | }
90 | .team-content .team-content-hover .team-subtitle {
91 | color: #fff;
92 | position: relative;
93 | }
94 | .team-content-hover p {
95 | color: #fff;
96 | opacity: 0;
97 | font-size: 8px;
98 | font-weight: 400;
99 | line-height: 22px;
100 | position: relative;
101 | font-size: 12px;
102 | -webkit-transition: 0.25s ease-in;
103 | -o-transition: 0.25s ease-in;
104 | transition: 0.25s ease-in;
105 | }
106 | .team-image.active .team-content-hover p {
107 | opacity: 1;
108 | font-size: 12px;
109 | }
110 | .team-socials {
111 | position: relative;
112 | margin: auto;
113 | margin-top: 45px;
114 | width: 100%;
115 | z-index: 101;
116 | text-align: center;
117 | font-size: 12px;
118 | line-height: 12px;
119 | }
120 | .team-socials a {
121 | color: #225165;
122 | outline: 0;
123 | text-decoration: none;
124 | }
125 | .team-socials svg {
126 | margin: 0 10px;
127 | width: 18px;
128 | }
129 | }
--------------------------------------------------------------------------------
/src/components/bad/dropdown.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from "react"
2 | import uuid from "uuid"
3 |
4 | import "./dropdown.scss"
5 |
6 | const Dropdown = ({ activatorText, items = [] }) => {
7 | const [isOpen, setIsOpen] = useState(false)
8 | const activatorRef = useRef()
9 | const listRef = useRef()
10 |
11 | const handleClick = () => {
12 | setIsOpen(!isOpen)
13 | }
14 | const handleClickOutside = (event) => {
15 | if (listRef.current.contains(event.target) || activatorRef.current.contains(event.target)) {
16 | return
17 | }
18 | setIsOpen(false)
19 | }
20 | useEffect(() => {
21 | if (isOpen) {
22 | document.addEventListener("mousedown", handleClickOutside)
23 | } else {
24 | document.removeEventListener("mousedown", handleClickOutside)
25 | }
26 | // clean up on unmount
27 | return function cleanup() {
28 | document.removeEventListener("mousedown", handleClickOutside)
29 | }
30 | }, [isOpen])
31 | return (
32 |
35 |
40 | { activatorText + '' }
41 |
42 |
50 | {items.map((item, index) => {
51 | return
54 | })}
55 |
56 |
57 | )
58 | }
59 | export default Dropdown
60 |
--------------------------------------------------------------------------------
/src/components/bad/dropdown.scss:
--------------------------------------------------------------------------------
1 | .wrap {
2 | position: relative;
3 | }
4 | .activator {
5 | align-items: center;
6 | background-color: inherit;
7 | border: 1px solid transparent;
8 | border-color: #ccc;
9 | color: inherit;
10 | display: flex;
11 | font-size: inherit;
12 | line-height: 1;
13 | max-width: 120px;
14 | padding: 1em;
15 |
16 | &:after {
17 | content: "";
18 | border-bottom: 1px solid #000;
19 | border-right: 1px solid #000;
20 | height: 0.5em;
21 | margin-left: auto;
22 | width: 0.5em;
23 | transform: rotate(45deg);
24 | }
25 | }
26 | .itemList {
27 | background-color: #ececec;
28 | color: black;
29 | display: none;
30 | margin: 0;
31 | min-width: 120px;
32 | padding: 0;
33 | position: absolute;
34 |
35 | &> div {
36 | list-style: none;
37 | margin: 0;
38 |
39 | a, a:link {
40 | display: block;
41 | padding: 0.5em;
42 | }
43 | }
44 | &.active {
45 | display: block;
46 | }
47 | }
--------------------------------------------------------------------------------
/src/components/better/__tests__/dropdown.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render, fireEvent } from '@testing-library/react'
3 |
4 | import Dropdown from '../dropdown'
5 |
6 | describe(`Dropdown`, () => {
7 | it(`renders activatorText`, () => {
8 | const activatorText = `Hamburgers`
9 | const { getByText } = render( )
10 |
11 | const text = getByText(activatorText)
12 |
13 | expect(text).toBeInTheDocument()
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/components/better/animation.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useEffect } from "react"
2 | import { css } from "@emotion/core"
3 | import styled from "@emotion/styled"
4 |
5 | const noAnimate = css`
6 | .pulse, circle {
7 | animation: none !important;
8 | }
9 | .pulse circle {
10 | opacity: 0.75;
11 | transform: scale(0.25);
12 | }
13 | .pulse circle:nth-child(2) {
14 | opacity: 0.25;
15 | transform: scale(0.75);
16 | }
17 | .pulse circle:nth-child(3) {
18 | opacity: 0.5;
19 | transform: scale(0.5);
20 | }
21 | `
22 | const Demo = styled.div`
23 | .animationToggle {
24 | background-color: black;
25 | border: none;
26 | border-radius: 0;
27 | color: white;
28 | font-size: 1rem;
29 | padding: 0.5em 1em;
30 | }
31 | .animationTarget {
32 | height: 400px;
33 | margin: 0 auto;
34 | overflow: hidden;
35 | position: relative;
36 | width: 400px;
37 | }
38 | .pulse {
39 | z-index: 1;
40 | position: absolute;
41 | top: 50%;
42 | left: 50%;
43 | transform: translate(-50%, -50%);
44 | max-width: 30rem;
45 |
46 | circle {
47 | fill: #ff5154;
48 | transform: scale(0);
49 | opacity: 0.4;
50 | transform-origin: 50% 50%;
51 | animation: pulse 2.5s cubic-bezier(.5,.5,0,1) infinite;
52 |
53 | &:nth-child(2) {
54 | fill: #7fc6a4;
55 | animation: pulse 2.5s 0.75s cubic-bezier(.5,.5,0,1) infinite;
56 | }
57 |
58 | &:nth-child(3) {
59 | fill: #e5f77d;
60 | animation: pulse 2.5s 1.5s cubic-bezier(.5,.5,0,1) infinite;
61 | }
62 |
63 | }
64 |
65 | }
66 |
67 | @keyframes pulse {
68 | 25% {
69 | opacity: 0.4;
70 | }
71 |
72 | 100% {
73 | transform: scale(1);
74 | opacity: 0;
75 | }
76 | }
77 |
78 |
79 | .no-animate {
80 | ${noAnimate}
81 | }
82 |
83 | /* if reduced-motion is selected on OSX/iOS */
84 | @media (prefers-reduced-motion) {
85 | /* hide toggle button */
86 | .animationToggle {
87 | display: none;
88 | }
89 | /* make sure animations actually stop */
90 | ${noAnimate}
91 | }
92 | `
93 |
94 | const AccessibleAnimationDemo = () => {
95 | let [animating, setAnimating] = useState(false)
96 | let [toggleText, setToggleText] = useState('on')
97 |
98 | const animationTarget = useRef()
99 | const animationToggle = useRef()
100 |
101 | useEffect(() => {
102 | if (localStorage.getItem('animating') === 'false') {
103 | disableAnimation()
104 | }
105 | }, [animating])
106 |
107 | function toggleBtnHandler(event) {
108 | if (animating) {
109 | disableAnimation()
110 | } else {
111 | enableAnimation()
112 | }
113 | }
114 | function disableAnimation() {
115 | setToggleText('on')
116 | setAnimating(false)
117 | localStorage.setItem('animating', 'false')
118 | }
119 | function enableAnimation() {
120 | setToggleText('off')
121 | setAnimating(true)
122 | localStorage.setItem('animating', 'true')
123 | }
124 | return (
125 |
126 |
131 |
132 | Animating circles
133 |
134 |
135 |
136 |
137 |
138 |
139 |
144 | Turn {toggleText} animation
145 |
146 |
147 |
148 | )
149 | }
150 |
151 | export default AccessibleAnimationDemo
152 |
--------------------------------------------------------------------------------
/src/components/better/card-flip.js:
--------------------------------------------------------------------------------
1 | import React, {useRef, useEffect, useState} from "react"
2 |
3 | import "./card-flip.scss"
4 |
5 | function CardFlip (props) {
6 | let toggleButtonRef = useRef(null)
7 | let closeButtonRef = useRef(null)
8 | let overlayRef = useRef(null)
9 |
10 | const [ isActive, setIsActive ] = useState(false)
11 | const [transitionStatus, changeTransitionStatus] = useState(0)
12 |
13 | useEffect(() => {
14 | const handler = (event) => {
15 | // you can listen for any animatable CSS property here!
16 | // Note: opacity alone is not enough to properly disable hidden content still in the DOM.
17 | if (event.propertyName === "opacity" && closeButtonRef.current !== null) {
18 | closeButtonRef.current.focus()
19 | }
20 | }
21 | window.addEventListener("transitionend", handler)
22 | }, [])
23 | const handleClick = (event) => {
24 | setIsActive(!isActive)
25 | }
26 | const closeOverlay = () => {
27 | setIsActive(false)
28 | toggleButtonRef.current.focus()
29 | }
30 |
31 | const activeClass = isActive ? "active" : ""
32 | // todo: reimplement focus trap
33 | const overlay = isActive
34 | ?
35 |
36 |
37 |
X
42 |
{props.memberName}
43 |
{props.subtitle}
44 |
{props.bio}
45 |
50 |
51 |
52 | : false
53 | return (
54 |
55 |
56 |
60 |
61 |
62 |
63 | {props.memberName}
64 | {props.subtitle}
65 |
66 |
67 |
71 | {overlay}
72 |
73 |
74 |
75 | )
76 | }
77 |
78 | export default CardFlip
--------------------------------------------------------------------------------
/src/components/better/card-flip.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 | .team-member {
5 | display: inline-block;
6 | position: relative;
7 | }
8 | .team-content {
9 | padding: 0;
10 | position: relative;
11 | z-index: 1;
12 | margin-top: 0;
13 | margin-bottom: 0px;
14 | width: 320px;
15 | text-align: center;
16 | }
17 | .team-content button {
18 | background-color: transparent;
19 | border: none;
20 | color: currentColor;
21 | -webkit-appearance: none;
22 | }
23 | .team-content .toggle-button * {
24 | text-decoration: underline;
25 | }
26 | .team-image {
27 | display: block;
28 | width: 190px;
29 | height: 190px;
30 | overflow: hidden;
31 | margin: auto;
32 | margin-top: 60px;
33 | margin-bottom: 20px;
34 | border-radius: 90px;
35 | box-shadow: 0 6px 20px rgba(0, 0, 0, 0.07);
36 | text-align: center;
37 | }
38 | .team-content .team-name {
39 | display: block;
40 | font-size: 18px;
41 | font-weight: 700;
42 | line-height: 1;
43 | margin: 0;
44 | margin-bottom: 10px;
45 | }
46 | .team-content .team-subtitle {
47 | margin-bottom: 0px;
48 | display: block;
49 | font-size: 14px;
50 | font-style: italic;
51 | line-height: 1;
52 | color: inherit;
53 | }
54 | .team-member .team-image img {
55 | margin: 0 auto;
56 | border: none;
57 | height: auto;
58 | width: 100%;
59 | }
60 | .team-content-overlay {
61 | background-color: #1c6300;
62 | color: #fff;
63 | top: 0;
64 | padding: 40px 35px;
65 | position: absolute;
66 | width: 190px;
67 | overflow: hidden;
68 | min-height: 190px;
69 | max-height: 190px;
70 | height: auto;
71 | left: 60px;
72 | opacity: 0.5;
73 | visibility: hidden;
74 | -webkit-transition: 0.25s ease-in;
75 | -o-transition: 0.25s ease-in;
76 | transition: 0.25s ease-in;
77 | z-index: 100;
78 | border-radius: 90px;
79 | }
80 | .active .team-content-overlay {
81 | opacity: 1;
82 | visibility: visible;
83 | margin-left: calc(95px - 50%);
84 | margin-top: 30px;
85 | max-height: 400px;
86 | width: 100%;
87 | height: auto;
88 | border-radius: 10px;
89 | }
90 | .team-content-overlay .gradient-overlay {
91 | position: absolute;
92 | width: 100%;
93 | height: 100%;
94 | z-index: 0;
95 | top: 0;
96 | left: 0;
97 | }
98 | .team-content .team-content-overlay .team-name {
99 | font-size: 18px;
100 | font-weight: 700;
101 | position: relative;
102 | }
103 | .team-content .team-content-overlay .team-subtitle {
104 | position: relative;
105 | }
106 | .team-close-button {
107 | border: none;
108 | font-weight: bold;
109 | position: absolute;
110 | top: 10px;
111 | right: 10px;
112 |
113 | &:focus {
114 | outline: 3px solid lightblue;
115 | }
116 | }
117 | .team-content-overlay p {
118 | color: #fff;
119 | opacity: 0;
120 | font-size: 8px;
121 | font-weight: 400;
122 | line-height: 22px;
123 | position: relative;
124 | font-size: 12px;
125 | // -webkit-transition: 0.25s ease-in;
126 | // -o-transition: 0.25s ease-in;
127 | // transition: 0.25s ease-in;
128 | }
129 | .active .team-content-overlay p {
130 | opacity: 1;
131 | font-size: 12px;
132 | }
133 | .team-socials {
134 | position: relative;
135 | margin: auto;
136 | margin-top: 45px;
137 | width: 100%;
138 | z-index: 101;
139 | text-align: center;
140 | line-height: 12px;
141 | }
142 | .team-socials a {
143 | color: #225165;
144 | outline: 0;
145 | text-decoration: none;
146 | }
147 | .team-socials svg {
148 | margin: 0 10px;
149 | width: 25px;
150 | }
--------------------------------------------------------------------------------
/src/components/better/downshift-autocomplete.js:
--------------------------------------------------------------------------------
1 | // This is a more complete example of an autocomplete component
2 | // with custom styling, filtering of objects, and more.
3 | // Much of the irrelevant bits are in the ../shared file.
4 | // which you may also want to become familiar with as many
5 | // examples will use those as well.
6 | import React from 'react'
7 | import Downshift from 'downshift'
8 | import {
9 | Label,
10 | Menu,
11 | ControllerButton,
12 | Input,
13 | Item,
14 | ArrowIcon,
15 | XIcon,
16 | css,
17 | itemToString,
18 | getItems,
19 | } from './downshift-utils'
20 |
21 | class Autocomplete extends React.Component {
22 | render() {
23 | return (
24 |
31 |
33 | alert(
34 | selection
35 | ? `You selected ${itemToString(selection)}`
36 | : 'selection cleared',
37 | )
38 | }
39 | itemToString={itemToString}
40 | >
41 | {({
42 | getLabelProps,
43 | getInputProps,
44 | getToggleButtonProps,
45 | getMenuProps,
46 | getItemProps,
47 | isOpen,
48 | clearSelection,
49 | selectedItem,
50 | inputValue,
51 | highlightedIndex,
52 | }) => (
53 |
54 |
Find a Star Wars character
55 |
56 |
62 | {selectedItem ? (
63 |
67 |
68 |
69 | ) : (
70 |
71 |
72 |
73 | )}
74 |
75 |
76 |
77 | {isOpen
78 | ? getItems(inputValue).map((item, index) => (
79 | -
88 | {itemToString(item)}
89 |
90 | ))
91 | : null}
92 |
93 |
94 |
95 | )}
96 |
97 |
98 | )
99 | }
100 | }
101 |
102 | export default Autocomplete
--------------------------------------------------------------------------------
/src/components/better/downshift-utils.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {css as emoCSS} from 'emotion'
3 | import styled from '@emotion/styled'
4 | import starWarsNames from 'starwars-names'
5 | import matchSorter from 'match-sorter'
6 |
7 | const allItems = starWarsNames.all.map(s => ({name: s, id: s.toLowerCase()}))
8 |
9 | const css = (...args) => {
10 | console.log(args)
11 | return ({className: emoCSS(...args)})
12 | }
13 |
14 | const Item = styled('li')(
15 | {
16 | position: 'relative',
17 | cursor: 'pointer',
18 | display: 'block',
19 | border: 'none',
20 | height: 'auto',
21 | textAlign: 'left',
22 | borderTop: 'none',
23 | lineHeight: '1em',
24 | color: 'rgba(0,0,0,.87)',
25 | fontSize: '1rem',
26 | textTransform: 'none',
27 | fontWeight: '400',
28 | boxShadow: 'none',
29 | padding: '.8rem 1.1rem',
30 | whiteSpace: 'normal',
31 | wordWrap: 'normal',
32 | },
33 | ({isActive, isSelected}) => {
34 | const styles = []
35 | if (isActive) {
36 | styles.push({
37 | color: 'rgba(0,0,0,.95)',
38 | background: 'rgba(0,0,0,.03)',
39 | })
40 | }
41 | if (isSelected) {
42 | styles.push({
43 | color: 'rgba(0,0,0,.95)',
44 | fontWeight: '700',
45 | })
46 | }
47 | return styles
48 | },
49 | )
50 | const onAttention = '&:hover, &:focus'
51 | const Input = styled('input')(
52 | {
53 | width: '100%', // full width - icon width/2 - border
54 | fontSize: '1rem',
55 | wordWrap: 'break-word',
56 | lineHeight: '1em',
57 | outline: 0,
58 | whiteSpace: 'normal',
59 | minHeight: '2em',
60 | background: '#fff',
61 | display: 'inline-block',
62 | padding: '1em 2em 1em 1em',
63 | color: 'rgba(0,0,0,1)',
64 | boxShadow: 'none',
65 | border: '1px solid rgba(34,36,38,1)',
66 | borderRadius: '.30rem',
67 | transition: 'box-shadow .1s ease,width .1s ease',
68 | [onAttention]: {
69 | borderColor: '#00b7f9',
70 | boxShadow: '0 2px 3px 0 rgba(34,36,38,.5)',
71 | },
72 | },
73 | ({isOpen}) =>
74 | isOpen
75 | ? {
76 | borderBottomLeftRadius: '0',
77 | borderBottomRightRadius: '0',
78 | [onAttention]: {
79 | boxShadow: 'none',
80 | },
81 | }
82 | : null,
83 | )
84 |
85 | const Label = styled('label')({
86 | fontSize: '1.5rem',
87 | fontWeight: 'bold',
88 | display: 'block',
89 | marginBottom: '0.25rem',
90 | })
91 |
92 | const BaseMenu = styled('ul')(
93 | {
94 | padding: 0,
95 | marginTop: 0,
96 | position: 'absolute',
97 | backgroundColor: 'white',
98 | width: '100%',
99 | maxHeight: '20rem',
100 | maxWidth: '100%',
101 | overflowY: 'auto',
102 | overflowX: 'hidden',
103 | outline: '0',
104 | transition: 'opacity .1s ease',
105 | borderRadius: '0 0 .28571429rem .28571429rem',
106 | boxShadow: '0 2px 3px 0 rgba(34,36,38,.15)',
107 | borderColor: '#96c8da',
108 | borderTopWidth: '0',
109 | borderRightWidth: 1,
110 | borderBottomWidth: 1,
111 | borderLeftWidth: 1,
112 | borderStyle: 'solid',
113 | },
114 | ({isOpen}) => ({
115 | border: isOpen ? null : 'none',
116 | }),
117 | )
118 |
119 | const Menu = React.forwardRef((props, ref) => (
120 |
121 | ))
122 |
123 | const ControllerButton = styled('button')({
124 | backgroundColor: 'transparent',
125 | fontSize: '1rem',
126 | lineHeight: '1',
127 | border: 'none',
128 | position: 'absolute',
129 | right: 0,
130 | top: 0,
131 | cursor: 'pointer',
132 | width: 47,
133 | display: 'flex',
134 | flexDirection: 'column',
135 | height: '100%',
136 | justifyContent: 'center',
137 | alignItems: 'center',
138 | })
139 |
140 | function ArrowIcon({isOpen}) {
141 | return (
142 |
151 |
152 |
153 | )
154 | }
155 |
156 | function XIcon() {
157 | return (
158 |
166 |
167 |
168 |
169 | )
170 | }
171 |
172 | function getItems(filter) {
173 | return filter
174 | ? matchSorter(allItems, filter, {
175 | keys: ['name'],
176 | })
177 | : allItems
178 | }
179 |
180 | function getStringItems(filter) {
181 | return getItems(filter).map(({name}) => name)
182 | }
183 |
184 | function sleep(ms) {
185 | return new Promise(resolve => {
186 | setTimeout(resolve, ms)
187 | })
188 | }
189 |
190 | async function getItemsAsync(filter, {reject}) {
191 | await sleep(Math.random() * 2000)
192 | if (reject) {
193 | // this is just so we can have examples that show what happens
194 | // when there's a request failure.
195 | throw new Error({error: 'request rejected'})
196 | }
197 | return getItems(filter)
198 | }
199 |
200 | const itemToString = i => (i ? i.name : '')
201 |
202 | export {
203 | Menu,
204 | ControllerButton,
205 | Input,
206 | Item,
207 | ArrowIcon,
208 | XIcon,
209 | Label,
210 | css,
211 | itemToString,
212 | getItems,
213 | getStringItems,
214 | getItemsAsync,
215 | }
--------------------------------------------------------------------------------
/src/components/better/dropdown.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from "react"
2 | import uuid from "uuid"
3 |
4 | import "./dropdown.scss"
5 |
6 | const Dropdown = ({ }) => {
7 | return (
8 |
11 |
12 |
13 | )
14 | }
15 | export default Dropdown
16 |
--------------------------------------------------------------------------------
/src/components/better/dropdown.scss:
--------------------------------------------------------------------------------
1 | .dropdown-wrap {
2 | position: relative;
3 | }
4 | .dropdown-activator {
5 | align-items: center;
6 | background-color: inherit;
7 | border: 1px solid transparent;
8 | border-color: #ccc;
9 | color: inherit;
10 | display: flex;
11 | font-size: inherit;
12 | max-width: 160px;
13 | padding: 1em;
14 |
15 | &:after {
16 | content: "";
17 | border-bottom: 1px solid #000;
18 | border-right: 1px solid #000;
19 | height: 0.5em;
20 | margin-left: 0.75em;
21 | width: 0.5em;
22 | transform: rotate(45deg);
23 | }
24 | }
25 | .dropdown-itemList {
26 | background-color: #ececec;
27 | color: black;
28 | display: none;
29 | margin: 0;
30 | min-width: 160px;
31 | padding: 0;
32 | position: absolute;
33 |
34 | li {
35 | list-style: none;
36 | margin: 0;
37 |
38 | a, a:link {
39 | display: block;
40 | padding: 0.5em;
41 | }
42 | }
43 |
44 | &.active {
45 | display: block;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/better/live-region.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useRef} from "react"
2 |
3 | const LiveRegion = () => {
4 |
5 | return (
6 | <>
7 | >
8 | )
9 | }
10 |
11 | export default LiveRegion
--------------------------------------------------------------------------------
/src/components/better/reduced-motion.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { css } from "@emotion/core"
3 | import styled from "@emotion/styled"
4 |
5 | const ReducedMotionDemo = () => {
6 | return (
7 | <>>
8 | )
9 | }
10 |
11 | export default ReducedMotionDemo
12 |
--------------------------------------------------------------------------------
/src/components/better/route-target-heading.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { css } from "@emotion/core"
3 |
4 | const styles = css`
5 | .routeSkipHeading {
6 | position: relative;
7 | }
8 | .routeSkipLink {
9 | display: inline-block;
10 | margin-left: -0.75em;
11 | opacity: 0;
12 | position: absolute;
13 | text-decoration: none;
14 | }
15 | .routeSkipLink:before {
16 | content: '⇽';
17 | display: block;
18 | }
19 | .routeSkipLink:focus,
20 | .routeSkipLink:hover {
21 | opacity: 1;
22 | }
23 | `
24 | const RouteHeading = ({level = 1, targetID, children}) => {
25 | const Heading = `h${level}`;
26 | return (
27 |
28 |
33 |
34 | {children}
35 |
36 | )
37 | }
38 | export default RouteHeading
39 |
--------------------------------------------------------------------------------
/src/components/better/tab-list.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 |
3 | const TabList = ({ items = [] }) => {
4 | const [isClient, setClient] = useState(false)
5 | /*
6 | * Think of this as componentDidMount
7 | * on the client, it will run once and update state
8 | */
9 | useEffect(() => {
10 | setClient(true)
11 | }, [])
12 | return (
13 | <>>
14 | )
15 | }
16 |
17 | export default TabList
--------------------------------------------------------------------------------
/src/components/site-chrome/code-figure.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import styled from "@emotion/styled"
3 |
4 | import theme from "../../theme"
5 |
6 | const Figure = styled.figure`
7 | font-size: 1.15rem;
8 | margin: 0;
9 | vertical-align: top;
10 |
11 | &.split {
12 | display: inline-block;
13 | min-width: auto;
14 | }
15 | &.center {
16 | margin: 0 auto;
17 | min-width: 80%;
18 | }
19 | &.left {
20 | margin-right: 1rem;
21 | }
22 | &.noRightMargin {
23 | margin-right: 0;
24 | }
25 | pre {
26 | margin: 0 !important;
27 | }
28 |
29 | figcaption {
30 | background-color: ${theme.colors.figcaptionBackground};
31 | box-sizing: border-box;
32 | color: ${theme.colors.figcaption};
33 | font-size: ${theme.styles.figcaption.fontSize};
34 | font-weight: bold;
35 | margin-top: 0;
36 | padding: 0.5rem;
37 | text-align: left;
38 |
39 | a {
40 | color: inherit;
41 | }
42 | }
43 | `
44 | const blockClassName = (side, marginClass) => {
45 | let margin = '';
46 | if (marginClass) {
47 | margin = marginClass;
48 | }
49 | if (side) {
50 | return `split ${side} ${margin}`;
51 | }
52 | return null;
53 | }
54 | export default ({ title, side, url, marginClass, children }) => (
55 |
56 | { children }
57 | { title ?
58 |
59 | { url ? {title} : title }
60 |
61 | : null }
62 |
63 | )
64 |
--------------------------------------------------------------------------------
/src/components/site-chrome/header.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'gatsby'
2 | import PropTypes from 'prop-types'
3 | import React from 'react'
4 |
5 | const Header = ({ siteTitle }) => (
6 |
7 |
10 |
11 |
14 | {siteTitle}
15 |
16 |
17 |
18 | )
19 |
20 | Header.propTypes = {
21 | siteTitle: PropTypes.string,
22 | }
23 |
24 | Header.defaultProps = {
25 | siteTitle: '',
26 | }
27 |
28 | export default Header
29 |
--------------------------------------------------------------------------------
/src/components/site-chrome/image.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StaticQuery, graphql } from 'gatsby'
3 | import Img from 'gatsby-image'
4 |
5 | /*
6 | * This component is built using `gatsby-image` to automatically serve optimized
7 | * images with lazy loading and reduced file sizes. The image is loaded using a
8 | * `StaticQuery`, which allows us to load the image from directly within this
9 | * component, rather than having to pass the image data down from pages.
10 | *
11 | * For more information, see the docs:
12 | * - `gatsby-image`: https://gatsby.app/gatsby-image
13 | * - `StaticQuery`: https://gatsby.app/staticquery
14 | */
15 |
16 | const Image = () => (
17 | }
30 | />
31 | )
32 | export default Image
33 |
--------------------------------------------------------------------------------
/src/components/site-chrome/layout.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { useStaticQuery, graphql } from 'gatsby'
4 |
5 | import Header from './header'
6 | import Navigation from './navigation'
7 | import './layout.scss'
8 | import 'focus-visible'
9 |
10 | const Layout = ({ children }) => {
11 | const data = useStaticQuery(graphql`
12 | query SiteTitleQuery {
13 | site {
14 | siteMetadata {
15 | title
16 | }
17 | }
18 | }
19 | `)
20 | return (
21 |
22 |
23 |
24 |
25 | {children}
26 |
27 |
28 | )
29 | }
30 |
31 | Layout.propTypes = {
32 | children: PropTypes.node.isRequired,
33 | }
34 |
35 | export default Layout
36 |
--------------------------------------------------------------------------------
/src/components/site-chrome/layout.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: inherit;
3 | }
4 | *:before {
5 | box-sizing: inherit;
6 | }
7 | *:after {
8 | box-sizing: inherit;
9 | }
10 | html {
11 | font-size: 18px;
12 | font-family: sans-serif;
13 | -ms-text-size-adjust: 100%;
14 | -webkit-text-size-adjust: 100%;
15 | }
16 | body {
17 | font-family: sans-serif;
18 | margin: 0;
19 | }
20 |
21 | a, a:link, a:visited {
22 | color: #333;
23 | }
24 | .js-focus-visible :focus:not(.focus-visible) {
25 | outline: none;
26 | }
27 | .js-focus-visible :focus-visible {
28 | outline: 5px auto lightblue;
29 | outline: 5px auto -webkit-focus-ring-color;
30 | }
31 | p > code {
32 | background-color: #f6f7b4;
33 | font-weight: bold;
34 | padding: 1px 3px;
35 | }
36 | .js-workshop-app {
37 | display: grid;
38 | grid-template-rows: auto 1fr;
39 | grid-template-columns: 1fr 3fr;
40 | grid-template-areas: "header header"
41 | "nav main";
42 | height: 100vh;
43 | width: 100vw;
44 |
45 | main {
46 | background-color: #fff;
47 | grid-area: main;
48 | }
49 | nav {
50 | background-color: #ececec;
51 | grid-area: nav;
52 | }
53 | header.globalHeader {
54 | grid-area: header;
55 | }
56 | p, ul {
57 | margin-bottom: 1em;
58 | line-height: 1.5;
59 | }
60 | }
61 | @media screen and (max-width: 600px) {
62 | .js-workshop-app {
63 | display: block;
64 | }
65 | }
66 | header.globalHeader {
67 | background-color: rebeccapurple;
68 | color: white;
69 | padding: 1em;
70 |
71 | .headerIcon {
72 | display: inline-block;
73 | height: 1em;
74 | width: 1em;
75 | }
76 | a, a:link, a:visited {
77 | color: currentColor;
78 | text-decoration: none;
79 | }
80 |
81 | svg path {
82 | fill: currentColor;
83 | }
84 | }
85 | main.app > div {
86 | padding: 1.5em;
87 | }
88 | .heading {
89 | font-weight: bold;
90 |
91 | &.level-3 {
92 | font-size: 1.35rem;
93 | margin-bottom: 1em;
94 | }
95 | }
96 | section.two-col {
97 | display: grid;
98 | grid-template-columns: 1fr 1fr;
99 | }
100 | .demo-button {
101 | background-color: #333;
102 | border: none;
103 | border-radius: 0.5rem;
104 | color: #fff;
105 | display: inline-block;
106 | font-size: 2rem;
107 | margin: 0rem 0 1.5rem;
108 | padding: 0.25rem 0.5rem;
109 |
110 | &.no-focus {
111 | outline: none;
112 | }
113 | }
114 | .modal-curtain {
115 | background-color: rgba(0, 0, 0, 0.75);
116 | position: fixed;
117 | left: 0;
118 | right: 0;
119 | top: 0;
120 | bottom: 0;
121 | width: 100%;
122 | height: 100%;
123 | z-index: 1;
124 | }
125 | [hidden] {
126 | display: none;
127 | }
128 | .semantic-layout {
129 | margin-bottom: 2em;
130 |
131 | & [data-el],
132 | &[data-el] {
133 | border: 1px solid rebeccapurple;
134 | padding: 0.5em;
135 | position: relative;
136 |
137 | &:before {
138 | background-color: rebeccapurple;
139 | color: #fff;
140 | content: attr(data-el);
141 | font-weight: bold;
142 | display: block;
143 | right: 0;
144 | line-height: 0.45;
145 | padding: 0.45em;
146 | position: absolute;
147 | top: 0;
148 | }
149 | }
150 | & > div[data-el],
151 | &[data-el] {
152 | padding: 1.5em;
153 | }
154 | }
155 | .grid-wrap-2 {
156 | display: grid;
157 | grid-template-columns: 1fr 2fr;
158 | }
159 | textarea {
160 | height: 200px;
161 | width: 320px;
162 | }
163 | .updateUI {
164 | max-width: 320px;
165 |
166 | .toast {
167 | background-color: lightyellow;
168 | display: flex;
169 | opacity: 0;
170 | padding: 0.5em;
171 | pointer-events: none;
172 |
173 | &.updating {
174 | opacity: 1;
175 | pointer-events: all;
176 | }
177 |
178 | .dismiss {
179 | background-color: transparent;
180 | border: none;
181 | display: inline-block;
182 | font-size: 1rem;
183 | font-weight: bold;
184 | margin-left: auto;
185 | }
186 | }
187 | }
188 | .skip-link {
189 | left: 0;
190 | position: absolute;
191 | top: 0;
192 |
193 | li {
194 | list-style: none;
195 | }
196 |
197 | a {
198 | display: block;
199 | opacity: 0;
200 |
201 | &:focus {
202 | background-color: black;
203 | opacity: 1;
204 | }
205 | }
206 | }
207 | .visually-hidden { /* https://snook.ca/archives/html_and_css/hiding-content-for-accessibility */
208 | position: absolute !important;
209 | height: 1px; width: 1px;
210 | overflow: hidden;
211 | clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
212 | clip: rect(1px, 1px, 1px, 1px);
213 | }
214 |
--------------------------------------------------------------------------------
/src/components/site-chrome/navigation.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Link } from "gatsby"
3 |
4 | import navStyles from "./navigation.module.scss"
5 |
6 | const Navigation = () => (
7 |
8 |
9 | Navigation
10 |
11 |
12 | App Home
13 | Slide deck
14 |
15 |
16 | Demos
17 |
18 |
19 | Dropdown
20 | Announcer
21 | Layout
22 | Animation
23 | Enhanced Tablist
24 |
25 |
26 |
27 |
28 | Concepts
29 |
30 |
31 | Focus management
32 | Announcements
33 | Semantic HTML
34 | Unobtrusive motion
35 | Progressive enhancement
36 |
37 |
38 |
39 |
40 | )
41 |
42 | export default Navigation
43 |
--------------------------------------------------------------------------------
/src/components/site-chrome/navigation.module.scss:
--------------------------------------------------------------------------------
1 | .nav {
2 | box-sizing: border-box;
3 | padding: 0 1em 0 1.5em;
4 |
5 | & * {
6 | box-sizing: border-box;
7 | }
8 |
9 | ul {
10 | list-style-position: inside;
11 | margin: 0;
12 | padding: 0;
13 | width: 100%;
14 |
15 | li {
16 | margin: 0;
17 | }
18 |
19 | li.navItemGroup {
20 | list-style: none;
21 | padding-left: 0;
22 | }
23 | }
24 | &> ul {
25 | padding: 1em 0;
26 | }
27 | .navHeading {
28 | margin: 1em 0;
29 | }
30 | }
--------------------------------------------------------------------------------
/src/components/site-chrome/seo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Helmet from 'react-helmet'
4 | import { StaticQuery, graphql } from 'gatsby'
5 |
6 | function SEO({ description, lang, meta, keywords, title }) {
7 | return (
8 | {
11 | const metaDescription =
12 | description || data.site.siteMetadata.description
13 | return (
14 | 0
56 | ? {
57 | name: 'keywords',
58 | content: keywords.join(', '),
59 | }
60 | : []
61 | )
62 | .concat(meta)}
63 | />
64 | )
65 | }}
66 | />
67 | )
68 | }
69 |
70 | SEO.defaultProps = {
71 | lang: 'en',
72 | meta: [],
73 | keywords: [],
74 | }
75 |
76 | SEO.propTypes = {
77 | description: PropTypes.string,
78 | lang: PropTypes.string,
79 | meta: PropTypes.array,
80 | keywords: PropTypes.arrayOf(PropTypes.string),
81 | title: PropTypes.string.isRequired,
82 | }
83 |
84 | export default SEO
85 |
86 | const detailsQuery = graphql`
87 | query DefaultSEOQuery {
88 | site {
89 | siteMetadata {
90 | title
91 | description
92 | author
93 | }
94 | }
95 | }
96 | `
97 |
--------------------------------------------------------------------------------
/src/css/reset.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: sans-serif;
3 | -ms-text-size-adjust: 100%;
4 | -webkit-text-size-adjust: 100%;
5 | }
6 | body {
7 | margin: 0;
8 | }
9 | article,
10 | aside,
11 | details,
12 | figcaption,
13 | figure,
14 | footer,
15 | header,
16 | main,
17 | menu,
18 | nav,
19 | section,
20 | summary {
21 | display: block;
22 | }
23 | audio,
24 | canvas,
25 | progress,
26 | video {
27 | display: inline-block;
28 | }
29 | audio:not([controls]) {
30 | display: none;
31 | height: 0;
32 | }
33 | progress {
34 | vertical-align: baseline;
35 | }
36 | [hidden],
37 | template {
38 | display: none;
39 | }
40 | a {
41 | background-color: transparent;
42 | -webkit-text-decoration-skip: objects;
43 | }
44 | a:active,
45 | a:hover {
46 | outline-width: 0;
47 | }
48 | abbr[title] {
49 | border-bottom: none;
50 | text-decoration: underline;
51 | text-decoration: underline dotted;
52 | }
53 | b,
54 | strong {
55 | font-weight: inherit;
56 | font-weight: bolder;
57 | }
58 | dfn {
59 | font-style: italic;
60 | }
61 | h1 {
62 | font-size: 2em;
63 | margin: .67em 0;
64 | }
65 | mark {
66 | background-color: #ff0;
67 | color: #000;
68 | }
69 | small {
70 | font-size: 80%;
71 | }
72 | sub,
73 | sup {
74 | font-size: 75%;
75 | line-height: 0;
76 | position: relative;
77 | vertical-align: baseline;
78 | }
79 | sub {
80 | bottom: -.25em;
81 | }
82 | sup {
83 | top: -.5em;
84 | }
85 | img {
86 | border-style: none;
87 | }
88 | svg:not(:root) {
89 | overflow: hidden;
90 | }
91 | code,
92 | kbd,
93 | pre,
94 | samp {
95 | font-family: monospace, monospace;
96 | font-size: 1em;
97 | }
98 | figure {
99 | margin: 1em 40px;
100 | }
101 | hr {
102 | box-sizing: content-box;
103 | height: 0;
104 | overflow: visible;
105 | }
106 | button,
107 | input,
108 | optgroup,
109 | select,
110 | textarea {
111 | font: inherit;
112 | margin: 0;
113 | }
114 | optgroup {
115 | font-weight: 700;
116 | }
117 | button,
118 | input {
119 | overflow: visible;
120 | }
121 | button,
122 | select {
123 | text-transform: none;
124 | }
125 | [type=reset],
126 | [type=submit],
127 | button,
128 | html [type=button] {
129 | -webkit-appearance: button;
130 | }
131 | [type=button]::-moz-focus-inner,
132 | [type=reset]::-moz-focus-inner,
133 | [type=submit]::-moz-focus-inner,
134 | button::-moz-focus-inner {
135 | border-style: none;
136 | padding: 0;
137 | }
138 | [type=button]:-moz-focusring,
139 | [type=reset]:-moz-focusring,
140 | [type=submit]:-moz-focusring,
141 | button:-moz-focusring {
142 | outline: 1px dotted ButtonText;
143 | }
144 | fieldset {
145 | border: 1px solid silver;
146 | margin: 0 2px;
147 | padding: .35em .625em .75em;
148 | }
149 | legend {
150 | box-sizing: border-box;
151 | color: inherit;
152 | display: table;
153 | max-width: 100%;
154 | padding: 0;
155 | white-space: normal;
156 | }
157 | textarea {
158 | overflow: auto;
159 | }
160 | [type=checkbox],
161 | [type=radio] {
162 | box-sizing: border-box;
163 | padding: 0;
164 | }
165 | [type=number]::-webkit-inner-spin-button,
166 | [type=number]::-webkit-outer-spin-button {
167 | height: auto;
168 | }
169 | [type=search] {
170 | -webkit-appearance: textfield;
171 | outline-offset: -2px;
172 | }
173 | [type=search]::-webkit-search-cancel-button,
174 | [type=search]::-webkit-search-decoration {
175 | -webkit-appearance: none;
176 | }
177 | ::-webkit-input-placeholder {
178 | color: inherit;
179 | opacity: .54;
180 | }
181 | ::-webkit-file-upload-button {
182 | -webkit-appearance: button;
183 | font: inherit;
184 | }
185 | html {
186 | font: 112.5%/1.45em georgia, serif;
187 | box-sizing: border-box;
188 | overflow-y: scroll;
189 | }
190 | * {
191 | box-sizing: inherit;
192 | }
193 | *:before {
194 | box-sizing: inherit;
195 | }
196 | *:after {
197 | box-sizing: inherit;
198 | }
199 | body {
200 | color: hsla(0, 0%, 0%, 0.8);
201 | font-family: georgia, serif;
202 | font-weight: normal;
203 | word-wrap: break-word;
204 | font-kerning: normal;
205 | -moz-font-feature-settings: "kern", "liga", "clig", "calt";
206 | -ms-font-feature-settings: "kern", "liga", "clig", "calt";
207 | -webkit-font-feature-settings: "kern", "liga", "clig", "calt";
208 | font-feature-settings: "kern", "liga", "clig", "calt";
209 | }
210 | img {
211 | max-width: 100%;
212 | margin-left: 0;
213 | margin-right: 0;
214 | margin-top: 0;
215 | padding-bottom: 0;
216 | padding-left: 0;
217 | padding-right: 0;
218 | padding-top: 0;
219 | margin-bottom: 1.45rem;
220 | }
221 | h1 {
222 | margin-left: 0;
223 | margin-right: 0;
224 | margin-top: 0;
225 | padding-bottom: 0;
226 | padding-left: 0;
227 | padding-right: 0;
228 | padding-top: 0;
229 | margin-bottom: 1.45rem;
230 | color: inherit;
231 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
232 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
233 | font-weight: bold;
234 | text-rendering: optimizeLegibility;
235 | font-size: 2.25rem;
236 | line-height: 1.1;
237 | }
238 | h2 {
239 | margin-left: 0;
240 | margin-right: 0;
241 | margin-top: 0;
242 | padding-bottom: 0;
243 | padding-left: 0;
244 | padding-right: 0;
245 | padding-top: 0;
246 | margin-bottom: 1.45rem;
247 | color: inherit;
248 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
249 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
250 | font-weight: bold;
251 | text-rendering: optimizeLegibility;
252 | font-size: 1.62671rem;
253 | line-height: 1.1;
254 | }
255 | h3 {
256 | margin-left: 0;
257 | margin-right: 0;
258 | margin-top: 0;
259 | padding-bottom: 0;
260 | padding-left: 0;
261 | padding-right: 0;
262 | padding-top: 0;
263 | margin-bottom: 1.45rem;
264 | color: inherit;
265 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
266 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
267 | font-weight: bold;
268 | text-rendering: optimizeLegibility;
269 | font-size: 1.38316rem;
270 | line-height: 1.1;
271 | }
272 | h4 {
273 | margin-left: 0;
274 | margin-right: 0;
275 | margin-top: 0;
276 | padding-bottom: 0;
277 | padding-left: 0;
278 | padding-right: 0;
279 | padding-top: 0;
280 | margin-bottom: 1.45rem;
281 | color: inherit;
282 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
283 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
284 | font-weight: bold;
285 | text-rendering: optimizeLegibility;
286 | font-size: 1rem;
287 | line-height: 1.1;
288 | }
289 | h5 {
290 | margin-left: 0;
291 | margin-right: 0;
292 | margin-top: 0;
293 | padding-bottom: 0;
294 | padding-left: 0;
295 | padding-right: 0;
296 | padding-top: 0;
297 | margin-bottom: 1.45rem;
298 | color: inherit;
299 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
300 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
301 | font-weight: bold;
302 | text-rendering: optimizeLegibility;
303 | font-size: 0.85028rem;
304 | line-height: 1.1;
305 | }
306 | h6 {
307 | margin-left: 0;
308 | margin-right: 0;
309 | margin-top: 0;
310 | padding-bottom: 0;
311 | padding-left: 0;
312 | padding-right: 0;
313 | padding-top: 0;
314 | margin-bottom: 1.45rem;
315 | color: inherit;
316 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
317 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
318 | font-weight: bold;
319 | text-rendering: optimizeLegibility;
320 | font-size: 0.78405rem;
321 | line-height: 1.1;
322 | }
323 | hgroup {
324 | margin-left: 0;
325 | margin-right: 0;
326 | margin-top: 0;
327 | padding-bottom: 0;
328 | padding-left: 0;
329 | padding-right: 0;
330 | padding-top: 0;
331 | margin-bottom: 1.45rem;
332 | }
333 | ul {
334 | margin-left: 1.45rem;
335 | margin-right: 0;
336 | margin-top: 0;
337 | padding-bottom: 0;
338 | padding-left: 0;
339 | padding-right: 0;
340 | padding-top: 0;
341 | margin-bottom: 1.45rem;
342 | list-style-position: outside;
343 | list-style-image: none;
344 | }
345 | ol {
346 | margin-left: 1.45rem;
347 | margin-right: 0;
348 | margin-top: 0;
349 | padding-bottom: 0;
350 | padding-left: 0;
351 | padding-right: 0;
352 | padding-top: 0;
353 | margin-bottom: 1.45rem;
354 | list-style-position: outside;
355 | list-style-image: none;
356 | }
357 | dl {
358 | margin-left: 0;
359 | margin-right: 0;
360 | margin-top: 0;
361 | padding-bottom: 0;
362 | padding-left: 0;
363 | padding-right: 0;
364 | padding-top: 0;
365 | margin-bottom: 1.45rem;
366 | }
367 | dd {
368 | margin-left: 0;
369 | margin-right: 0;
370 | margin-top: 0;
371 | padding-bottom: 0;
372 | padding-left: 0;
373 | padding-right: 0;
374 | padding-top: 0;
375 | margin-bottom: 1.45rem;
376 | }
377 | p {
378 | margin-left: 0;
379 | margin-right: 0;
380 | margin-top: 0;
381 | padding-bottom: 0;
382 | padding-left: 0;
383 | padding-right: 0;
384 | padding-top: 0;
385 | margin-bottom: 1.45rem;
386 | }
387 | figure {
388 | margin-left: 0;
389 | margin-right: 0;
390 | margin-top: 0;
391 | padding-bottom: 0;
392 | padding-left: 0;
393 | padding-right: 0;
394 | padding-top: 0;
395 | margin-bottom: 1.45rem;
396 | }
397 | pre {
398 | margin-left: 0;
399 | margin-right: 0;
400 | margin-top: 0;
401 | padding-bottom: 0;
402 | padding-left: 0;
403 | padding-right: 0;
404 | padding-top: 0;
405 | margin-bottom: 1.45rem;
406 | font-size: 0.85rem;
407 | line-height: 1.42;
408 | background: hsla(0, 0%, 0%, 0.04);
409 | border-radius: 3px;
410 | overflow: auto;
411 | word-wrap: normal;
412 | padding: 1.45rem;
413 | }
414 | table {
415 | margin-left: 0;
416 | margin-right: 0;
417 | margin-top: 0;
418 | padding-bottom: 0;
419 | padding-left: 0;
420 | padding-right: 0;
421 | padding-top: 0;
422 | margin-bottom: 1.45rem;
423 | font-size: 1rem;
424 | line-height: 1.45rem;
425 | border-collapse: collapse;
426 | width: 100%;
427 | }
428 | fieldset {
429 | margin-left: 0;
430 | margin-right: 0;
431 | margin-top: 0;
432 | padding-bottom: 0;
433 | padding-left: 0;
434 | padding-right: 0;
435 | padding-top: 0;
436 | margin-bottom: 1.45rem;
437 | }
438 | blockquote {
439 | margin-left: 1.45rem;
440 | margin-right: 1.45rem;
441 | margin-top: 0;
442 | padding-bottom: 0;
443 | padding-left: 0;
444 | padding-right: 0;
445 | padding-top: 0;
446 | margin-bottom: 1.45rem;
447 | }
448 | form {
449 | margin-left: 0;
450 | margin-right: 0;
451 | margin-top: 0;
452 | padding-bottom: 0;
453 | padding-left: 0;
454 | padding-right: 0;
455 | padding-top: 0;
456 | margin-bottom: 1.45rem;
457 | }
458 | noscript {
459 | margin-left: 0;
460 | margin-right: 0;
461 | margin-top: 0;
462 | padding-bottom: 0;
463 | padding-left: 0;
464 | padding-right: 0;
465 | padding-top: 0;
466 | margin-bottom: 1.45rem;
467 | }
468 | iframe {
469 | margin-left: 0;
470 | margin-right: 0;
471 | margin-top: 0;
472 | padding-bottom: 0;
473 | padding-left: 0;
474 | padding-right: 0;
475 | padding-top: 0;
476 | margin-bottom: 1.45rem;
477 | }
478 | hr {
479 | margin-left: 0;
480 | margin-right: 0;
481 | margin-top: 0;
482 | padding-bottom: 0;
483 | padding-left: 0;
484 | padding-right: 0;
485 | padding-top: 0;
486 | margin-bottom: calc(1.45rem - 1px);
487 | background: hsla(0, 0%, 0%, 0.2);
488 | border: none;
489 | height: 1px;
490 | }
491 | address {
492 | margin-left: 0;
493 | margin-right: 0;
494 | margin-top: 0;
495 | padding-bottom: 0;
496 | padding-left: 0;
497 | padding-right: 0;
498 | padding-top: 0;
499 | margin-bottom: 1.45rem;
500 | }
501 | b {
502 | font-weight: bold;
503 | }
504 | strong {
505 | font-weight: bold;
506 | }
507 | dt {
508 | font-weight: bold;
509 | }
510 | th {
511 | font-weight: bold;
512 | }
513 | li {
514 | margin-bottom: calc(1.45rem / 2);
515 | }
516 | ol li {
517 | padding-left: 0;
518 | }
519 | ul li {
520 | padding-left: 0;
521 | }
522 | li > ol {
523 | margin-left: 1.45rem;
524 | margin-bottom: calc(1.45rem / 2);
525 | margin-top: calc(1.45rem / 2);
526 | }
527 | li > ul {
528 | margin-left: 1.45rem;
529 | margin-bottom: calc(1.45rem / 2);
530 | margin-top: calc(1.45rem / 2);
531 | }
532 | blockquote *:last-child {
533 | margin-bottom: 0;
534 | }
535 | li *:last-child {
536 | margin-bottom: 0;
537 | }
538 | p *:last-child {
539 | margin-bottom: 0;
540 | }
541 | li > p {
542 | margin-bottom: calc(1.45rem / 2);
543 | }
544 | code {
545 | font-size: 0.85rem;
546 | line-height: 1.45rem;
547 | }
548 | kbd {
549 | font-size: 0.85rem;
550 | line-height: 1.45rem;
551 | }
552 | samp {
553 | font-size: 0.85rem;
554 | line-height: 1.45rem;
555 | }
556 | abbr {
557 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
558 | cursor: help;
559 | }
560 | acronym {
561 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
562 | cursor: help;
563 | }
564 | abbr[title] {
565 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5);
566 | cursor: help;
567 | text-decoration: none;
568 | }
569 | thead {
570 | text-align: left;
571 | }
572 | td,
573 | th {
574 | text-align: left;
575 | border-bottom: 1px solid hsla(0, 0%, 0%, 0.12);
576 | font-feature-settings: "tnum";
577 | -moz-font-feature-settings: "tnum";
578 | -ms-font-feature-settings: "tnum";
579 | -webkit-font-feature-settings: "tnum";
580 | padding-left: 0.96667rem;
581 | padding-right: 0.96667rem;
582 | padding-top: 0.725rem;
583 | padding-bottom: calc(0.725rem - 1px);
584 | }
585 | th:first-child,
586 | td:first-child {
587 | padding-left: 0;
588 | }
589 | th:last-child,
590 | td:last-child {
591 | padding-right: 0;
592 | }
593 | tt,
594 | code {
595 | background-color: hsla(0, 0%, 0%, 0.04);
596 | border-radius: 3px;
597 | font-family: "SFMono-Regular", Consolas, "Roboto Mono", "Droid Sans Mono",
598 | "Liberation Mono", Menlo, Courier, monospace;
599 | padding: 0;
600 | padding-top: 0.2em;
601 | padding-bottom: 0.2em;
602 | }
603 | pre code {
604 | background: none;
605 | line-height: 1.42;
606 | }
607 | code:before,
608 | code:after,
609 | tt:before,
610 | tt:after {
611 | letter-spacing: -0.2em;
612 | content: " ";
613 | }
614 | pre code:before,
615 | pre code:after,
616 | pre tt:before,
617 | pre tt:after {
618 | content: "";
619 | }
620 | @media only screen and (max-width: 480px) {
621 | html {
622 | font-size: 100%;
623 | }
624 | }
625 |
--------------------------------------------------------------------------------
/src/images/bagley.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/src/images/bagley.jpg
--------------------------------------------------------------------------------
/src/images/blueicon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/src/images/blueicon.jpg
--------------------------------------------------------------------------------
/src/images/rainier-headshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/src/images/rainier-headshot.jpg
--------------------------------------------------------------------------------
/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Layout from '../components/site-chrome/layout'
4 | import SEO from '../components/site-chrome/seo'
5 |
6 | import RouteTargetHeading from "../components/better/route-target-heading.js"
7 |
8 | const NotFoundPage = () => (
9 |
10 |
11 |
15 | NOT FOUND
16 |
17 | You just hit a route that doesn't exist... the sadness.
18 |
19 | )
20 |
21 | export default NotFoundPage
22 |
--------------------------------------------------------------------------------
/src/pages/animation.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Layout from '../components/site-chrome/layout'
4 | import SEO from '../components/site-chrome/seo'
5 |
6 | import RouteTargetHeading from "../components/better/route-target-heading.js"
7 |
8 | import InaccessibleCardFlip from "../components/bad/card-flip"
9 | import BetterCardFlip from "../components/better/card-flip"
10 |
11 | import rainierImg from "../images/rainier-headshot.jpg"
12 | import bagleyImg from "../images/bagley.jpg"
13 |
14 | const friends = [{
15 | name: 'Rainier McCheddarton',
16 | headshot: rainierImg,
17 | subtitle: 'Labradoodle, squeaker, cheese fan',
18 | bio: 'Doggo ipsum very hand that feed shibe heckin good boys and girls fat boi much ruin diet you are doing me the shock wrinkler length boy, I am bekom fat lotsa pats dat tungg tho shooberino.',
19 | twitterLink: 'http://twitter.com'
20 | },
21 | {
22 | name: 'Bagley Fluffpants',
23 | headshot: bagleyImg,
24 | subtitle: 'Cheshire cat, supreme loaf',
25 | bio: "The door is opening! how exciting oh, it's you, meh drink water out of the faucet. Spend six hours per day washing, but still have a crusty butthole, so lies down or sit on human they not getting up ever, but lick human with sandpaper tongue.",
26 | twitterLink: 'http://twitter.com'
27 | }]
28 |
29 | const ReducedMotionDemoPage = () => (
30 |
31 |
32 |
33 |
34 | Reducing motion for accessibility
35 |
36 |
Let‘s build safe interfaces that delight.
37 |
38 |
39 |
Inaccessible animation demo
40 | [source 1 ]
41 |
48 |
49 |
50 |
Animation demo 2: add styles
51 | [source 2 ]
52 |
59 |
60 |
61 |
62 |
63 | )
64 |
65 | export default ReducedMotionDemoPage
66 |
--------------------------------------------------------------------------------
/src/pages/announcer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import Layout from '../components/site-chrome/layout'
4 | import SEO from '../components/site-chrome/seo'
5 |
6 | import RouteTargetHeading from "../components/better/route-target-heading.js"
7 |
8 | import InaccessibleAsyncFormDemo from "../components/bad/async-form"
9 | import LiveRegion from "../components/better/live-region"
10 |
11 | const LiveRegionDemoPage = () => {
12 | return (
13 |
14 |
15 |
16 |
17 | Live Region Demo
18 |
19 |
20 |
21 |
Inaccessible async form demo [source ]
22 |
23 |
24 |
25 |
More accessible async form demo
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | export default LiveRegionDemoPage
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/pages/dropdown.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import Layout from '../components/site-chrome/layout'
4 | import SEO from '../components/site-chrome/seo'
5 |
6 | import RouteTargetHeading from "../components/better/route-target-heading.js"
7 |
8 | import BadDropdown from '../components/bad/dropdown'
9 | import BetterDropdown from '../components/better/dropdown'
10 |
11 | const DropdownPage = () => (
12 |
13 |
14 |
15 |
16 | Dropdowns
17 |
18 |
Tuck controls into an expandible, collapsible, accessible menu. Consider, however, whether you really need a dropdown.
19 |
Keyboard interactions will depend on what's in the menu. List of links? The TAB key is fine. UI actions with a button or radio buttons? Script the arrow keys.
20 |
21 |
22 |
23 |
Bad link list dropdown [source ]
24 |
34 |
35 |
36 |
Better link list dropdown
37 |
47 |
48 |
49 |
50 |
51 | )
52 |
53 | export default DropdownPage
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/pages/enhanced-tablist.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import Layout from '../components/site-chrome/layout'
4 | import SEO from '../components/site-chrome/seo'
5 |
6 | import RouteTargetHeading from "../components/better/route-target-heading.js"
7 |
8 | import EnhancingList from "../components/better/tab-list"
9 |
10 | const ProgressiveEnhancementPage = () => {
11 | return (
12 |
13 |
14 |
15 |
16 | Progressive Enhancement Demo
17 |
18 |
The goal of an enhanced widget is to output plain HTML markup at build time, with ARIA role enhancements when JavaScript loads.
19 |
To test this kind of demo: run gatsby build && gatsby serve
and turn off JavaScript in your browser
20 |
[starter component source ]
21 |
25 |
26 |
27 | )
28 | }
29 |
30 | export default ProgressiveEnhancementPage
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/pages/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import Layout from '../components/site-chrome/layout'
4 | import SEO from '../components/site-chrome/seo'
5 |
6 | const IndexPage = () => (
7 |
8 |
9 |
10 |
11 | Let’s make the web more accessible!
12 |
13 |
Here are some web accessibility tools, resources, and books for web designers and developers.
14 |
15 |
16 |
30 |
31 |
Screen reader cheat sheets
32 |
37 |
38 |
Framework accessibility docs
39 |
46 |
47 |
Resources
48 |
65 |
Books
66 |
78 |
79 |
80 | )
81 |
82 | export default IndexPage
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/pages/layout.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import Layout from '../components/site-chrome/layout'
4 | import SEO from '../components/site-chrome/seo'
5 |
6 | import RouteTargetHeading from "../components/better/route-target-heading.js"
7 |
8 | const LayoutPage = () => (
9 |
10 |
11 |
12 |
13 | Semantics and CSS layout
14 |
15 |
CSS layouts are modern now. You can use CSS Grid and HTML5! Inspect these semantic layouts in DevTools and screen readers. Play with the rotor/elements list, heading navigation,
16 |
[page source ]
17 |
18 |
19 |
20 |
DIVs all the way down
21 |
27 |
28 |
29 |
Doggo ipsum stop it fren borking doggo shoober floofs, very jealous pupper thicc. Pats smol borking doggo with a long snoot for pats blop pupper, borking doggo wrinkler. Lotsa pats big ol pupper h*ck heckin angery woofer pupper, blop heckin mlem.
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | HTML5 and CSS Grid Layout
38 |
39 |
43 |
44 |
45 |
46 | Maximum borkdrive
47 | Very hand that feed shibe puggorino very taste wow. Maximum borkdrive much ruin diet very hand that feed shibe very taste wow long water shoob doge doggo heckin, shoober ruff shoob wrinkler bork much ruin diet. he made many woofs woofer. Wow such tempt mlem very hand that feed shibe porgo, wrinkler shoob. Vvv puggo doggorino very good spot such treat length boy puggo, woofer heckin angery woofer wow very biscit pupperino very taste wow.
48 |
49 |
50 |
51 |
52 |
53 | )
54 |
55 | export default LayoutPage
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/pages/semantics.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import Layout from '../components/site-chrome/layout'
4 | import SEO from '../components/site-chrome/seo'
5 |
6 | const SemanticsPage = () => (
7 |
8 |
9 |
10 |
11 |
12 |
13 | )
14 |
15 | export default SemanticsPage
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/slides/index.mdx:
--------------------------------------------------------------------------------
1 | import highlight from '@mdx-deck/themes/syntax-highlighter-prism'
2 |
3 | import Slide from "../templates/slide"
4 | import ImageSlide from "../templates/image-slide"
5 | import CodeBlock from "../components/site-chrome/code-figure"
6 | import AccessibleAnimationDemo from "../components/better/animation"
7 | import Autocomplete from "../components/better/downshift-autocomplete"
8 |
9 | import baseTheme from '../theme'
10 | export const themes = [baseTheme, highlight]
11 | const openModal = () => {
12 | document.querySelector('.modal-curtain').removeAttribute('hidden')
13 | }
14 | const buttonAction = () => {
15 | alert('Button clicked')
16 | }
17 |
18 |
19 |
20 | # Accessibility in JavaScript Applications
21 |
22 |
23 |
24 | ---
25 |
26 |
27 |
28 |
29 |
Who is Marcy Sutton?
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | @marcysutton ~ marcysutton.com
41 |
42 |
43 |
44 |
45 | ---
46 |
47 |
48 |
49 | # Accessibility
50 |
51 | Making the web more inclusive
52 | **_with_** _and_ **_for_ people with disabilities**
53 |
54 | http://bit.ly/microsoft-inclusive-toolkit
55 |
56 |
57 |
58 | ---
59 |
60 |
61 |
62 | # JavaScript Applications
63 |
64 | - Client-rendered: no traditional page reloads
65 | - Built with frameworks—React, Vue, Ember, Angular, etc.
66 | - Sometimes server-rendered with “hydration”
67 | - Challenges and opportunities
68 |
69 |
70 |
71 | ---
72 |
73 |
74 |
75 | # What we’ll cover
76 |
77 |
78 |
79 | - Accessibility debugging
80 | - Accessibility in JavaScript apps
81 | - Focus management
82 | - Client-side routing
83 | - Announcements
84 | - Semantic HTML
85 | - Unobtrusive motion
86 | - Progressive enhancement
87 | - Accessibility units
88 | - Accessible pages
89 | - Q & A
90 |
91 |
92 |
93 |
94 |
95 | ---
96 |
97 |
98 |
99 |
100 |
101 | # An app for today
102 |
103 |
104 |
105 | - Built with Gatsby and React
106 | - Server and client-rendered
107 | - Outputs HTML pages by default
108 | - Includes this slide deck
109 |
110 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | ---
126 |
127 |
128 |
129 |
130 |
131 | # Install time
132 |
133 |
134 |
135 | https://github.com/marcysutton/js-a11y-workshop
136 |
137 |
138 |
139 | ```shell
140 | git clone https://github.com/marcysutton/js-a11y-workshop.git
141 | cd js-a11y-workshop
142 | npm install
143 | npm run develop
144 | ```
145 |
146 |
147 |
148 | View the site locally: http://localhost:8000
149 |
150 | Online: https://marcysutton.github.io/js-a11y-workshop
151 |
152 |
153 |
154 | ---
155 |
156 |
157 |
158 | # Get some tools
159 |
160 | ⚒
161 |
162 | - [NVDA](https://nvaccess.org/download) (Windows)
163 | - [axe extensions](https://deque.com/axe)
164 | - [Accessibility Insights](https://accessibilityinsights.io)
165 | - [more](http://marcysutton.github.io/js-a11y-workshop#Testing-Tools)
166 |
167 |
168 |
169 | ---
170 |
171 |
172 |
173 | # Prototyping with [Parcel](https://parceljs.org/)
174 | Modern development with any JavaScript framework/library
175 |
176 |
177 |
178 | ```shell
179 | git clone https://github.com/marcysutton/parcel-prototype-scaffold parcel-demo
180 | cd parcel-demo
181 |
182 | npm install
183 | npm start
184 | ```
185 |
186 |
187 |
188 | Alternatively: Codepen, Codesandbox, etc.
189 |
190 |
191 |
192 | ---
193 |
194 |
195 |
196 | # Which.js?
197 |
198 | - React
199 | - Ember
200 | - Angular
201 | - Polymer
202 | - Vue
203 | - Svelte
204 | - Vanilla
205 |
206 |
207 |
208 | ---
209 |
210 |
211 |
212 | # It doesn’t matter.
213 | ## People just want to use your site
214 |
215 | 
216 |
217 |
218 |
219 | ---
220 |
221 |
222 |
223 | # Responsive design is good for accessibility
224 |
225 |
226 |
227 |
228 |
229 | ---
230 |
231 |
232 |
233 | # #A11y debugging
234 |
235 | - Render in a web browser
236 | - Test controls with the keyboard
237 | - Use accessibility web extensions
238 | - Check color contrast
239 | - Test with screen readers
240 | - Use magnification & zoom
241 |
242 |
243 |
244 | ---
245 |
246 |
247 |
248 | # Apps to test in the wild
249 |
250 |
251 | 🔍
252 |
253 |
254 | - [Slack JSON file viewer](https://gatsbyjs.slack.com/files/U964CL6GZ/FKPT589U4/feedback-with-comments.json?origin_team=T6VN36NMP)
255 | - [GitHub](https://github.com)
256 | - [Google Maps](https://www.google.com/maps)
257 |
258 |
259 |
260 | ---
261 |
262 |
263 |
264 | # Debugging in this Gatsby app
265 | Fire up a build in a browser and start testing
266 |
267 |
268 |
269 | ```shell
270 | $ npm run build
271 | $ npm run serve
272 | ```
273 |
274 |
275 |
276 | http://localhost:9000
277 |
278 |
279 |
280 | ---
281 |
282 |
283 |
284 | # Hidden vs. Visible CSS
285 |
286 |
287 | Demo button
288 |
289 |
290 |
291 |
292 | ```css
293 | .visually-hidden {
294 | border: 0;
295 | clip: rect(0 0 0 0);
296 | height: 1px;
297 | margin: -1px;
298 | overflow: hidden;
299 | padding: 0;
300 | position: absolute;
301 | width: 1px;
302 | }
303 | ```
304 |
305 |
306 |
307 |
308 |
309 | ```css
310 | .opacity {
311 | opacity: 0;
312 | }
313 | .displayNone {
314 | display: none;
315 | }
316 | .visibility {
317 | visibility: hidden;
318 | }
319 | ```
320 |
321 |
322 |
323 |
324 |
325 | ---
326 |
327 |
328 |
329 | # The accessibility tree
330 |
331 | ## A parallel structure to the DOM
332 |
333 | Uses platform Accessibility APIs to communicate page structure & content to assistive technologies
334 |
335 | 
336 |
337 |
338 |
339 | ---
340 |
341 |
342 |
343 | # Visualize with browser DevTools
344 |
345 | Semantic markup and CSS styles impact the accessibility tree
346 |
347 | 
348 |
349 |
350 |
351 | ---
352 |
353 |
354 |
355 | # Focus testing haxx
356 |
357 | ## Paste into browser devTools console
358 |
359 |
360 |
361 | ```js
362 | document.body.addEventListener('focusin', (event) => {
363 | console.log(document.activeElement)
364 | })
365 | ```
366 |
367 |
368 |
369 |
370 |
371 | ---
372 |
373 |
374 |
375 | # Safari, y u no focus?
376 |
377 |
378 |
379 |
380 |
381 | ---
382 |
383 |
384 |
385 | # Exercise 1
386 | ## Test pages with the keyboard
387 |
388 | 
389 |
390 |
391 |
392 | ---
393 |
394 |
395 |
396 | # Exercise 2
397 | ## Run a browser extension
398 |
399 | - [axe](https://deque.com/axe)
400 | - [Accessibility Insights](https://accessibilityinsights.io)
401 | - [Lighthouse](https://developers.google.com/web/tools/lighthouse/)
402 | - [WAVE](https://wave.webaim.org/extension/)
403 | - [NoCoffee](https://chrome.google.com/webstore/detail/nocoffee/jjeeggmbnhckmgdhmgdckeigabjfbddl?hl=en-US)
404 |
405 |
406 |
407 | ---
408 |
409 |
410 |
411 | # Exercise 3
412 | ## Test with accessibility devTools
413 |
414 | - [Chrome color picker & accessibility pane](https://developers.google.com/web/tools/chrome-devtools/accessibility/reference#pane)
415 | - [Firefox accessibility tools](https://developer.mozilla.org/en-US/docs/Tools/Accessibility_inspector#Accessing_the_Accessibility_Inspector)
416 |
417 |
418 |
419 | ---
420 |
421 |
422 |
423 | # Testing with Screen Readers
424 |
425 | Cheat sheets for getting started
426 | - https://webaim.org/articles/voiceover/
427 | - https://webaim.org/articles/nvda/
428 | - https://webaim.org/articles/jaws/
429 |
430 |
431 |
432 | ---
433 |
434 |
435 |
436 | # Exercise 4
437 | ## Screen reader testing
438 |
439 | - OSX Voiceover and Safari
440 | - NVDA and Firefox Windows
441 | - JAWS and IE11 or Edge
442 | - iOS Voiceover and Safari
443 | - Android Accessibility and Chrome
444 | - Orca on Linux
445 |
446 |
447 |
448 | ---
449 |
450 |
451 |
452 | # Magnification
453 | ## For users with low vision
454 |
455 | - Browser zoom (all the way: 500%!)
456 | - OS-level zoom
457 | - ZoomText & other assistive tech
458 |
459 |
460 |
461 | ---
462 |
463 |
464 |
465 | # Exercise 5
466 | ## Test with magnification
467 |
468 | Things to watch out for:
469 |
470 | - Page scrolling
471 | - Font sizes & scaling
472 | - UX of interactions when zoomed
473 |
474 |
475 |
476 | ---
477 |
478 |
479 |
480 | # Intro to Accessibility in JavaScript Applications
481 |
482 |
483 |
484 | - Focus management
485 | - Live Region announcements
486 | - Semantic HTML
487 | - Unobtrusive motion
488 | - Progressive enhancement
489 |
490 |
491 |
492 |
493 |
494 | ---
495 |
496 |
497 |
498 | # Focus management
499 |
500 | _Moving the user’s focus as part of an interaction to alert them to new content_
501 |
502 | _Also: handling focus in disabled and mutated parts of the page_
503 |
504 |
505 |
506 | ---
507 |
508 |
509 |
510 | # Focus management building blocks
511 |
512 | - Reachable and operable elements
513 | - TAB, escape, and arrow keys
514 | - Visible focus styles
515 | - Hidden/inert content
516 |
517 |
518 |
519 | ---
520 |
521 |
522 |
523 | # tabindex in HTML
524 |
525 | Make non-interactive elements focusable
526 |
527 |
528 | Demo DIV
529 |
530 |
531 |
532 |
533 | ```javascript
534 | tabIndex="0" // in the tab order. see following slides
535 | tabIndex="-1" // focusable by script, or removes from tab order
536 | tabIndex="99641" // all up in your tab order. hard to manage
537 | ```
538 |
539 |
540 |
541 |
542 | 💡 Screen readers go beyond the TAB key
543 |
544 |
545 |
546 |
547 | ---
548 |
549 |
550 |
551 | # tabindex + role + name
552 |
553 | Expose accessibility information for focusable elements.
554 |
555 |
556 |
557 | Demo DIV
558 |
559 |
560 |
561 |
562 |
563 | ```html
564 |
567 |
568 | ```
569 |
570 |
571 |
572 |
573 |
574 | ```js
575 |
576 | // focusable
577 | // a button widget, not a DIV
578 | // an accessible name
579 |
580 | ```
581 |
582 |
583 |
584 |
585 |
586 | 💡 Intended for custom interactive elements, not wrapper DIVs
587 |
588 |
589 |
590 |
591 |
592 | ---
593 |
594 |
595 |
596 | # tabindex + role + name + events
597 |
598 | Make custom controls fully interactive
599 |
600 |
601 |
602 | DIV button
603 |
604 |
605 |
606 |
607 |
608 | ```jsx
609 |
614 |
615 | ```
616 |
617 |
618 |
619 |
620 |
621 |
622 | ```jsx
623 | // or just use a button :)
624 |
626 |
627 | ```
628 |
629 |
630 |
631 |
632 |
633 | ---
634 |
635 |
636 |
637 | # ARIA: Accessible Rich Internet Applications
638 |
639 | https://www.w3.org/TR/wai-aria-1.1/
640 |
641 | - **role**: what is it?
642 | - **state**: what's happening to it?
643 | - **property**: what's the nature of it?
644 |
645 | The first [rule of ARIA](https://www.w3.org/TR/using-aria/) is _don’t use it_
646 |
647 |
648 |
649 | ---
650 |
651 |
652 |
653 | # Modal layers: disabling background content
654 |
655 |
656 |
Open curtain
657 |
660 |
661 |
662 | - `aria-hidden="true"` and `tabindex="-1"`
663 | - `inert` (+ [polyfill](https://github.com/WICG/inert))
664 | - CSS `display: none`
665 |
666 |
667 |
668 |
669 |
670 | ---
671 |
672 |
673 |
674 | # Focus management patterns
675 |
676 | - Dropdowns and menus
677 | - Layers and modals
678 | - View changes and deletes
679 | - Loading screens
680 |
681 |
682 |
683 | ---
684 |
685 |
686 |
687 | # Widget interactions: arrow keys or nah?
688 |
689 | It depends on the role and pattern ✨
690 |
691 | http://w3c.github.io/aria-practices/
692 |
693 |
694 |
695 | ---
696 |
697 |
698 |
699 | # Navigation vs. Actions
700 | ## a.k.a. Links vs. Buttons
701 |
702 |
705 |
706 |
707 |
708 | ```html
709 | Page
710 | ```
711 |
712 |
713 |
714 |
715 |
716 | ```jsx
717 | Thing
718 | ```
719 |
720 |
721 |
722 | https://marcysutton.com/links-vs-buttons-in-modern-web-applications
723 |
724 |
725 |
726 | ---
727 |
728 |
729 |
730 | # Visible focus styles
731 | Useful for so many people and situations, but often turned off for everyone
732 |
733 |
734 | Demo button
735 |
736 |
737 |
738 |
739 | ```css
740 | *:focus {
741 | outline: none;
742 | }
743 | ```
744 |
745 |
746 |
747 |
748 | ```css
749 | :hover, :focus {
750 | /* interaction styles here */
751 | outline: 5px auto blue;
752 | }
753 | ```
754 |
755 |
756 |
757 |
758 | ---
759 |
760 |
761 |
762 |
763 |
764 | # Styling input modalities
765 |
766 |
767 |
768 |
769 | Demo button
770 |
771 |
772 |
773 |
774 | ```css
775 | :focus:not(.focus-visible) {
776 | outline: 0;
777 | }
778 | :focus-visible {
779 | outline: 3px solid blue;
780 | }
781 | ```
782 |
783 |
784 |
785 |
786 | ```js
787 | // in a layout or component
788 | import 'what-input'
789 | ```
790 |
791 | ```css
792 | /* in your CSS */
793 | [data-whatintent='mouse'] *:focus {
794 | outline: none;
795 | }
796 | ```
797 |
798 |
799 | _An OS-level preference would be ideal_ 👍
800 |
801 |
802 |
803 | ---
804 |
805 |
806 |
807 |
808 |
809 | # CSS-in-JS
810 |
811 |
812 | Cached class names aren't user friendly, e.g. `class="sc-bdVaJa"`
813 |
814 |
815 |
816 | ```jsx
817 | import React from 'react'
818 | import { styled } from 'styled-components'
819 | const MegaHeader = styled.header`
820 | background-color: rebeccapurple;
821 | `
822 | const Layout = () => (
823 | // add a stable CSS class
824 |
825 | My Gatsby Site
826 |
827 | )
828 | ```
829 |
830 |
831 |
832 |
833 |
834 | ---
835 |
836 |
837 |
838 | # Dropdown Demo
839 |
840 | http://localhost:8000/dropdown
841 |
842 |
843 |
844 | ---
845 |
846 |
847 |
848 | # Exercise
849 | ## Create an accessible dropdown
850 |
851 | - React component shell: [dropdown.js](https://github.com/marcysutton/js-a11y-workshop/blob/master/src/components/better/dropdown.js)
852 | - vanilla.js shell: https://codepen.io/marcysutton/pen/aeJdNq
853 |
854 |
855 |
856 | ---
857 |
858 |
859 |
860 | # Dropdown answers
861 |
862 | - React: [dropdown component](https://github.com/marcysutton/js-a11y-workshop/blob/master/examples/dropdown/)
863 | - vanilla.js: https://codepen.io/marcysutton/pen/JgjYVv
864 |
865 |
866 |
867 | ---
868 |
869 |
870 |
871 |
872 |
873 | # Client-side routing
874 |
875 | Navigation where JavaScript controls browser history and dynamically maps URLs to each page or view
876 |
877 | 
878 |
879 | [User testing research on the Gatsby blog](https://www.gatsbyjs.org/blog/2019-07-11-user-testing-accessible-client-routing/)
880 |
881 |
882 |
883 |
884 |
885 | ---
886 |
887 |
888 |
889 | # Routing approach
890 |
891 | - A small UI control in each view target, like a skip link
892 | - Label with nearby content and its action, e.g. "Portfolio, skip to navigation"
893 | - When a user clicks a nav link, move focus to this control
894 |
895 | 
896 |
897 |
898 |
899 | ---
900 |
901 |
902 |
903 | # Focus management patterns: underlying tech
904 |
905 | - Access DOM nodes with [React refs](https://reactjs.org/docs/refs-and-the-dom.html)
906 | - New: [React FocusScopes](https://twitter.com/devongovett/status/1100829054800846848)
907 | - [Vue.js $refs](https://vuejs.org/v2/api/#vm-refs)
908 | - [Ember suggestions](https://twitter.com/marcysutton/status/1154942698408648704)
909 | - Other framework APIs?
910 | - Custom focus manager APIs
911 |
912 |
913 |
914 | ---
915 |
916 |
917 |
918 | # Client-side routing demo
919 |
920 | http://localhost:8000
921 |
922 |
923 |
924 | ---
925 |
926 |
927 |
928 | # Exercise
929 | ## Focus on client-side route change
930 |
931 | - Gatsby [`onRouteUpdate` method](https://www.gatsbyjs.org/docs/browser-apis/#onRouteUpdate) and skip link focus
932 | - vanilla.js shell: https://codepen.io/marcysutton/pen/wVJeJQ
933 |
934 |
935 |
936 | ---
937 |
938 |
939 |
940 | # Client-side routing answers
941 |
942 | - Gatsby component: [examples/client-side-routing](https://github.com/marcysutton/js-a11y-workshop/tree/master/examples/client-side-routing)
943 | - vanilla.js: https://codepen.io/marcysutton/pen/MNpmMd
944 |
945 |
946 |
947 | ---
948 |
949 |
950 |
951 | # Announcements using ARIA Live Regions
952 |
953 | 
954 |
955 |
956 |
957 | ---
958 |
959 |
960 |
961 | # Announcement patterns
962 | Notify assistive tech users without moving focus
963 |
964 | - Asynchronous save / update / etc.
965 | - Combobox usage / list filtering
966 | - Chat widgets
967 | - Title changes*
968 |
969 |
970 |
971 | ---
972 |
973 |
974 |
975 | # ARIA Live Regions
976 | Message command centers of varying importance
977 |
978 | https://www.w3.org/TR/wai-aria/#live_region_roles
979 |
980 |
981 |
982 | ```html
983 |
984 |
985 | ```
986 |
987 |
988 |
989 |
990 |
991 | ```html
992 |
993 |
994 | ```
995 |
996 |
997 |
998 |
999 |
1000 |
1001 | ---
1002 |
1003 |
1004 |
1005 | # Live Region Example
1006 |
1007 |
1010 |
1011 |
1014 |
1015 |
1016 |
1017 | ---
1018 |
1019 |
1020 |
1021 | # Live Region Gotchas & Tips
1022 |
1023 | - Include [multiple regions](https://github.com/dequelabs/ngA11y) for stubborn situations
1024 | - Politeness levels depend on the use case
1025 | - Site-level announcement manager APIs 👍
1026 |
1027 | https://github.com/AlmeroSteyn/react-aria-live
1028 |
1029 |
1030 |
1031 | ---
1032 |
1033 |
1034 |
1035 | # Live region demo
1036 |
1037 | http://localhost:8000/announcer
1038 |
1039 |
1040 |
1041 | ---
1042 |
1043 |
1044 |
1045 | # Exercise
1046 | ## Build a live region
1047 |
1048 | - React.js shell: [live-region](https://github.com/marcysutton/js-a11y-workshop/tree/master/src/components/better/live-region.js)
1049 | - vanilla.js shell: https://codepen.io/marcysutton/pen/xvqdzx
1050 |
1051 |
1052 |
1053 | ---
1054 |
1055 |
1056 |
1057 | # Live region answers
1058 |
1059 | - React component: [examples/live-region](https://github.com/marcysutton/js-a11y-workshop/tree/master/examples/live-region)
1060 | - vanilla.js: https://codepen.io/marcysutton/pen/ZgeKaV
1061 |
1062 |
1063 |
1064 | ---
1065 |
1066 |
1067 |
1068 | # Semantic HTML ✨
1069 |
1070 | - Use headings & landmarks
1071 | - Start wih native UI controls
1072 | - Build semantics into templates
1073 | - Verify assistive tech output
1074 |
1075 | [WebAIM: Semantic Structure](https://webaim.org/techniques/semanticstructure/)
1076 |
1077 |
1078 |
1079 | ---
1080 |
1081 |
1082 |
1083 | # Why semantics are important
1084 |
1085 | > _Headings save time, and programattic information is useful_
1086 |
1087 |
1088 | Semantic HTML communicates what's on a page to users of assistive technology, reader modes, conversational UIs, search engines, and more
1089 |
1090 |
1091 |
1092 |
1093 | ---
1094 |
1095 |
1096 |
1097 |
1098 | # Semantics IRL
1099 |
1100 |
1101 | 
1102 |
1103 |
1104 |
1105 | ---
1106 |
1107 |
1108 |
1109 | # Tools for testing semantics
1110 |
1111 | - [Accessibility Insights: Headings](https://accessibilityinsights.io)
1112 | - [Firefox Web Developer Extension](https://addons.mozilla.org/en-US/firefox/addon/web-developer/): _Information > View Document Outline_
1113 | - [NVDA heading navigation, elements list](https://dequeuniversity.com/screenreaders/nvda-keyboard-shortcuts#nvda-the_basics)
1114 | - [Voiceover rotor](https://dequeuniversity.com/screenreaders/voiceover-keyboard-shortcuts#vo-mac-the-rotor)
1115 | - Browser reader modes
1116 |
1117 |
1118 |
1119 | ---
1120 |
1121 |
1122 |
1123 | # Semantics demo
1124 |
1125 | http://localhost:8000/layout
1126 |
1127 |
1128 |
1129 | ---
1130 |
1131 |
1132 |
1133 | # Practice Problem
1134 | ## Use semantic markup
1135 |
1136 | Add markup to a blank page and test it using Accessibility Insights.
1137 |
1138 | - Gatsby page shell: [pages/semantics.jsx](https://github.com/marcysutton/js-a11y-workshop/blob/master/src/pages/semantics.jsx)
1139 |
1140 |
1141 |
1142 | ---
1143 |
1144 |
1145 |
1146 | # Unobtrusive motion
1147 |
1148 | Building safe _and_ delightful interfaces
1149 |
1150 | - `prefers-reduced-motion` CSS
1151 | - media, animation playback controls
1152 | - opt-in for autoplay
1153 |
1154 | https://source.opennews.org/articles/motion-sick/
1155 |
1156 |
1157 |
1158 | ---
1159 |
1160 |
1161 |
1162 | # Animation demo
1163 |
1164 |
1165 |
1166 | https://codepen.io/marcysutton/pen/yqVVeY
1167 |
1168 |
1169 |
1170 | ---
1171 |
1172 |
1173 |
1174 | # prefers-reduced-motion
1175 |
1176 |
1177 |
1178 | ```css
1179 | @media (prefers-reduced-motion: reduce) {
1180 | .animation {
1181 | animation: none;
1182 | transition: none;
1183 | }
1184 | }
1185 | ```
1186 |
1187 |
1188 |
1189 |
1190 |
1191 | ```js
1192 | var motionQuery = matchMedia('(prefers-reduced-motion)');
1193 | function handleReduceMotionChanged() {
1194 | if (motionQuery.matches) {
1195 | /* adjust 'transition' or 'animation' properties */
1196 | } else {
1197 | /* standard motion */
1198 | }
1199 | }
1200 | motionQuery.addListener(handleReduceMotionChanged);
1201 | handleReduceMotionChanged(); // trigger once on load
1202 | ```
1203 |
1204 |
1205 |
1206 | [WebKit guide](https://webkit.org/blog/7551/responsive-design-for-motion/) ~ [MDN docs](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion)
1207 |
1208 |
1209 |
1210 |
1211 | ---
1212 |
1213 |
1214 |
1215 | # reduced-motion demo
1216 |
1217 | http://localhost:8000/animation
1218 |
1219 |
1220 |
1221 | ---
1222 |
1223 |
1224 |
1225 | # Exercise!
1226 |
1227 | Play with reduced-motion and CSS animation or transitions
1228 |
1229 | - React component shell: [card-flip.js](https://github.com/marcysutton/js-a11y-workshop/blob/master/src/components/better/card-flip.js)
1230 | - vanilla.js shell: https://codepen.io/marcysutton/pen/OKppJe
1231 |
1232 |
1233 |
1234 | ---
1235 |
1236 |
1237 |
1238 | # Reduced-motion answers
1239 |
1240 | - React: [examples/reduced-motion](https://github.com/marcysutton/js-a11y-workshop/blob/master/examples/reduced-motion)
1241 | - vanilla.js: https://codepen.io/marcysutton/pen/yqVVeY
1242 |
1243 |
1244 |
1245 | ---
1246 |
1247 |
1248 |
1249 | # Progressive enhancement
1250 |
1251 | Emphasize core web page content first, then add layers of presentation and features on top as browsers/network connections allow
1252 |
1253 |
1254 |
1255 | Turn off JavaScript
1256 | Provide accessible baseline markup
1257 | Add ARIA with scripting
1258 | Prioritize core user flows
1259 |
1260 |
1261 |
1262 |
1263 |
1264 |
1265 | ---
1266 |
1267 |
1268 |
1269 | # Gatsby & SSR
1270 |
1271 | Output static HTML by default at build time
1272 |
1273 |
1274 |
1275 | ```bash
1276 | gatsby build
1277 | ```
1278 |
1279 |
1280 |
1281 |
1282 |
1283 | ---
1284 |
1285 |
1286 |
1287 | # Gatsby progressive enhancement demo
1288 |
1289 | http://localhost:8000/enhanced/tablist
1290 |
1291 |
1292 |
1293 | ---
1294 |
1295 |
1296 |
1297 | # Exercise!
1298 |
1299 | Play with progressive enhancement
1300 |
1301 | - React component shell: [components/tab-list.js](https://github.com/marcysutton/js-a11y-workshop/blob/master/src/components/better/tab-list.js)
1302 | - vanilla.js starter: https://codepen.io/marcysutton/pen/WBvRxq
1303 |
1304 |
1305 |
1306 | ---
1307 |
1308 |
1309 |
1310 | # Progressive enhancement answers
1311 |
1312 | - React component: [examples/tablist.js](https://github.com/marcysutton/js-a11y-workshop/blob/master/examples/progressive-enhancement)
1313 | - vanilla.js: https://codepen.io/marcysutton/pen/oKBzdr
1314 |
1315 |
1316 |
1317 | ---
1318 |
1319 |
1320 |
1321 | # Testing 🧪
1322 |
1323 | Improving quality in development
1324 |
1325 | - linting
1326 | - unit tests
1327 | - integration/end-to-end
1328 | - accessibility test APIs
1329 | - continuous integration
1330 | - manual & user testing
1331 |
1332 |
1333 |
1334 | ---
1335 |
1336 |
1337 |
1338 | # Linting
1339 |
1340 | Testing for quality live in a file or on a commit
1341 |
1342 | [eslint-plugin-jsx-a11y](https://github.com/evcohen/eslint-plugin-jsx-a11y)
1343 |
1344 | - you might need to [reconfigure rules](https://www.gatsbyjs.org/docs/making-your-site-accessible/#linting-with-eslint-jsx-plugin-a11y)
1345 | - CSS can have an impact
1346 | - rendering makes a difference
1347 |
1348 |
1349 |
1350 | ---
1351 |
1352 |
1353 |
1354 | # Accessibility Units
1355 |
1356 | ## component-level API concerns
1357 |
1358 | 🧩
1359 |
1360 |
1361 |
1362 | ---
1363 |
1364 |
1365 |
1366 | # Unit test approach
1367 |
1368 | - Test code in isolation
1369 | - Stub inputs/fixture data
1370 | - Often headless
1371 | - Fast changing of state
1372 |
1373 |
1374 |
1375 | ---
1376 |
1377 |
1378 |
1379 | # Accessibility unit tests
1380 |
1381 | - Component-specific behavior
1382 | - Interaction/focus APIs
1383 | - Text alternatives
1384 | - ARIA states
1385 |
1386 | Article: [Writing Automated tests for Accessibility](https://www.deque.com/blog/writing-automated-tests-accessibility/)
1387 |
1388 |
1389 |
1390 | ---
1391 |
1392 |
1393 |
1394 | # Unit test demo
1395 |
1396 | - Dropdown with Jest: [\__tests\__](https://github.com/marcysutton/js-a11y-workshop/tree/master/src/components/better/__tests__)
1397 |
1398 |
1399 |
1400 | ---
1401 |
1402 |
1403 |
1404 | # Exercise
1405 |
1406 | Write an accessibility unit test!
1407 |
1408 | - Jest test shell: [dropdown.test.js](https://github.com/marcysutton/js-a11y-workshop/tree/master/src/components/better/__tests__/dropdown.test.js)
1409 | - Jasmine shell: https://codepen.io/marcysutton/pen/OKpVKZ?editors=1010
1410 | - https://testing-library.com/docs/dom-testing-library
1411 | - https://github.com/testing-library/jest-dom
1412 |
1413 |
1414 |
1415 | ---
1416 |
1417 |
1418 |
1419 | # Unit testing answers
1420 |
1421 | - Jest test: [dropdown.test.js](https://github.com/marcysutton/js-a11y-workshop/tree/master/examples/unit-testing/dropdown.test.js)
1422 | - Jasmine test: [Modal dialog](https://codepen.io/marcysutton/pen/YbXZYK?editors=1010)
1423 |
1424 |
1425 |
1426 | ---
1427 |
1428 |
1429 |
1430 | # Accessible Pages
1431 | ## interroperability of components
1432 |
1433 | 📰
1434 |
1435 |
1436 |
1437 | ---
1438 |
1439 |
1440 |
1441 | # Integration & end-to-end tests
1442 |
1443 | - Real-world browser testing
1444 | - Document/page-level rules
1445 | - Widget interrop
1446 | - Color contrast
1447 | - Framework flexibility
1448 |
1449 |
1450 |
1451 | ---
1452 |
1453 |
1454 |
1455 | # [Selenium Webdriver example](https://github.com/marcysutton/axe-webdriverjs-demo)
1456 |
1457 |
1458 |
1459 | ```js
1460 | const WebDriver = require('selenium-webdriver')
1461 |
1462 | describe('Keyboard tests', () => {
1463 | let driver = new selenium.Builder().forBrowser('chrome').build()
1464 | driver.get('http://localhost:4000').then(() => done())
1465 |
1466 | it('should change state with the keyboard', () => {
1467 | var selector = 'span[role="radio"][aria-labelledby="radiogroup-0-label-0"]';
1468 |
1469 | driver.findElement(selenium.By.css(selector))
1470 | .then((element) => {
1471 | element.sendKeys(Key.SPACE)
1472 | return element
1473 | })
1474 | .then((element) => element.getAttribute('aria-checked'))
1475 | .then((attr) => {
1476 | expect(attr).toEqual('true')
1477 | })
1478 | })
1479 | })
1480 | ```
1481 |
1482 |
1483 |
1484 |
1485 | ---
1486 |
1487 |
1488 |
1489 | # [Cypress example](https://www.gatsbyjs.org/docs/end-to-end-testing/)
1490 |
1491 |
1492 |
1493 | ```js
1494 | ///
1495 | describe("Accessibility checks", () => {
1496 | beforeEach(() => {
1497 | cy.visit("/")
1498 | cy.wait(500)
1499 | })
1500 | it("Checks if footer link is focusable and has the correct attributes", () => {
1501 | cy.getAllByText("Gatsby").focus()
1502 | cy.focused()
1503 | .should("have.text", "Gatsby")
1504 | .should("have.attr", "href", "https://www.gatsbyjs.org")
1505 | .should("not.have.css", "outline-width", "0px")
1506 | })
1507 | })
1508 | ```
1509 |
1510 |
1511 |
1512 |
1513 |
1514 | ---
1515 |
1516 |
1517 |
1518 | # _No matter what kind of test: focus on the outcome, not the code implementation_ ✨
1519 |
1520 |
1521 |
1522 | ---
1523 |
1524 |
1525 |
1526 | # Using an A11y test API
1527 | ## Rules you don’t have to write
1528 |
1529 |
1530 |
1531 | - [axe-core](https://github.com/dequelabs/axe-core)
1532 | - [cypress-axe](https://www.npmjs.com/package/cypress-axe)
1533 | - [axe-webdriverjs](https://github.com/dequelabs/axe-webdriverjs)
1534 |
1535 |
1536 |
1537 |
1538 | - [WAVE](https://wave.webaim.org/api/)
1539 | - [accessibilityjs](https://github.com/github/accessibilityjs)
1540 | - [Lighthouse](https://developers.google.com/web/tools/lighthouse/)*
1541 |
1542 |
1543 |
1544 |
1545 |
1546 | ---
1547 |
1548 |
1549 |
1550 | # What can we automate?
1551 |
1552 | Tests can catch roughly 30-50% of accessibility issues, depending on the rule set
1553 |
1554 | 👉 _Screen readers can’t be automated_
1555 |
1556 |
1557 |
1558 | ---
1559 |
1560 |
1561 |
1562 |
1563 |
Write a combination of tests to catch a variety of bugs 💻 🔎 🐛
1564 |
1565 |
1566 |
1567 |
1568 | ---
1569 |
1570 |
1571 |
1572 | # Integration test demo
1573 |
1574 | [cypress/integration/index.js](https://github.com/marcysutton/js-a11y-workshop/blob/master/cypress/integration/index.js)
1575 |
1576 |
1577 |
1578 | ---
1579 |
1580 |
1581 |
1582 | # Exercise!
1583 |
1584 | Write an end-to-end accessibility test for keyboard interactions and/or with an accessibility test API
1585 |
1586 | - Cypress test shell: [index.js](https://github.com/marcysutton/js-a11y-workshop/blob/master/cypress/integration/index.js)
1587 | - Cypress API docs: https://cypress.io
1588 |
1589 |
1590 |
1591 | ---
1592 |
1593 |
1594 |
1595 | # Integration test answers
1596 |
1597 | - Cypress tests: [examples/integration-testing](https://github.com/marcysutton/js-a11y-workshop/blob/master/examples/integration-testing)
1598 |
1599 |
1600 |
1601 | ---
1602 |
1603 |
1604 |
1605 | # Continuous Integration
1606 |
1607 | Opportunities to test for accessibility on every commit, pull request, and deployment
1608 |
1609 | https://marcysutton.github.io/a11y-and-ci/
1610 |
1611 |
1612 |
1613 | ---
1614 |
1615 |
1616 |
1617 | # Track who broke the build
1618 | https://twitter.com/zqxwzq/status/868039653697482753
1619 |
1620 |
1621 |
1622 | 
1623 |
1624 |
1625 |
1626 |
1627 |
1628 | ---
1629 |
1630 |
1631 |
1632 | # Spoiler: we need to do more
1633 |
1634 | Automated testing and linting only gets us so far. Projects need manual human testing, too
1635 |
1636 | 🙇♀️
1637 |
1638 |
1639 |
1640 | ---
1641 |
1642 |
1643 |
1644 | # Manual testing advice
1645 | From Eric Bailey
1646 |
1647 | https://www.smashingmagazine.com/2018/09/importance-manual-accessibility-testing/
1648 |
1649 |
1650 |
1651 | ---
1652 |
1653 |
1654 |
1655 | # Accessibility User Testing
1656 |
1657 | - [Access Works](https://access-works.com/)
1658 | - [Fable Tech Labs](https://www.makeitfable.com/)
1659 |
1660 | https://inclusivedesign24.org/2019/
1661 |
1662 |
1663 |
1664 | ---
1665 |
1666 |
1667 |
1668 | # Q & A
1669 |
1670 |
1671 |
--------------------------------------------------------------------------------
/src/templates/image-slide.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | const ImageSlide = ({ heading, imageSrc, imageAlt }) => (
4 | <>
5 | {heading}
6 |
7 |
16 | >
17 | )
18 |
19 | export default ImageSlide
20 |
--------------------------------------------------------------------------------
/src/templates/slide.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { css } from "@emotion/core"
3 | import styled from "@emotion/styled"
4 |
5 | import SEO from "../components/site-chrome/seo"
6 | import theme from "../theme"
7 |
8 | const headerFooterStyles = css`
9 | background-color: ${theme.colors.headerFooterBackground};
10 | font-size: 1.25rem;
11 | left: 0;
12 | position: absolute;
13 | text-align: left;
14 | width: 100%;
15 | `
16 | const Header = styled.header`
17 | padding: 0.5em;
18 | top: 0;
19 | ${headerFooterStyles}
20 |
21 | a {
22 | color: ${theme.colors.link};
23 | display: flex;
24 | flex-direction: row;
25 | line-height: 26px;
26 |
27 | svg {
28 | display: block;
29 | height: 30px;
30 | margin-right: 0.5em;
31 | }
32 | }
33 | `
34 | const Footer = styled.footer`
35 | bottom: 0;
36 | padding: 0 0 1em 0;
37 | ${headerFooterStyles}
38 | `
39 | const Separator = styled.hr`
40 | background: red; /* For browsers that do not support gradients */
41 | background: -webkit-linear-gradient(left, orange , yellow, green, cyan, blue, violet); /* For Safari 5.1 to 6.0 */
42 | background: -o-linear-gradient(right, orange, yellow, green, cyan, blue, violet); /* For Opera 11.1 to 12.0 */
43 | background: -moz-linear-gradient(right, orange, yellow, green, cyan, blue, violet); /* For Firefox 3.6 to 15 */
44 | background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet); /* Standard syntax (must be last) */
45 | border: 0;
46 | display: block;
47 | height: 3px;
48 | margin: 0 0 1em;
49 | `
50 |
51 | const Main = styled.main`
52 | max-width: 95%;
53 | margin-left: auto;
54 | margin-right: auto;
55 | position: relative;
56 | `
57 |
58 | const P = styled.p`
59 | display: inline-block;
60 | font-size: inherit;
61 | margin-top: 0;
62 | margin-right: auto;
63 | `
64 | const FooterByline = styled.p`
65 | font-size: inherit;
66 | font-weight: normal;
67 | margin: 0;
68 | text-align: center;
69 | `
70 | export default ({ header = 'true', children }) => (
71 | <>
72 |
73 | { header === 'true' ?
83 | : null }
84 |
85 | {children}
86 |
87 |
88 |
89 |
90 | Workshop Slides: Accessibility in JavaScript Applications ~ by @marcysutton
91 |
92 |
93 | >
94 | )
95 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDeck } from 'gatsby-theme-mdx-deck'
3 |
4 | function Counter ({ children }) {
5 | const css = {
6 | fontSize: '1.15rem',
7 | padding: '0.5em',
8 | position: 'absolute',
9 | right: 0,
10 | top: 0,
11 | textAlign: 'right',
12 | }
13 |
14 | return
15 | {children}
16 |
17 | }
18 |
19 | const Provider = props => {
20 | const deck = useDeck()
21 | const { index, length } = deck
22 |
23 | return <>
24 | {props.children}
25 | {index}/{length}
26 | >
27 | }
28 |
29 | export default {
30 | styles: {
31 | body: {
32 | },
33 | Slide: {
34 | textAlign: 'center'
35 | },
36 | p: {
37 | fontSize: '2.25rem',
38 | margin: "1rem 0",
39 | textAlign: 'center'
40 | },
41 | h1: {
42 | fontSize: '4.5rem',
43 | marginBottom: "0.25rem"
44 | },
45 | h2: {
46 | fontSize: '4rem',
47 | margin: "0.5rem 0"
48 | },
49 | 'figure': {
50 | textAlign: 'left'
51 | },
52 | code: {
53 | fontSize: "inherit"
54 | },
55 | figcaption: {
56 | fontSize: "1.2rem"
57 | },
58 | li: {
59 | marginBottom: "0.5rem",
60 | textAlign: 'left'
61 | },
62 | },
63 | text: {
64 | heading: {
65 | margin: '0 auto 0.5rem',
66 | textAlign: 'center'
67 | },
68 | },
69 | colors: {
70 | headerFooterBackground: '#fff',
71 | text: '#000',
72 | background: '#fff',
73 | link: '#000',
74 | heading: '#000',
75 | quote: '#000',
76 | pre: '#333',
77 | preBackground: 'rgb(245, 242, 240)',
78 | code: '#333',
79 | codeBackground: 'transparent',
80 | figcaptionBackground: "rebeccapurple",
81 | figcaption: "#fff"
82 | },
83 | Provider
84 | }
85 |
--------------------------------------------------------------------------------
/static/a11y-octopus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/a11y-octopus.png
--------------------------------------------------------------------------------
/static/aea-grid-inspect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/aea-grid-inspect.png
--------------------------------------------------------------------------------
/static/angular-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/angular-logo.png
--------------------------------------------------------------------------------
/static/announcement.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/announcement.webp
--------------------------------------------------------------------------------
/static/axe-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/axe-logo.png
--------------------------------------------------------------------------------
/static/chrome-a11y-inspector.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/chrome-a11y-inspector.png
--------------------------------------------------------------------------------
/static/chrome-accessibility-tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/chrome-accessibility-tree.png
--------------------------------------------------------------------------------
/static/ci-bandits.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/ci-bandits.jpg
--------------------------------------------------------------------------------
/static/elmo-shrug.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/elmo-shrug.gif
--------------------------------------------------------------------------------
/static/finally.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/finally.webp
--------------------------------------------------------------------------------
/static/focus-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/focus-diagram.png
--------------------------------------------------------------------------------
/static/gatsby-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/gatsby-icon.png
--------------------------------------------------------------------------------
/static/keyboard.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/keyboard.gif
--------------------------------------------------------------------------------
/static/magnification-a11y.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/magnification-a11y.png
--------------------------------------------------------------------------------
/static/mail.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/mail.gif
--------------------------------------------------------------------------------
/static/marcysutton-sketch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/marcysutton-sketch.png
--------------------------------------------------------------------------------
/static/mythbusters.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/mythbusters.png
--------------------------------------------------------------------------------
/static/paul-adam-aria.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/paul-adam-aria.png
--------------------------------------------------------------------------------
/static/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/react-logo.png
--------------------------------------------------------------------------------
/static/safari-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcysutton/js-a11y-workshop/6401242645c1d1ea6a1fc84df6fda6f36a9e5d78/static/safari-settings.png
--------------------------------------------------------------------------------