Skip to content

CAMS-706 Fix NVDA screen reader announcement of alert titles#2000

Open
fmaddenflx wants to merge 8 commits intomainfrom
CAMS-706-accessable-alert-title
Open

CAMS-706 Fix NVDA screen reader announcement of alert titles#2000
fmaddenflx wants to merge 8 commits intomainfrom
CAMS-706-accessable-alert-title

Conversation

@fmaddenflx
Copy link
Contributor

@fmaddenflx fmaddenflx commented Feb 27, 2026

Summary

Fixed accessibility issue where NVDA screen reader was not announcing alert titles when alerts appeared dynamically on the screen.

Problem

When an alert with a title popped up on an already-rendered screen, NVDA would not read the alert title. It would only read the title if the page loaded with the alert already visible and NVDA read the entire page.

Solution

Implemented proper ARIA live region support for dynamic alert announcements:

  1. Added aria-labelledby and aria-atomic="true" - Establishes semantic relationship between alert and heading, ensures complete content announcement
  2. Render content only when visible - Content changes (not visibility changes) trigger aria-live announcements
  3. Use z-index instead of visibility:hidden - Keeps aria-live region accessible to assistive technology at all times
  4. Auto-generate IDs - Uses React's useId() hook to ensure unique IDs when not provided
  5. Updated static alerts - Added show={true} to alerts that should display immediately

Technical Details

  • Changed from display: none / visibility: hidden to opacity + z-index for visual hiding
  • Alert content only renders when isVisible === True, creating actual DOM changes for aria-live
  • Removed unused CSS classes (usa-alert__visible/hidden/unset) that were applying visibility:hidden
  • Updated tests to check container visibility instead of removed classes

Test Plan

  • Manually tested with NVDA on Windows - alert title and content now announced when alert appears
  • All existing unit tests pass
  • All integration tests pass

Definition of Done

  • Code changes implemented
  • Tests updated and passing
  • Manual testing with NVDA completed
  • Temporary test page removed

🤖 Generated with Claude Code

Summary by Sourcery

Improve accessibility and visibility behavior of the Alert component for dynamically displayed messages.

New Features:

  • Add ARIA live-region support to alerts via aria-atomic and conditional aria-labelledby tied to the alert title.
  • Auto-generate stable alert and heading IDs when an explicit id is not provided.

Bug Fixes:

  • Ensure dynamically shown alerts, including their titles, are announced correctly by screen readers such as NVDA.
  • Fix static error alerts so they render as visible immediately when errors are present.

Enhancements:

  • Simplify alert visibility styling by replacing visibility-based classes with container-level opacity and z-index transitions, and render alert body content only when visible.

Tests:

  • Update alert tests to assert container visibility instead of removed CSS visibility classes and add coverage for new ARIA attributes on alerts.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 27, 2026

Reviewer's Guide

Implements ARIA live region-friendly behavior for the Alert component so NVDA announces dynamically appearing alert titles, by changing how alerts are rendered, styled, and tested, and ensuring static errors show immediately.

Sequence diagram for dynamic alert announcement with NVDA

sequenceDiagram
  actor User
  participant AppComponent
  participant Alert as Alert_component
  participant DOM
  participant NVDA as ScreenReader_NVDA

  User->>AppComponent: Triggers action that causes error
  AppComponent->>Alert: call show()
  Alert->>Alert: set isVisible = True
  Alert->>DOM: Render alert body with heading
  Note over Alert,DOM: Heading has id headingId
  Alert->>DOM: Set role=status or alert
  Alert->>DOM: Set aria-live=polite/assertive
  Alert->>DOM: Set aria-atomic=true
  Alert->>DOM: Set aria-labelledby=headingId

  DOM-->>NVDA: Live region content changed
  NVDA-->>User: Announces alert title and message

  User->>AppComponent: Dismiss or resolves condition
  AppComponent->>Alert: call hide()
  Alert->>Alert: set isVisible = False
  Alert->>DOM: Remove alert body content (live region persists)
Loading

Class diagram for updated Alert component structure

classDiagram
  direction LR

  class AlertProps {
    +string id
    +string title
    +string message
    +UswdsAlertStyle type
    +string role
    +boolean slim
    +boolean show
    +ReactNode children
  }

  class AlertRefType {
    +show() void
    +hide() void
  }

  class Alert_ {
    +useId() string autoId
    -IsVisible isVisible
    -string alertId
    -string headingId
    +show() void
    +hide() void
    +render() ReactElement
  }

  class IsVisible {
    <<enum>>
    True
    False
    Unset
  }

  AlertProps <.. Alert_ : uses
  AlertRefType <.. Alert_ : implements
  IsVisible <.. Alert_ : state

  Alert_ : aria-live
  Alert_ : aria-atomic = true
  Alert_ : aria-labelledby = headingId when isVisible == True and title
  Alert_ : renders body only when isVisible == True
  Alert_ : container uses class visible for CSS visibility
