Skip to content

Improve screen reader support for credit card error messages #103846

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: trunk
Choose a base branch
from

Conversation

gcsecsey
Copy link
Contributor

@gcsecsey gcsecsey commented May 30, 2025

Fixes DOTCOM-13375 Make checkout form errors accessible to screen readers

Proposed Changes

This PR ensures that the error notice is announced by VoiceOver when there are form errors.

  • Refactored StripeErrorMessage Component:
    • Converted from a styled span to a styled div for improved semantics and flexibility.
    • Added aria-live="assertive" to ensure screen readers announce error messages. This doesn't work with VoiceOver specifically.
    • Added aria-invalid="true" to indicate the presence of an error.
  • Notice Component Accessibility:
    • Added ariaLive and role props to the Notice component (client/components/notice/index.tsx).
    • These props are now rendered on the root <div> of the notice, allowing for custom ARIA live region and role attributes.
  • Notice Types Update:
    • Updated BaseNoticeOptions, NoticeOptions, and NoticeActionOptions types to include ariaLive and role properties (client/state/notices/types.ts).
  • Accessible Error Notices:
    • Updated the credit card field error handling to dispatch error notices with ariaLive: 'assertive' and role: 'alert', ensuring that screen readers announce the error as soon as it appears.

Why are these changes being made?

Form validation errors on the checkout page are not currently announced by screen readers.

Testing Instructions

  • Checkout this branch or use the live preview
  • Add a new site and choose a plan to go to the checkout page
  • With VoiceOver enabled, submit with missing or invalid required fields (e.g., leave the cardholder name or card number empty).
  • Check that an error notice appears at the top of the page, and VoiceOver announces it
  • Tab to the form errors and check that VoiceOver announces the error messages
CleanShot.2025-06-02.at.15.59.57.mp4

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

Copy link

github-actions bot commented May 30, 2025

@matticbot
Copy link
Contributor

matticbot commented May 30, 2025

This PR modifies the release build for the following Calypso Apps:

For info about this notification, see here: PCYsg-OT6-p2

  • blaze-dashboard
  • odyssey-stats

To test WordPress.com changes, run install-plugin.sh $pluginSlug dotcom-13375-make-checkout-form-errors-accessible-to-screen-readers on your sandbox.

@matticbot
Copy link
Contributor

matticbot commented May 30, 2025

Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

App Entrypoints (~37 bytes added 📈 [gzipped])

name                   parsed_size           gzip_size
entry-subscriptions          +35 B  (+0.0%)       +0 B
entry-reauth-required        +35 B  (+0.0%)       +2 B  (+0.0%)
entry-login                  +35 B  (+0.0%)       +3 B  (+0.0%)

Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used.

Sections (~797 bytes added 📈 [gzipped])

