Skip to content

Extract reusable USWDS Alert ViewComponent #368

@giverm

Description

@giverm

Summary

USWDS alert markup is duplicated across 12 view files with inconsistent ARIA roles, heading levels, and margin classes. Extract a reusable AlertComponent using ViewComponent (already a project dependency at v4.5.0, used throughout the Strata SDK), then sweep all instances to use it.

Background

The _flash.html.erb partial has a TODO dating back to early development: "Dry this up using a partial or something like View Components." Since then, alert usage has grown across the app. Current issues:

  • Inconsistency: Some info alerts incorrectly use role="alert" (which is assertive and interrupts screen readers). Per USWDS guidelines, only error/emergency alerts should have role="alert".
  • Duplication: Each instance re-implements the same usa-alert / usa-alert__body / usa-alert__text structure with minor variations.
  • Heading levels: Some alerts hardcode <h3>, others use <h4> — the correct level depends on page context.

Reference: Similar Cleanup

PR #344 did the same kind of cleanup for USWDS icons: a uswds_icon helper was created, then all 6 files with inline SVG markup were swept to use it. Each replacement was mechanical (4 lines → 1 helper call) with identical HTML output and no behavior change.

This ticket follows the same approach, but uses a ViewComponent instead of a plain helper because alerts need to render nested content (error lists, buttons, accordions).

The work can be split across two PRs following the same pattern:

  1. Create AlertComponent with specs
  2. Sweep all 12 views to use the component (pure refactor, no behavior change)

Affected Files (12 instances)

~8 are simple alerts (heading + text). 3 need block mode for complex content:

  • app/views/application/_flash.html.erb — success + error flash alerts (error lists, reload button)
  • app/views/application/_case_documents.html.erb
  • app/views/application/_case_tasks.html.erb
  • app/views/dashboard/_request_for_information.html.erb — conditional link buttons
  • app/views/dashboard/_activity_report_denied.html.erb
  • app/views/dashboard/_exemption_denied.html.erb
  • app/views/document_staging/create.html.erb
  • app/views/information_requests/_edit.html.erb
  • app/views/staff/certification_batch_uploads/index.html.erb
  • app/views/staff/certification_batch_uploads/new.html.erb — accordion inside alert
  • app/views/staff/certification_batch_uploads/_status_alert.html.erb
  • app/views/users/passwords/reset.html.erb

Proposed API

# app/components/alert_component.rb
class AlertComponent < ViewComponent::Base
  TYPES = %w[info success warning error].freeze

  renders_one :body  # for complex content (block mode)

  def initialize(type:, heading: nil, message: nil, heading_level: 2, classes: nil)
    # type determines ARIA role: only "error" gets role="alert"
    # heading_level allows callers to set contextually correct heading
    # classes for extra CSS (margins, slim, no-icon)
  end
end

Simple mode:

<%= render AlertComponent.new(type: "success", heading: "Saved", message: "Your changes were saved.") %>

Block mode (for complex content like lists, buttons, accordions):

<%= render AlertComponent.new(type: "error", heading: "Errors found") do |c| %>
  <% c.with_body do %>
    <ul class="usa-list">
      <% errors.each do |error| %>
        <li><%= error %></li>
      <% end %>
    </ul>
  <% end %>
<% end %>

Acceptance Criteria

  • AlertComponent exists with type, heading, message, heading_level, and classes params
  • Supports simple mode (heading + message) and block mode (arbitrary content via renders_one :body)
  • role="alert" only applied for type: "error" — all other types omit it
  • Component specs cover all types, both modes, heading level override, and ARIA role logic
  • All 12 view files refactored to use the component
  • No visual or behavioral changes (verify with existing tests + manual spot check)
  • Existing test suite passes

Technical Notes

  • ViewComponent is already in the Gemfile (v4.5.0) and used by Strata SDK (AccordionComponent, etc.)
  • Follow existing Strata component conventions for file layout (app/components/, app/components/*.html.erb)
  • The Strata SDK itself has the same gap (_no_tasks_alert.html.erb uses raw markup) — this component could eventually be contributed upstream

Out of Scope

  • Contributing this component to the Strata SDK. The SDK has the same gap (_no_tasks_alert.html.erb uses raw alert markup), but the component API should stabilize in OSCER first. Revisit after this work ships.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions