Skip to content

feat(a11y): live-region announcements for errors and status - #1650

Open
AbhishekChorotiya wants to merge 1 commit into
feat/a11y-keyboard-focusfrom
feat/a11y-live-regions
Open

feat(a11y): live-region announcements for errors and status#1650
AbhishekChorotiya wants to merge 1 commit into
feat/a11y-keyboard-focusfrom
feat/a11y-live-regions

Conversation

@AbhishekChorotiya

@AbhishekChorotiya AbhishekChorotiya commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates
  • Documentation
  • CI/CD

Description

This PR improves live-region behavior for payment status and error states. It keeps announcement regions stable in the page, uses concise assertive messaging for failed submit states, and reuses the shared visually-hidden utility instead of duplicating screen-reader-only styling.

The user impact is more predictable screen-reader feedback during submit and validation flows, while avoiding extra noise during ordinary navigation or payment-method switching.

This PR is stacked after the keyboard and focus PR because it depends on the shared accessibility helper module already present in the stack.

Closes #1649

How did you test it?

Validated as part of the completed accessibility stack. The checks cover the combined flow after all stacked PRs are applied, and live-region behavior was checked in the local payment-element accessibility smoke flow.

  • Ran npm run re:build on the completed accessibility stack.
  • Ran npm run test:hooks on the completed accessibility stack.
  • Ran npm run build on the completed accessibility stack.
  • Ran committed-range whitespace validation on the completed accessibility stack.

Checklist

  • I ran npm run re:build
  • I reviewed submitted code
  • I added unit tests for my changes where possible

@semanticdiff-com

Copy link
Copy Markdown

Review changes with  SemanticDiff

@XyneSpaces

Copy link
Copy Markdown

💡 Announcer.res error handling swallows exceptions silently

The message handler in Announcer.res has a try-catch that silently swallows all exceptions:

try {
  let dict = ev.data->Utils.safeParse->Utils.getDictFromJson
  // ... processing
} catch {
| _ => ()
}

While this prevents crashes from malformed messages, it also hides legitimate errors during development. Consider logging parsing failures at least in development builds:

catch {
| exn =>
  if GlobalVars.env === "development" {
    Console.log("Announcer: failed to process message", exn)
  }
}

Global announcer (polite role=status + assertive role=alert, auto-clear); aria-busy on
pay button; announce processing/failure (localized: processingPaymentText, paymentFailedText);
announced validation errors via shared LiveError; accessible name for the VGS field.
@XyneSpaces

Copy link
Copy Markdown

Review Summary

PR: #1650 - feat(a11y): live-region announcements for errors and status

Findings

  1. Good: New Announcer.res component implements ARIA live regions for dynamic status announcements.

  2. Good: Auto-clear after 5s prevents stale announcements from being re-read on focus changes.

  3. Suggestion: The live region uses aria-live="polite" which is appropriate for non-critical updates. For critical errors (payment failures, security issues), consider using aria-live="assertive" to interrupt screen readers immediately.

  4. Observation: The atom-based state management (AccessibilityAnnouncer.announcementAtom) allows any component to trigger announcements without direct prop drilling.

No blocking issues. The live region implementation follows WCAG guidelines for status announcements.

@XyneSpaces

Copy link
Copy Markdown

⚠️ Live region updates may cause excessive React re-renders

The new useAnnounce hook and Announcer component use state for announcement text, which triggers React re-renders on every announcement. Consider:

  1. Using a ref-based approach to update DOM directly without React commit
  2. A single global announcer outside the component tree
  3. Debouncing rapid successive announcements

This is particularly important for error states where multiple validations may fire in sequence.

@XyneSpaces

Copy link
Copy Markdown

🚨 Bare catch block swallows all errors in announcer handler

The Announcer.res component has the same pattern as other a11y PRs — catch { | _ => () } silently discards all exceptions in the message handler. If safeParse or getDictFromJson throws, the failure is invisible.

Consider logging to the configured logger or at minimum document why errors must be suppressed.

} catch {
| _ => ()
}

{React.string(errorStr->Belt.Option.getWithDefault(""))}
</div>
<LiveError
text={errorStr->Belt.Option.getWithDefault("")}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
text={errorStr->Belt.Option.getWithDefault("")}
text={errorStr->Option.getOr("")}

if !(submitSuccessfulVal->JSON.Decode.bool->Option.getOr(false)) {
setIsPayNowButtonDisable(_ => false)
setShowLoader(_ => false)
announce(~assertive=true, localeString.paymentFailedText)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PayNowButton.res — calls announce(~assertive=true, localeString.paymentFailedText) (generic message)

Then Announcer.res— message listener extracts the specific error message from the response payload
Both listen for the same window "message" event with submitSuccessful === false. The Announcer registers its listener at mount time (in useEffect), while PayNowButton registers via addSmartEventListener at click time. Listeners fire in registration order, so Announcer fires first (sets specific error), then PayNowButton fires (overwrites with generic message)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

image "submitSuccessful" is used many places, check once if it's breaking for them or not.

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.

3 participants