name                             parsed_size           gzip_size
site-purchases                        +180 B  (+0.0%)      +86 B  (+0.0%)
purchases                             +180 B  (+0.0%)      +86 B  (+0.0%)
checkout                              +180 B  (+0.0%)     +103 B  (+0.0%)
a8c-for-agencies-client               +180 B  (+0.0%)     +103 B  (+0.0%)
themes                                 +35 B  (+0.0%)      +17 B  (+0.0%)
theme                                  +35 B  (+0.0%)      +17 B  (+0.0%)
subscribers                            +35 B  (+0.0%)      +19 B  (+0.0%)
stepper-user-step                      +35 B  (+0.0%)      +20 B  (+0.0%)
stats                                  +35 B  (+0.0%)      +17 B  (+0.0%)
staging-site                           +35 B  (+0.0%)      +18 B  (+0.0%)
site-settings                          +35 B  (+0.0%)      +18 B  (+0.0%)
site-blocks                            +35 B  (+0.0%)      +18 B  (+0.0%)
signup                                 +35 B  (+0.0%)      +23 B  (+0.0%)
settings-writing                       +35 B  (+0.0%)      +19 B  (+0.0%)
settings-security                      +35 B  (+0.0%)      +19 B  (+0.0%)
settings-reading                       +35 B  (+0.0%)      +24 B  (+0.0%)
settings-podcast                       +35 B  (+0.0%)      +24 B  (+0.0%)
settings-performance                   +35 B  (+0.0%)      +19 B  (+0.0%)
settings-jetpack                       +35 B  (+0.0%)      +19 B  (+0.0%)
settings-discussion                    +35 B  (+0.0%)      +19 B  (+0.0%)
settings                               +35 B  (+0.0%)      +19 B  (+0.0%)
security                               +35 B  (+0.0%)      +18 B  (+0.0%)
scan                                   +35 B  (+0.0%)      +26 B  (+0.0%)
reauth-required                        +35 B  (+0.1%)      +18 B  (+0.1%)
purchase-product                       +35 B  (+0.0%)      +14 B  (+0.0%)
promote-post-i2                        +35 B  (+0.0%)      +17 B  (+0.0%)
privacy                                +35 B  (+0.0%)      +18 B  (+0.0%)
posts-custom                           +35 B  (+0.0%)      +17 B  (+0.0%)
posts                                  +35 B  (+0.0%)      +17 B  (+0.0%)
plugins                                +35 B  (+0.0%)      +19 B  (+0.0%)
plans                                  +35 B  (+0.0%)      +19 B  (+0.0%)
people                                 +35 B  (+0.0%)      +19 B  (+0.0%)
pages                                  +35 B  (+0.0%)      +17 B  (+0.0%)
overview                               +35 B  (+0.0%)      +20 B  (+0.0%)
notification-settings                  +35 B  (+0.0%)      +18 B  (+0.0%)
migrate                                +35 B  (+0.0%)      +21 B  (+0.0%)
media                                  +35 B  (+0.0%)      +21 B  (+0.0%)
me                                     +35 B  (+0.0%)      +18 B  (+0.0%)
marketplace                            +35 B  (+0.0%)      +25 B  (+0.0%)
marketing                              +35 B  (+0.0%)      +19 B  (+0.0%)
jetpack-social                         +35 B  (+0.0%)      +26 B  (+0.0%)
jetpack-search                         +35 B  (+0.0%)      +26 B  (+0.0%)
jetpack-connect                        +35 B  (+0.0%)      +19 B  (+0.0%)
jetpack-cloud-plugin-management        +35 B  (+0.0%)      +22 B  (+0.0%)
jetpack-cloud-partner-portal           +35 B  (+0.0%)      +22 B  (+0.0%)
jetpack-cloud-agency-dashboard         +35 B  (+0.0%)      +22 B  (+0.0%)
jetpack-app                            +35 B  (+0.0%)      +19 B  (+0.0%)
import                                 +35 B  (+0.0%)      +16 B  (+0.0%)
hosting                                +35 B  (+0.0%)      +18 B  (+0.0%)
home                                   +35 B  (+0.0%)      +22 B  (+0.0%)
help                                   +35 B  (+0.0%)      +18 B  (+0.0%)
google-my-business                     +35 B  (+0.0%)      +20 B  (+0.0%)
github-deployments                     +35 B  (+0.0%)      +20 B  (+0.0%)
export                                 +35 B  (+0.0%)      +19 B  (+0.0%)
email                                  +35 B  (+0.0%)      +16 B  (+0.0%)
earn                                   +35 B  (+0.0%)      +19 B  (+0.0%)
domains                                +35 B  (+0.0%)      +20 B  (+0.0%)
domain-connect-authorize               +35 B  (+0.2%)      +20 B  (+0.4%)
developer                              +35 B  (+0.0%)      +18 B  (+0.0%)
concierge                              +35 B  (+0.0%)      +18 B  (+0.0%)
comments                               +35 B  (+0.0%)      +24 B  (+0.0%)
backup                                 +35 B  (+0.0%)      +26 B  (+0.0%)
async-step-use-my-domain               +35 B  (+0.0%)      +20 B  (+0.0%)
async-step-unified-plans               +35 B  (+0.0%)      +19 B  (+0.0%)
async-step-unified-domains             +35 B  (+0.0%)      +20 B  (+0.0%)
activity                               +35 B  (+0.0%)      +23 B  (+0.0%)
account-close                          +35 B  (+0.0%)      +18 B  (+0.0%)
account                                +35 B  (+0.0%)      +18 B  (+0.0%)
accept-invite                          +35 B  (+0.0%)      +19 B  (+0.0%)
a8c-for-agencies-sites                 +35 B  (+0.0%)      +26 B  (+0.0%)
a8c-for-agencies-plugins               +35 B  (+0.0%)      +21 B  (+0.0%)

Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to.