Loading

File-Level Changes

Change Details Files
Refactored Alert component visibility handling and ARIA attributes to support proper screen reader announcements for dynamic alerts.
  • Replaced internal visibility state classes (usa-alert__visible/hidden/unset) with a simple isVisible gate that only renders alert body content when visible.
  • Introduced useId() to auto-generate a stable alertId and derived headingId, falling back to props.id when provided.
  • Computed resolvedRole (alert/status) and added aria-live based on role while always setting aria-atomic="true" on the alert element.
  • Added aria-labelledby pointing to a headingId only when the alert is visible and a title exists, and applied that id to the

    heading.

  • Updated alert data-testid to continue supporting id-based querying while aligning with new container/alert separation.
user-interface/src/lib/components/uswds/Alert.tsx
Updated Alert styles to keep the alert container in the accessibility tree while visually hiding it using opacity and z-index instead of display/visibility toggling.
  • Changed .usa-alert-container to use z-index:-1 and opacity:0 by default, with a transition on opacity and z-index for smooth show/hide behavior.
  • Updated .usa-alert-container.visible to bring alerts to the foreground via z-index:10 and opacity:1 with coordinated transitions.
  • Removed usa-alert__visible, usa-alert__hidden, and usa-alert__unset classes and their visibility: hidden/visible rules, since visibility is now controlled via the container and conditional rendering.
  • Preserved inline alert positioning while relying on the new visibility model.
user-interface/src/lib/components/uswds/Alert.scss
Adjusted Alert unit and integration tests to align with the new visibility model and ARIA behavior.
  • Switched tests from querying a generic 'alert' test id to using 'alert-container' where appropriate and asserting the presence/absence of the 'visible' class on the container.
  • Removed expectations around the old usa-alert__visible/hidden/unset classes and replaced them with assertions on container visibility state.
  • Ensured tests that read alert message content explicitly call alertRef.current?.show() before asserting text, matching the new conditional rendering.
  • Added tests to verify aria-atomic is always set and aria-labelledby is present only when a title is provided, and that the heading element has the corresponding id.
  • Updated other panel/screen tests (e.g., Court Docket, Data Verification) to assert on container visibility rather than inner alert visibility classes.
user-interface/src/lib/components/uswds/Alert.test.tsx
user-interface/src/case-detail/panels/CaseDetailCourtDocket.test.tsx
user-interface/src/data-verification/DataVerificationScreenAlert.test.tsx
Ensured static error alerts render as visible immediately so their content and titles are announced when present.
  • Updated TrusteeOversightAssignmentModal to pass show={true} to the Alert when an error is present, ensuring the alert content is rendered and announced without an explicit show() call.
  • Updated TrusteeAssignedStaff panel to similarly render error Alerts with show={true} when an error exists, so these alerts are part of the DOM and eligible for aria-live announcement on mount.
user-interface/src/trustees/modals/TrusteeOversightAssignmentModal.tsx
user-interface/src/trustees/panels/TrusteeAssignedStaff.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@fmaddenflx fmaddenflx changed the title Fix NVDA screen reader announcement of alert titles CAMS-706 Fix NVDA screen reader announcement of alert titles Feb 27, 2026
fmaddenflx and others added 7 commits February 26, 2026 18:17
Improve screen reader announcement of alert titles when alerts appear dynamically. NVDA was not reading alert titles when alerts popped up on rendered screens.

Changes:
- Add aria-labelledby to link alert to its heading element
- Add aria-atomic="true" to ensure complete alert content is announced
- Auto-generate unique IDs using useId() hook when id prop not provided
- Add test page at /test/alert-accessibility for manual NVDA testing
- Add tests to verify aria attributes are correctly applied

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Change Alert to only render content when visible so that aria-live regions announce content changes. Screen readers only announce when content is added/changed within a live region, not when visibility changes.

Changes:
- Use visibility:hidden instead of display:none for alert container so aria-live region stays in DOM
- Only render alert body content when isVisible === True
- This triggers aria-live announcement because content actually changes in the live region

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update Alert to render content when either:
- show() method is called (controlled mode with show prop)
- show prop is undefined (uncontrolled mode - always visible when rendered)

This allows alerts to work in both modes:
1. Controlled: <Alert ref={ref} show={false} /> then ref.current.show()
2. Uncontrolled: {error && <Alert>{error}</Alert>}

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Only render alert body content when isVisible is True. This ensures aria-live regions see actual content changes (not just visibility changes) and properly announce to screen readers.

Static alerts that should be immediately visible must now use show={true}.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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.

5 participants