From 40e8908307e3e9fb6639eba860441b6641852362 Mon Sep 17 00:00:00 2001 From: Marcy Sutton Date: Mon, 29 Jul 2019 15:03:22 -0700 Subject: [PATCH] add examples, set up '/better' to be blank slate --- cypress/integration/index.js | 20 - .../client-side-routing/gatsby-browser.js | 8 + examples/client-side-routing/header.js | 28 ++ examples/client-side-routing/page.jsx | 26 ++ .../route-target-heading.js | 12 + examples/dropdown/dropdown.js | 69 ++++ examples/integration-testing/nav.spec.js | 14 + examples/integration-testing/routing.js | 21 + examples/live-region/async-form.js | 52 +++ .../progressive-enhancement/tab-list.js | 4 +- examples/reduced-motion/animation.js | 151 +++++++ examples/unit-testing/dropdown.test.js | 71 ++++ gatsby-browser.js | 7 +- jest.config.js | 3 +- package-lock.json | 214 ++++++++++ package.json | 1 + src/components/bad/dropdown.js | 20 +- .../{dropdown.module.scss => dropdown.scss} | 8 +- .../better/__tests__/dropdown.test.js | 2 +- src/components/better/async-form.js | 47 +-- src/components/better/dropdown.js | 63 +-- .../{dropdown.module.scss => dropdown.scss} | 17 +- src/components/better/reduced-motion.js | 11 + src/components/better/tab-list.js | 17 + src/components/site-chrome/header.js | 2 +- src/components/site-chrome/layout.scss | 14 +- src/pages/animation.jsx | 9 +- src/pages/async-form.jsx | 9 +- src/pages/dropdown.jsx | 11 +- src/pages/enhanced-tablist.jsx | 9 +- src/pages/index.jsx | 10 +- src/pages/layout.jsx | 11 +- src/pages/semantics.jsx | 17 + src/slides/index.mdx | 371 +++++++++++++----- src/templates/slide.jsx | 6 +- static/safari-settings.png | Bin 0 -> 135561 bytes 36 files changed, 1045 insertions(+), 310 deletions(-) create mode 100644 examples/client-side-routing/gatsby-browser.js create mode 100644 examples/client-side-routing/header.js create mode 100644 examples/client-side-routing/page.jsx create mode 100644 examples/client-side-routing/route-target-heading.js create mode 100644 examples/dropdown/dropdown.js create mode 100644 examples/integration-testing/nav.spec.js create mode 100644 examples/integration-testing/routing.js create mode 100644 examples/live-region/async-form.js rename src/components/better/enhancing-list.js => examples/progressive-enhancement/tab-list.js (85%) create mode 100644 examples/reduced-motion/animation.js create mode 100644 examples/unit-testing/dropdown.test.js rename src/components/bad/{dropdown.module.scss => dropdown.scss} (91%) rename src/components/better/{dropdown.module.scss => dropdown.scss} (82%) create mode 100644 src/components/better/reduced-motion.js create mode 100644 src/components/better/tab-list.js create mode 100644 src/pages/semantics.jsx create mode 100644 static/safari-settings.png diff --git a/cypress/integration/index.js b/cypress/integration/index.js index 10dae13..137a1e0 100644 --- a/cypress/integration/index.js +++ b/cypress/integration/index.js @@ -1,21 +1 @@ /// -describe("Accessibility checks", () => { - beforeEach(() => { - }) - it("Has no detectable a11y violations on load", () => { - cy.visit("http://localhost:8000") - cy.injectAxe() - cy.wait(500) - cy.checkA11y() - }) - it("Handles focus on route change via click", () => { - cy.visit("http://localhost:8000") - cy.focused() - .should("not.have.class", "routeSkipLink") - - cy.get('#page-navigation').find('a').eq(0).click() - - cy.focused() - .should("have.class", "routeSkipLink") - }) -}) \ No newline at end of file diff --git a/examples/client-side-routing/gatsby-browser.js b/examples/client-side-routing/gatsby-browser.js new file mode 100644 index 0000000..40441cd --- /dev/null +++ b/examples/client-side-routing/gatsby-browser.js @@ -0,0 +1,8 @@ +exports.onRouteUpdate = ({ location, prevLocation }) => { + if (prevLocation !== null) { + const skipLink = document.querySelector('.routeSkipLink') + if (skipLink) { + skipLink.focus() + } + } +} \ No newline at end of file diff --git a/examples/client-side-routing/header.js b/examples/client-side-routing/header.js new file mode 100644 index 0000000..1a3fe83 --- /dev/null +++ b/examples/client-side-routing/header.js @@ -0,0 +1,28 @@ +import { Link } from 'gatsby' +import PropTypes from 'prop-types' +import React from 'react' + +const Header = ({ siteTitle }) => ( +
+ +

