Make Accordion keyboard operable (WCAG 2.2 SC 2.1.1) - #118
Merged
Merged
Conversation
The accordion header was rendered as a <div role="button"> that was never focusable and never listened for key events: no tabindex in the template, and the Alpine summary bind registered only @click. Since role="button" on a div does not synthesise Enter/Space activation the way a native <button> does, both halves were missing — the header could not be reached by Tab, and would not have activated even if it were. With the panel hidden via x-show/x-collapse there was no alternative route to the content, so keyboard-only users could not read anything inside an accordion. Reported on the forum as a WCAG 2.2 Success Criterion 2.1.1 (Keyboard, Level A) failure. - Add static tabindex="0" to the summary, gated on preview mode so it does not interfere with the editor's focus handling. Static rather than Alpine-bound so the header is focusable before Alpine hydrates. - Add @keydown.enter.prevent and @keydown.space.prevent to the summary bind; .prevent on Space stops the page scrolling. - Drop the redundant :aria-hidden on the panel. x-show already applies display:none, and keeping both marked the panel hidden while it was still visible mid-transition. - Remove role="group", aria-roledescription and the aria-label that interpolated the internal node id, which screen readers announced verbatim. - Switch the summary focus ring to focus-visible so it only shows for keyboard users, and use ring-brand-500 so it follows the theme instead of a hardcoded blue. Verified in a browser harness: Tab reaches the header, Enter and Space each toggle exactly once, Space calls preventDefault, aria-expanded tracks state, grouped accordions still close siblings, openOnLoad still renders open, and Tab now moves into the open panel's content. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the accordion keyboard accessibility failure reported on the forum: https://forums.realmacsoftware.com/t/bug-report-accordion-component-is-not-keyboard-operable-wcag-2-2-level-a-failure/57319
The bug
The accordion header was rendered as a
<div role="button">that was never focusable and never listened for key events — notabindexin the template, and the Alpinesummarybind registered only@click.Both halves were missing.
role="button"on adivdoes not synthesise Enter/Space activation the way a native<button>does, so the header could not be reached by Tab and would not have activated even if focus were forced onto it. With the panel hidden byx-show/x-collapsethere was no alternative route to the content, so keyboard-only users could not read anything inside an accordion.That's a WCAG 2.2 Success Criterion 2.1.1 (Keyboard, Level A) failure, affecting every Elements site using the component.
Corroborating that it was an oversight rather than a decision: the summary already shipped
focus:ring-2styling for an element that could never receive focus.What changed
Keyboard operability
tabindex="0"on the summary, gated@if(!edit)so it doesn't interfere with the editor's focus handling. Static rather than Alpine-bound, so the header is focusable before Alpine hydrates (the shared Alpine scripts are deferred).@keydown.enter.preventand@keydown.space.preventon the summary bind..preventon Space stops the page scrolling.ARIA cleanup
:aria-hiddenon the panel —x-showalready appliesdisplay:none, and keeping both marked the panel hidden while it was still visible mid-transition.role="group",aria-roledescriptionand thearia-labelthat interpolated the internal node id (screen readers were announcing the raw id).Focus ring
focus:→focus-visible:so it only shows for keyboard users, andring-blue-500→ring-brand-500so it follows the theme.Design note
Kept
div[role="button"]rather than moving to a native<button>. The header title is a@dropzoneholding arbitrary author content, so wrapping it in a<button>risks invalid nested-interactive markup and would fight click-to-edit in the editor.div[role="button"]+tabindex+ explicit key handlers is the ARIA-sanctioned equivalent and the minimal change that clears the Level A failure.Verification
Driven in a browser harness reproducing the rendered markup with the pack's own
alpine.js/alpine-collapse.js::focus-visiblematchesdefaultPrevented === true, confirming the scroll guardaria-expandedtracks state;aria-hiddenis gone from the panelopenOnLoad: truestill renders openFor the reviewer
hooks.jsis regenerated vianpm run build:hooks; onlyhooks.source.jswas hand-edited.switchToBoolfrom the current shared hooks, so the accordion'shooks.jscarries that change too. The repo's other 27 generatedhooks.jsfiles are stale against the current RWElementsPacksTools (commit8d1a2a5), and were reverted here to keep this diff focused — worth a deliberate sync separately.x-collapse's height animation lands at 0 there. It reproduces on the untouched mouse@clickpath, all markup variants fail identically, and plainx-showwithoutx-collapseworks — so it's the plugin's transition timing in that environment, not this change. Still worth a quick look in the real app.Not addressed here
Flagged rather than fixed, to keep the diff reviewable: the APG heading-wrapper structure (tangled up with how dropzones work),
role="region"creating one landmark per panel, focus management when a grouped sibling auto-closes, and the malformedbg--50classes (which affect 5 components, not just this one).🤖 Generated with Claude Code