diff --git a/e2e/routes.ts b/e2e/routes.ts index f1ced8d7b..0522cdb85 100644 --- a/e2e/routes.ts +++ b/e2e/routes.ts @@ -93,6 +93,7 @@ export const A11Y_PAGES: string[] = [ // GENERATED:accessibility-nightmare-a11y "/adventures/accessibility-nightmare/", "/adventures/accessibility-nightmare/levels/beginner/", + "/adventures/accessibility-nightmare/levels/intermediate/", // /GENERATED:accessibility-nightmare-a11y ]; @@ -135,5 +136,6 @@ export const ROUTES_WITHOUT_FULL_COVERAGE: string[] = [ "/challenges/lighthouse/", "/challenges/wcag-2-2/", "/challenges/react/", + "/challenges/guidepup-virtual-screen-reader/", // /GENERATED:accessibility-nightmare-challenges ]; diff --git a/src/assets/diagrams/accessibility-nightmare-intermediate.svg b/src/assets/diagrams/accessibility-nightmare-intermediate.svg new file mode 100644 index 000000000..4a4743d0c --- /dev/null +++ b/src/assets/diagrams/accessibility-nightmare-intermediate.svg @@ -0,0 +1,120 @@ + + The Checkout Trap: the purchase journey and the three layers that check it + Buying something spans three surfaces. The product page holds the size + picker, adding to basket opens a confirmation dialog, and the checkout page holds the + delivery form. Each carries one fault: the picker exposes no role, name or value; the + confirmation never takes or holds the keyboard; the form's errors are never announced. + Three checks run against the same page. axe-core reports zero violations before and after + the repairs, because every fault is a behaviour rather than a property of the markup. A + Playwright keyboard walk and the Guidepup Virtual Screen Reader are what actually catch + them. + + + + + + + + + + + + THE PURCHASE, ONE ROUTE AT A TIME + + + + Product page + /#/product/running-shoes + + SizePicker.jsx + no role, no name, no value + Tab skips it entirely, so the + size cannot be changed at all + + + Add to basket + + + + Basket confirmation + opens over the page + + BasketDialog.jsx + not a dialog, loses focus + Tab walks out behind it, + Escape does nothing + + + Checkout + + + + Checkout page + /#/checkout + + CheckoutForm.jsx + errors never announced + red text on screen, silent to + a screen reader + + + + + React and Vite, port 5173 in the Dev Container, reached from the Ports tab + + You edit these three components and the pages that host them + + + THREE WAYS OF LOOKING AT THE SAME PAGE + + + ./verify.sh + runs the suite and + prints a checklist + + + + + Playwright + drives Chromium with + real key presses + + + + + + + axe-core + zero violations, before your repairs and after them + + + keyboard walk + asserts where focus actually lands at each step + + + Guidepup Virtual Screen Reader + reads back what would have been announced + + + + + Every fault above is a behaviour, not a property of the markup, which is why the scanner + never sees one of them. + + diff --git a/src/data/adventures/accessibility-nightmare/adventure.yaml b/src/data/adventures/accessibility-nightmare/adventure.yaml index c15ed06ff..838c58c2e 100644 --- a/src/data/adventures/accessibility-nightmare/adventure.yaml +++ b/src/data/adventures/accessibility-nightmare/adventure.yaml @@ -60,6 +60,10 @@ contributor: about: "Frontend developer with 4+ years building production apps in Next.js, React, and TypeScript. Open-source contributor to projects at Microsoft, W3C, and GitHub. Author of two npm packages: a 12-language RTL text engine and a TypeScript SVG mapping toolkit." +upcoming_levels: + - level: expert + name: Expert + difficulty: Expert levels: - level: beginner emoji: 🟢 @@ -189,10 +193,218 @@ levels: command: ./verify.sh description: Once you think you've solved the challenge, run the verification script. If it fails it will tell you which checks didn't pass. If it passes, it generates a Certificate of Completion you can paste into the discussion. -upcoming_levels: - level: intermediate - name: Intermediate - difficulty: Intermediate - - level: expert - name: Expert - difficulty: Expert + emoji: 🟡 + title: The Checkout Trap + devcontainer: accessibility-nightmare_intermediate + community_url: "" + topics: + - axe-core + - Playwright + - Guidepup Virtual Screen Reader + - WCAG 2.2 + - React + meta_description: Repair a React checkout that passes every automated accessibility scan and still cannot be used. Focus + management across a modal, the ARIA combobox pattern, and live-region error announcement, verified with Playwright + and a virtual screen reader. + summary: Repair the shared checkout components so keyboard and screen reader users can complete a purchase, on a page + where the automated scanner already reports no violations. + audience: Frontend developers who have run an accessibility scanner before and fixed what it reported. Comfortable with + React state, effects and refs, and ready for the faults that no scanner will hand them. + backstory: + - The homepage is fixed and every automated scan comes back green, so the team declared the problem solved. + - The complaints kept arriving, and support has traced all of them to the checkout. One customer adds a pair of + shoes to their basket, and the confirmation that appears never receives the keyboard. Their next Tab lands + somewhere behind it, on the page it is covering, and they carry on through controls they can no longer see. + - "Another fills in the delivery details, submits, and is told nothing at all: the error sits on screen in red, + and their screen reader never mentions it." + - The design team keeps shipping these same patterns, and nobody can see the problem, because the scanner insists + there isn't one. Fix the checkout before the next batch of complaints reaches legal. + objective: + - Complete the checkout end to end using only the keyboard. + - Have every step of that journey announced to a screen reader, including why a submission was rejected. + - Rebuild the size picker as a select-only combobox, so it exposes a role, a name and the size that is chosen. + - Keep the axe-core scan reporting no violations throughout. + what_you_learn: + - How to move, hold and restore focus across a modal boundary, and why hiding the background is not the same as + putting it out of reach. ([Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)) + - How to build a control that reports its own role, name, value and state, following the [select-only combobox + pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/). + - How a validation failure actually reaches a screen reader user, through [live + regions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) and the attributes + that bind a message to its field. + - Why an automated scan can report a clean page that no keyboard or screen reader user can operate. + ([Understanding WCAG 4.1.2](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value)) + architecture: + - A React and Vite storefront runs on port 5173 inside the Dev Container. The homepage arrives with the beginner + level's repairs already applied. The product page and the checkout do not. + - Buying something takes two pages. On /#/product/running-shoes you choose a size and add the item to your basket, + which opens a confirmation. From there /#/checkout collects the delivery details and places the order. + - "Three components carry the faults, and all three are what you edit: src/components/SizePicker.jsx, + src/components/BasketDialog.jsx and src/components/CheckoutForm.jsx. The pages that host them, + src/pages/Product.jsx and src/pages/Checkout.jsx, are yours to change too." + - Playwright drives the browser with real key presses and runs the [Guidepup Virtual Screen + Reader](https://github.com/guidepup/virtual-screen-reader) inside the page, reading back the log of what a + screen reader would announce. axe-core scans the same page and should stay silent from start to finish. + - Leave tests/ and verify.sh alone. They define the required outcomes and should not be weakened or bypassed. + - Do not add dependencies. Radix, React Aria and their equivalents solve the panel and the picker in a single + import, and reaching for a maintained library is the right call on real work. The ban exists because the + exercise is understanding what those libraries do for you. verify.sh checks that the dependency list is + unchanged. + toolbox: + - name: Guidepup Virtual Screen Reader + url: https://github.com/guidepup/virtual-screen-reader + description: Produces a text log of what a screen reader would announce. It is a simulation built from the accessibility + tree, not NVDA, JAWS or VoiceOver, and it will not reproduce the differences between them. Passing it means + the accessibility tree says the right thing, which is necessary but not sufficient. Test with a real screen + reader on real work. + - name: Playwright + url: https://playwright.dev/ + description: Drives real Tab, Escape and arrow presses and reports where focus actually lands. + - name: axe-core + url: https://github.com/dequelabs/axe-core + description: The automated scanner that reports this page as clean, before and after. + - name: ARIA Authoring Practices Guide + url: https://www.w3.org/WAI/ARIA/apg/patterns/ + description: The reference implementations for the dialog and combobox patterns. + services: + - name: ShopSmart + port: 5173 + description: The storefront. The product page is at /#/product/running-shoes and the checkout at /#/checkout. + how_to_play: + - id: start + title: Start ShopSmart + content: | + The storefront is already running. Open the **Ports** tab in the editor, + find **ShopSmart** on port 5173, and click the globe icon to open it in a + browser tab. Codespaces serves it from an address ending in + `.app.github.dev`, so there is no localhost to visit. + + If it is not running, start it from the level directory with `make app`. + + Add the product route to whatever address the Ports tab gave you: + + ```text + -5173.app.github.dev/#/product/running-shoes + ``` + + Use a browser tab rather than the editor's built-in preview. The preview + cannot load the forwarded port while it is private, because the request is + sent through GitHub's authentication and a preview pane has no way to + complete that. + + If you would rather work inside the preview, make the port public first: + in the Ports panel, right-click ShopSmart, choose Port Visibility, then + Public, and reload the preview. A public port can be reached by anyone who + has the URL, so turn it back to Private when you are finished. + + **On a Mac, turn keyboard navigation on before you start.** Safari does + not move focus to links and buttons unless it is enabled, under System + Settings, Keyboard, Keyboard navigation. Without it Tab appears to do + nothing at all here, which looks exactly like a broken page. Chrome and + Firefox have it on already. + + Buying something takes two pages: choose a size and add to basket there, + then go on to /#/checkout to place the order. + - id: explore + title: Explore the Broken Checkout + content: | + Run the checks first, and notice which one passes: + + ```bash + npm run test:a11y + ``` + + The axe-core scan is green. It was green before you arrived and it has to + stay green, so it will not be telling you what to fix. + + Then put the mouse away. From the product page, press Tab and keep + pressing it. Try to choose a size. Add the item to your basket and see + where the keyboard goes when the confirmation appears, where it goes next, + and what happens when you press Escape. Then go on to the checkout and + submit the form empty. + + To watch it live while you work, add `?listen` to the end of whatever is + in your address bar. It works either side of the route: + + ```text + -5173.app.github.dev/#/product/running-shoes?listen + ``` + + Or to print the same journey in the terminal: + + ```bash + npm run listen + ``` + + That walks the same journey and prints the announcements, naming any step + that changed the page without telling anyone about it. It grades nothing. + Use it as often as you like, and run it again after each repair to see + what changed. + - id: implement + title: Repair the Checkout + content: | + Take them smallest first. The checkout form is a handful of attributes and + two elements, the basket confirmation is a known pattern, and the size + picker is a component to build. Doing them in that order gets you three + separate wins before the long one. + + Work in: + + ```text + src/components/CheckoutForm.jsx + src/components/BasketDialog.jsx with src/pages/Product.jsx + src/components/SizePicker.jsx + ``` + + One thing is worth knowing before you start, because it costs hours and + teaches nothing: **a live region that appears at the same moment as its + text will never announce.** The element has to already be in the document, + empty, with its content filled in later. This is a React rendering + question rather than an accessibility one. + + Everything else is the exercise. Do not add dependencies, and do not + weaken the tests. + + `npm run listen` is there throughout, and costs nothing to run. + + When you are ready: + + ```bash + ./verify.sh + ``` + - id: reflect + title: Ask What the Scan Measured + content: | + The axe-core scan reported no violations before you started and reports + none now. Nothing you fixed was ever visible to it. + + What did the scan actually measure, and what would you have to add to + catch any of this automatically? That question is the expert level. + helpful_links: + - title: "ARIA Authoring Practices: Dialog (Modal)" + url: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ + description: The keyboard and focus contract a modal is expected to keep. + - title: "ARIA Authoring Practices: Select-Only Combobox" + url: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ + description: A worked listbox popup with no editable text input, which is the picker you need. + - title: "Understanding WCAG 4.1.3: Status Messages" + url: https://www.w3.org/WAI/WCAG22/Understanding/status-messages + description: Why a message that appears without moving focus has to be announced some other way. + - title: "Understanding WCAG 2.1.2: No Keyboard Trap" + url: https://www.w3.org/WAI/WCAG22/Understanding/no-keyboard-trap + description: Holding the keyboard inside a modal is only acceptable when there is a way out. + - title: "Using ARIA: live regions" + url: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions + description: How assistive technology decides that new text is worth announcing. + contributor: + name: Sinduri Guntupalli + url: https://sinduri.lol/ + about: Experienced in product and program management, web development, configuration management, web analytics, SEO, and + community and ecosystem management. Passionate about open source with active involvement in the Drupal + community. + verification: + command: ./verify.sh + description: Once you think you've solved the challenge, run the verification script. If it fails it will tell you which + checks didn't pass. If it passes, it generates a Certificate of Completion you can paste into the discussion. + architecture_diagram: accessibility-nightmare-intermediate.svg diff --git a/src/data/adventures/accessibility-nightmare/intermediate-posts.json b/src/data/adventures/accessibility-nightmare/intermediate-posts.json new file mode 100644 index 000000000..bacedaacf --- /dev/null +++ b/src/data/adventures/accessibility-nightmare/intermediate-posts.json @@ -0,0 +1,5 @@ +{ + "discussionUrl": "", + "discussionPosts": [], + "totalReplies": 0 +}