Skip to content

Improve consent banner/modal accessibility (WCAG 2.1 AA) - #9

Merged
davidperezgar merged 1 commit into
mainfrom
feature/banner-accessibility
Sep 29, 2026
Merged

davidperezgar merged 1 commit into
mainfrom
feature/banner-accessibility

Conversation

@Castellon-ACM

Copy link
Copy Markdown
Contributor

Summary

Closes #2. Audited and improved the existing cookie consent banner/popup markup and JS against the issue's "Areas to cover" checklist. Much of the ground work (native <button> elements, role="dialog"/aria-modal on the popup layout vs. role="region" on the bar/box layouts, a Tab focus trap, focus restore to the triggering element, :focus-visible styles, algorithmic WCAG-AA contrast enforcement for customizable colors, and a prefers-reduced-motion rule) was already in place before this PR. This PR fills the remaining gaps:

  • Screen reader announcements (previously missing): added an always-present, visually hidden role="status" aria-live="polite" live region (CookieNotice::render_status_announcer()) that JS fills in to announce the banner appearing and later consent-decision changes ("Cookies accepted."/"Cookies rejected."). Strings are localized via wp_localize_script (frcnCookieNoticeA11y).
  • Accessible description: the popup/banner wrapper now has aria-describedby pointing at the message paragraph's own id, so assistive technology has more than just a generic "region"/"dialog" announcement.
  • Escape on the true modal (popup layout): Escape is now treated as an explicit reject decision — restores focus to the triggering element and unbinds the modal keydown handler — rather than doing nothing, while staying GDPR-safe (rejecting is a valid, equivalent decision, not a silent dismissal without any decision recorded).
  • Broader focus trap coverage: the existing Tab-trap now also considers input/[tabindex] elements, not just links/buttons.
  • Reduced motion: extended the existing prefers-reduced-motion: reduce rule to also cover the accept/reject buttons' and reopen trigger's hover/active/focus transitions, not just the banner's own reveal transition.

Acceptance criteria (from #2)

  • Banner and preferences modal are fully operable by keyboard only — accept/reject/reopen are native <button>s; popup Tab-traps and Escape provides an explicit way out.
  • Focus is managed correctly on open/close of the modal — popup moves focus to the accept button on open (non-modal bar/box layouts deliberately do not steal focus, per WAI-ARIA best practice for non-blocking live-region-announced banners), and restores focus to the triggering element on both button-click and Escape close.
  • All interactive controls have accessible names and correct roles/states — native buttons/links, aria-label/aria-describedby on the banner, aria-hidden on the decorative icon. (No custom toggle/checkbox exists in this free plugin's scope yet — that's a PRO/add-on "Customize" feature — so no aria-checked work was needed here.)
  • Default color scheme passes WCAG AA contrast; contrast for customizable colors is validated — this was already handled by CookieNotice::get_readable_text_color()/get_readable_on_white_color(), which algorithmically derive a WCAG-AA-safe text/link color for any configured accent/background pair (verified by the pre-existing CookieNoticeContrastTest).
  • Automated accessibility scan (axe-core) reports no critical/serious violations — not run. This sandboxed environment has no headless browser available; a real axe-core pass against a live page is recommended before release.
  • Manually verified with at least one screen reader (NVDA or VoiceOver) — not done here; recommended as a manual QA step before shipping, especially to confirm the new live-region announcements and Escape behavior read naturally in practice.

Test plan

  • Added tests/Unit/CookieNoticeAccessibilityTest.php (PHPUnit): asserts role="region" vs role="dialog"/aria-modal="true" per layout, aria-label/aria-describedby, native <button> markup, the decorative icon's aria-hidden, and that the new status-announcer live region is always present (including on the configured policy page).
  • Added tests/js/cookie-notice-focus-management.test.js (Node's built-in test runner, matching the existing cookie-notice-injection.test.js style/DOM-stub approach): covers opening the popup moving focus to the accept button and announcing the banner, Tab/Shift+Tab wrapping at the first/last focusable element (and not wrapping in the middle), Escape rejecting + announcing + restoring focus + unbinding the handler, and accepting via the button also restoring focus and unbinding the handler.
  • composer lint and composer phpstan: both pass clean on includes/ (the only files those gates cover).
  • node --test tests/js/*.test.js: all 26 tests pass (20 pre-existing + 6 new).
  • composer test (PHPUnit): could not be run in this environment — there's no MySQL/MariaDB available to stand up the WordPress test suite (bin/install-wp-tests.sh requires it), and this is a pre-existing limitation of the sandbox, not something introduced by this PR — the same failure reproduces on the pre-existing CookieNoticeContrastTest with no changes applied. The new test file was verified with php -l and follows the exact patterns (Yoast\WPTestUtils\WPIntegration\TestCase, render_banner() + ob_start()) of the existing, presumably-passing-in-CI test suite. Recommend confirming it passes in CI/a real dev environment before merging.

…ocus

- Add an always-present, visually hidden aria-live="polite" status
  region (render_status_announcer()) so screen reader users hear the
  banner appearing and later consent-decision changes (accepted/
  rejected), which a purely visual class toggle never conveyed.
- Associate the banner's message text via aria-describedby/id so its
  accessible description isn't just a generic "region"/"dialog" role.
- In the popup (true modal) layout, treat Escape as an explicit reject
  decision (restoring focus to the triggering element and unbinding the
  keydown handler), instead of only trapping Tab — consistent with
  Escape needing to close a modal without leaving it as a dead end that
  never records a decision.
- Extend the existing focus trap to also cover input/[tabindex]
  elements, not just links/buttons.
- Cover the reduced-motion media query for button/reopen hover and
  focus transitions, not just the banner's own reveal transition.

Refs #2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@davidperezgar
davidperezgar merged commit dc5b1a5 into main Sep 29, 2026
6 checks passed
@davidperezgar
davidperezgar deleted the feature/banner-accessibility branch September 29, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make the consent banner/widget accessible (WCAG 2.1 AA)

2 participants