+ + {siteTitle} + +

+
+) + +Header.propTypes = { + siteTitle: PropTypes.string, +} + +Header.defaultProps = { + siteTitle: '', +} + +export default Header diff --git a/examples/client-side-routing/page.jsx b/examples/client-side-routing/page.jsx new file mode 100644 index 0000000..e0109d1 --- /dev/null +++ b/examples/client-side-routing/page.jsx @@ -0,0 +1,26 @@ +import React from "react" + +import Layout from '../components/site-chrome/layout' +import SEO from '../components/site-chrome/seo' + +import RouteTargetHeading from "./route-target-heading.js" + +const HeadingDemoPage = () => { + return ( + + +
+ + Heading Demo + +
+
+ ) +} + +export default HeadingDemoPage + + diff --git a/examples/client-side-routing/route-target-heading.js b/examples/client-side-routing/route-target-heading.js new file mode 100644 index 0000000..b53636b --- /dev/null +++ b/examples/client-side-routing/route-target-heading.js @@ -0,0 +1,12 @@ +import React from "react" +import { css } from "@emotion/core" + +const styles = css` + +` +const RouteHeading = () => { + return ( + <> + ) +} +export default RouteHeading diff --git a/examples/dropdown/dropdown.js b/examples/dropdown/dropdown.js new file mode 100644 index 0000000..504f1c5 --- /dev/null +++ b/examples/dropdown/dropdown.js @@ -0,0 +1,69 @@ +import React, { useState, useRef, useEffect } from "react" +import uuid from "uuid" + +import "./dropdown.scss" + +const Dropdown = ({ activatorText = 'Dropdown', items = [] }) => { + const [isOpen, setIsOpen] = useState(false) + const activatorRef = useRef(null) + const dropdownListRef = useRef(null) + + const wrapKeyHandler = (event) => { + if (event.keyCode === 27 && isOpen) { + // escape key + setIsOpen(false) + activatorRef.current.focus() + } + } + const clickHandler = () => { + setIsOpen(!isOpen) + } + const clickOutsideHandler = (event) => { + if (dropdownListRef.current.contains(event.target) || activatorRef.current.contains(event.target)) { + return + } + setIsOpen() + } + useEffect(() => { + if (isOpen) { + document.addEventListener('mousedown', clickOutsideHandler) + + dropdownListRef.current.querySelector('a').focus() + } else { + document.removeEventListener('mousedown', clickOutsideHandler) + } + + return () => { + document.removeEventListener('mousedown', clickOutsideHandler) + } + }, [isOpen]) + return ( +
+ + +
+ ) +} +export default Dropdown diff --git a/examples/integration-testing/nav.spec.js b/examples/integration-testing/nav.spec.js new file mode 100644 index 0000000..8223ad1 --- /dev/null +++ b/examples/integration-testing/nav.spec.js @@ -0,0 +1,14 @@ +context("Nav menu", () => { + beforeEach(() => { + cy.visit(`https://marcysutton.github.io/js-a11y-workshop`) + cy.injectAxe() + cy.wait(100) + }) + it("has no accessibility violations on load", () => { + cy.checkA11y() + }) + it("has a focusable, labeled button", () => { + cy.get("[aria-label='Open menu']").focus() + cy.focused().should("have.attr", "aria-label") + }) +}) \ No newline at end of file diff --git a/examples/integration-testing/routing.js b/examples/integration-testing/routing.js new file mode 100644 index 0000000..10dae13 --- /dev/null +++ b/examples/integration-testing/routing.js @@ -0,0 +1,21 @@ +/// +describe("Accessibility checks", () => { + beforeEach(() => { + }) + it("Has no detectable a11y violations on load", () => { + cy.visit("http://localhost:8000") + cy.injectAxe() + cy.wait(500) + cy.checkA11y() + }) + it("Handles focus on route change via click", () => { + cy.visit("http://localhost:8000") + cy.focused() + .should("not.have.class", "routeSkipLink") + + cy.get('#page-navigation').find('a').eq(0).click() + + cy.focused() + .should("have.class", "routeSkipLink") + }) +}) \ No newline at end of file diff --git a/examples/live-region/async-form.js b/examples/live-region/async-form.js new file mode 100644 index 0000000..fb46280 --- /dev/null +++ b/examples/live-region/async-form.js @@ -0,0 +1,52 @@ +import React, {useState} from "react" +import {DebounceInput} from 'react-debounce-input' + +const AccessibleAsyncForm = () => { + const [message, setMessage] = useState(null) + const [updating, setUpdating] = useState(false) + + const handleSubmit = (event) => { + event.preventDefault() + } + const handleTextChange = (value) => { + setUpdating(true) + } + const dismissToast = () => { + setUpdating(false) + } + return ( +
+