Async-loaded Components (~222 bytes added 📈 [gzipped])

name                                                      parsed_size           gzip_size
async-load-store-app-store-stats                                +35 B  (+0.0%)      +20 B  (+0.0%)
async-load-signup-steps-user                                    +35 B  (+0.0%)      +19 B  (+0.0%)
async-load-signup-steps-plans-theme-preselected                 +35 B  (+0.0%)      +19 B  (+0.0%)
async-load-signup-steps-plans                                   +35 B  (+0.0%)      +19 B  (+0.0%)
async-load-signup-steps-domains                                 +35 B  (+0.0%)      +20 B  (+0.0%)
async-load-signup-steps-courses                                 +35 B  (+0.1%)      +21 B  (+0.1%)
async-load-design-playground                                    +35 B  (+0.0%)      +20 B  (+0.0%)
async-load-design-blocks                                        +35 B  (+0.0%)      +19 B  (+0.0%)
async-load-design                                               +35 B  (+0.0%)      +20 B  (+0.0%)
async-load-calypso-reader-discover-discover-stream              +35 B  (+0.3%)      +23 B  (+0.5%)
async-load-calypso-my-sites-current-site-notice                 +35 B  (+0.1%)      +20 B  (+0.1%)
async-load-calypso-my-sites-current-site-domain-warnings        +35 B  (+0.1%)      +22 B  (+0.3%)
async-load-calypso-lib-account-settings-helper                  +35 B  (+0.0%)      +18 B  (+0.0%)
async-load-calypso-components-global-notices                    +35 B  (+0.5%)      +20 B  (+0.8%)

React components that are loaded lazily, when a certain part of UI is displayed for the first time.

Legend

What is parsed and gzip size?

Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
Gzip Size: Compressed size of the JS and CSS files. This much data needs to be downloaded over network.

Generated by performance advisor bot at iscalypsofastyet.com.

@gcsecsey gcsecsey marked this pull request as ready for review June 2, 2025 15:49
@gcsecsey gcsecsey requested a review from a team as a code owner June 2, 2025 15:49
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Jun 2, 2025
@gcsecsey gcsecsey requested a review from a team June 2, 2025 15:49
Copy link
Member

@sirbrillig sirbrillig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is a WIP (maybe? it still says that in the title) but from a Payments perspective, this looks like a nice improvement!

@gcsecsey gcsecsey changed the title WIP add aria attributes to credit card error messages Add aria attributes to credit card error messages Jun 2, 2025
@gcsecsey
Copy link
Contributor Author

gcsecsey commented Jun 2, 2025

I know this is a WIP (maybe? it still says that in the title) but from a Payments perspective, this looks like a nice improvement!

Thanks for reviewing it @sirbrillig! Sorry, I just forgot to update the title when I made this ready for review, updated it now. 👍

@gcsecsey gcsecsey changed the title Add aria attributes to credit card error messages Improve screen reader support for credit card error messages Jun 2, 2025
Copy link
Contributor

@bcotrim bcotrim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When testing this I noticed the placeholder text gets picked up by the VoiceOver.

image

Changes LGTM and comments aren't blockers
👍

@@ -76,6 +78,8 @@ export default function Notice( {
status,
text,
theme = 'dark',
ariaLive,
role,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
role,
role = 'status',

Nit: In my opinion this is easier to read and follows the same pattern as the other props with a default value in this component (like theme)

@@ -45,6 +45,8 @@ interface NoticeProps {
theme?: 'light' | 'dark';
text?: ReactNode;
children?: ReactNode;
ariaLive?: 'off' | 'polite' | 'assertive';
role?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we maybe limit these a bit for role that make sense?
In this component I would imagine these would be a good start: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles#4._live_region_roles

What do you think?

@gavande1
Copy link
Contributor

gavande1 commented Jun 3, 2025

@gcsecsey, I noticed that VoiceOver didn't read the Cardholder name field when an error occurred. Is that expected? I'm not sure if that should be part of this PR, but when an error occurs, the focus is not set to the error field, as a result, VoiceOver don't read the error message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants