Skip to content
Merged
12 changes: 12 additions & 0 deletions reporting-app/app/components/alert_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<%= tag.div class: alert_classes, role: (error? ? "alert" : nil), style: style do %>
Copy link
Copy Markdown
Contributor

@giverm giverm Mar 23, 2026

Choose a reason for hiding this comment

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

Some of these have "role=status", which I think we'll want to preserve for accessibility.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yea that makes sense. Right now I see most of them either have role as status or alert except for one. I am wondering if all these alerts should be either one or the other. And as stated in the ticket only type error should alert? so would just be a change to make the role one or the other. instead of preserving it on the view side of things

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

TLDR: do we need more than 2 roles / expecting nil roles or can we just update the code under the comment to just be alert or status?

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.

Are we able to pass role as an argument? That feels like it'd the most straight forward. Looking at the USWDS docs, I think we'd only ever want 'alertor 'status'. I think thenil` should also probably be given a role as well.
https://designsystem.digital.gov/components/alert/#accessibility-guidance

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yea we can pass role as an argument for sure. Just wasnt sure if it was worth adding that if we are always going to have it as 1 or the other and from the ticket comments we know when we want to alert and when we want a status.

This is the easy way without adding the argument

<%= tag.div class: alert_classes, role: (error? ? "alert" :"status"), style: style do %>

Otherwise we would be adding the role argument to each of those spots, but from the guidance there is a possible of the role being region so that is why we would add the argument. Which I think I just talked myself into incase we add any of those in the future we wouldnt need to refactor anything.

<div class="usa-alert__body">
<% if heading.present? %>
<%= content_tag(:"h#{heading_level}", heading, class: "usa-alert__heading") %>
<% end %>
<% if body %>
<%= body %>
<% elsif message.present? %>
<p class="usa-alert__text"><%= message %></p>
<% end %>
</div>
<% end %>
33 changes: 33 additions & 0 deletions reporting-app/app/components/alert_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

# USWDS alert (usa-alert). Simple mode: message and optional heading, or use the body slot
# for lists, buttons, accordions. Only type "error" sets role="alert". Optional style is
# forwarded to the root element (e.g. slim / no-icon layouts).
class AlertComponent < ViewComponent::Base
TYPES = %w[info success warning error].freeze
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.

Up to you, but I have a preference for defining the individual strings as constants for both types and roles. It's not as critical for the types, since those are validated, but I still think using the constants reads better. You could do something like

Suggested change
TYPES = %w[info success warning error].freeze
module TYPES
INFO = "info"
SUCCESS = "success"
WARNING = "warning"
ERROR = "error"
ALL = [INFO, SUCCESS, WARNING, ERROR].freeze
end
module ROLES
ALERT = "alert"
STATUS = "status"
ALL = [ALERT, STATUS].freese


renders_one :body

def initialize(type:, heading: nil, message: nil, heading_level: 2, classes: nil, style: nil)
@type = type.to_s
raise ArgumentError, "Invalid alert type: #{type.inspect}" unless TYPES.include?(@type)

@heading = heading
@message = message
@heading_level = Integer(heading_level)
raise ArgumentError, "heading_level must be between 1 and 6" unless (1..6).cover?(@heading_level)

@classes = classes
@style = style
end

attr_reader :type, :heading, :message, :heading_level, :classes, :style

def alert_classes
[ "usa-alert", "usa-alert--#{type}", classes ].compact.join(" ")
end

def error?
type == "error"
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.

Then we could do:

Suggested change
type == "error"
type == TYPES::ERROR

end
end
11 changes: 6 additions & 5 deletions reporting-app/app/views/application/_case_documents.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
<% end %>
</ul>
<% else %>
<div style="padding: unset;" class="usa-alert usa-alert--info usa-alert--slim usa-alert--no-icon" role="status">
<div class="usa-alert__body">
<p class="usa-alert__text">No documents available</p>
</div>
</div>
<%= render AlertComponent.new(
type: "info",
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
type: "info",
type: AlertComponent::TYPES::INFO

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yea that definitely reads better and I would rather in sync with the standardization of the code so I will make that change thank you for information

message: "No documents available",
classes: "usa-alert--slim usa-alert--no-icon",
style: "padding: unset;"
) %>
<% end %>
<% end %>
11 changes: 6 additions & 5 deletions reporting-app/app/views/application/_case_tasks.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
<% end %>
</ul>
<% else %>
<div style="padding: unset;" class="usa-alert usa-alert--info usa-alert--slim usa-alert--no-icon" role="status">
<div class="usa-alert__body">
<p class="usa-alert__text">No tasks available</p>
</div>
</div>
<%= render AlertComponent.new(
type: "info",
message: "No tasks available",
classes: "usa-alert--slim usa-alert--no-icon",
style: "padding: unset;"
) %>
<% end %>
<% end %>
17 changes: 6 additions & 11 deletions reporting-app/app/views/application/_flash.html.erb
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
<% # @TODO: Dry this up using a partial or something like View Components %>
<% # https://api.rubyonrails.org/classes/ActionDispatch/Flash.html %>
<% if flash[:notice] || flash[:errors] || alert || notice %>
<div class="grid-row margin-bottom-3">
<div class="grid-col-12">
<% if flash[:notice] || notice %>
<div class="usa-alert usa-alert--success">
<div class="usa-alert__body">
<p class="usa-alert__text"><%= flash[:notice] || notice %></p>
</div>
</div>
<%= render AlertComponent.new(type: "success", message: flash[:notice] || notice) %>
<% end %>

<% if flash[:errors] || alert %>
<div class="usa-alert usa-alert--error" role="alert">
<div class="usa-alert__body">
<%= render AlertComponent.new(type: "error") do |c| %>
<% c.with_body do %>
<% if flash[:errors] %>
<h3 class="usa-alert__heading">
<%= t "flash.error_heading", count: flash[:errors].count %>
Expand All @@ -37,9 +32,9 @@
</ul>
<% end %>
</div>
</div>
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
<div class="grid-container">
<div class="usa-alert usa-alert--error" role="alert">
<div class="usa-alert__body">
<h3 class="usa-alert__heading"><%= t(".heading") %></h3>
<p class="usa-alert__text">
<%= t(".intro") %>
</p>
</div>
</div>
<%= render AlertComponent.new(type: "error", heading: t(".heading"), message: t(".intro"), heading_level: 3) %>
<div class="margin-top-3">
<%= link_to t(".view_activity_report_button"), activity_report_application_form_path(activity_report), class: "usa-button" %>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
<div class="grid-container">
<div class="usa-alert usa-alert--error" role="alert">
<div class="usa-alert__body">
<h3 class="usa-alert__heading"><%= t(".heading") %></h3>
<p class="usa-alert__text">
<%= t(".intro") %>
</p>
</div>
</div>
<%= render AlertComponent.new(type: "error", heading: t(".heading"), message: t(".intro"), heading_level: 3) %>
<div class="margin-top-3">
<%= link_to t(".view_exemption_button"), exemption_application_form_path(exemption_application), class: "usa-button" %>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<div class="grid-container">
<div class="usa-alert usa-alert--warning margin-bottom-3" role="status">
<div class="usa-alert__body">
<h3 class="usa-alert__heading"><%= t('.heading') %></h3>
<%= render AlertComponent.new(type: "warning", heading: t('.heading'), heading_level: 3, classes: "margin-bottom-3") do |c| %>
<% c.with_body do %>
<p class="usa-alert__text">
<%= t('.intro_html', due_date: information_request.due_date&.strftime('%B %d, %Y') || '<date due>') %>
</p>
Expand All @@ -12,6 +11,6 @@
<%= link_to t('.cta_button'), edit_exemption_information_request_path(information_request), class: 'usa-button display-inline-block' %>
<% end %>
</div>
</div>
</div>
<% end %>
<% end %>
</div>
6 changes: 1 addition & 5 deletions reporting-app/app/views/document_staging/create.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
<h1><%= t(".heading") %></h1>

<% if @error %>
<div class="usa-alert usa-alert--error margin-bottom-3" role="alert">
<div class="usa-alert__body">
<p class="usa-alert__text"><%= @error %></p>
</div>
</div>
<%= render AlertComponent.new(type: "error", message: @error, classes: "margin-bottom-3") %>
<% end %>

<p>
Expand Down
8 changes: 4 additions & 4 deletions reporting-app/app/views/information_requests/_edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
<h2><%= t('information_requests.edit.instructions_heading') %></h2>
<p class="font-serif-md"><%= t('information_requests.edit.instructions_html') %></p>

<div class="usa-alert usa-alert--info margin-top-3" role="status">
<div class="usa-alert__body">
<%= render AlertComponent.new(type: "info", classes: "margin-top-3") do |c| %>
<% c.with_body do %>
<p class="usa-alert__text">
<%= @information_request.staff_comment %>
<p class="margin-top-2"></p>
<%= t('information_requests.edit.due_by', due_date: @information_request.due_date.strftime('%B %d, %Y')) %>
</p>
</div>
</div>
<% end %>
<% end %>

<%= strata_form_with(model: @information_request) do |f| %>
<%= f.file_field :supporting_documents, label: t('information_requests.edit.upload_documents'), multiple: true %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
<%# Shared USWDS alert banner for batch upload status messages %>
<%# Locals: type (info/error/success/warning), heading (optional), message %>
<div class="usa-alert usa-alert--<%= type %>" <%= type == "error" ? 'role="alert"' : 'role="status"' %>>
<div class="usa-alert__body">
<% if local_assigns[:heading] %>
<h2 class="usa-alert__heading"><%= heading %></h2>
<% end %>
<p class="usa-alert__text"><%= message %></p>
</div>
</div>
<%= render AlertComponent.new(
type: type,
heading: local_assigns[:heading],
message: message,
heading_level: 2
) %>
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@
</tbody>
</table>
<% else %>
<div class="usa-alert usa-alert--info margin-top-4" role="alert">
<div class="usa-alert__body">
<p class="usa-alert__text"><%= t(".no_uploads") %></p>
</div>
</div>
<%= render AlertComponent.new(type: "info", message: t(".no_uploads"), classes: "margin-top-4") %>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<p>
<%= link_to t(".download_template"), "/certification_batch_upload_template.csv", class: "usa-link", download: true %>
</p>
<div class="usa-alert usa-alert--info" role="alert">
<div class="usa-alert__body">
<%= render AlertComponent.new(type: "info") do |c| %>
<% c.with_body do %>
<h2 class="usa-alert__heading"><%= t(".csv_format.heading") %></h2>
<p class="usa-alert__text"><%= t(".csv_format.description") %></p>
<div class="usa-accordion usa-accordion--bordered maxw-tablet">
Expand Down Expand Up @@ -43,8 +43,8 @@
</ul>
</div>
</div>
</div>
</div>
<% end %>
<% end %>
<%= form_with(url: certification_batch_uploads_path, multipart: true, class: "margin-top-4") do |f| %>
<div class="usa-form-group">
<%= f.label :csv_file, t(".csv_file_label"), class: "usa-label" %>
Expand Down
16 changes: 7 additions & 9 deletions reporting-app/app/views/users/passwords/reset.html.erb
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
<% content_for :title, t(".title") %>

<div class="usa-alert usa-alert--info">
<div class="usa-alert__body">
<h2 class="usa-alert__heading"><%= t(".instructions_heading") %></h2>
<p class="usa-alert__text">
<%= t(".instructions_html", verify_account_url: url_for(users_verify_account_path)) %>
</p>
</div>
</div>
<%= render AlertComponent.new(
type: "info",
heading: t(".instructions_heading"),
message: t(".instructions_html", verify_account_url: url_for(users_verify_account_path)),
heading_level: 2
) %>

<h1><%= t(".title") %></h1>

Expand All @@ -29,4 +27,4 @@
</button>

<%= f.submit t(".submit") %>
<% end %>
<% end %>
88 changes: 88 additions & 0 deletions reporting-app/spec/components/alert_component_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe AlertComponent, type: :component do
describe "types" do
it "renders each type with matching modifier class" do
AlertComponent::TYPES.each do |alert_type|
render_inline(described_class.new(type: alert_type, message: "x"))
expect(page).to have_css(".usa-alert.usa-alert--#{alert_type}")
end
end

it "raises for invalid type" do
expect do
described_class.new(type: "bogus", message: "x")
end.to raise_error(ArgumentError, /Invalid alert type/)
end
end

describe "ARIA role" do
it "sets role=alert only for error" do
render_inline(described_class.new(type: "error", message: "e"))
expect(page).to have_css('.usa-alert[role="alert"]')
end

it "omits role for non-error types" do
%w[info success warning].each do |alert_type|
render_inline(described_class.new(type: alert_type, message: "m"))
expect(page).to have_css(".usa-alert.usa-alert--#{alert_type}")
expect(page).to have_no_css('.usa-alert[role]')
end
end
end

describe "simple mode" do
it "renders message in usa-alert__text" do
render_inline(described_class.new(type: "success", message: "Saved"))
expect(page).to have_css("p.usa-alert__text", text: "Saved")
end

it "renders optional heading at the given level" do
render_inline(described_class.new(type: "info", heading: "Title", message: "Body", heading_level: 4))
expect(page).to have_css("h4.usa-alert__heading", text: "Title")
expect(page).to have_css("p.usa-alert__text", text: "Body")
end
end

describe "block mode (body slot)" do
it "renders slot content instead of message" do
render_inline(described_class.new(type: "error", heading: "Problems")) do |c|
c.with_body { "<ul class='usa-list'><li>a</li></ul>".html_safe }
end
expect(page).to have_css("h2.usa-alert__heading", text: "Problems")
expect(page).to have_css("ul.usa-list li", text: "a")
expect(page).to have_no_css("p.usa-alert__text")
end

it "renders heading before body when both heading and slot are used (e.g. warning + actions)" do
render_inline(described_class.new(type: "warning", heading: "Notice", heading_level: 3)) do |c|
c.with_body { "<p class='usa-alert__text'>Details</p>".html_safe }
end
expect(page).to have_css("h3.usa-alert__heading", text: "Notice")
expect(page).to have_css("p.usa-alert__text", text: "Details")
expect(page).to have_no_css('.usa-alert[role]')
end
end

describe "extra attributes" do
it "merges classes onto the root" do
render_inline(described_class.new(type: "info", message: "m", classes: "margin-top-4 usa-alert--slim"))
expect(page).to have_css(".usa-alert.margin-top-4.usa-alert--slim")
end

it "passes style to the root element" do
render_inline(described_class.new(type: "info", message: "m", style: "padding: unset;"))
expect(page).to have_css('.usa-alert[style="padding: unset;"]')
end
end

describe "heading_level validation" do
it "rejects invalid heading levels" do
expect do
described_class.new(type: "info", message: "m", heading_level: 7)
end.to raise_error(ArgumentError, /heading_level/)
end
end
end
3 changes: 3 additions & 0 deletions reporting-app/spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
require 'view_component/test_helpers'
require 'capybara/rspec'
require 'webmock/rspec'
# Add additional requires below this line. Rails is not loaded until this point!
require_relative 'support/factory_bot'
Expand Down Expand Up @@ -72,6 +74,7 @@
config.include Strata::Testing::ApiAuthHelpers
config.include Devise::Test::ControllerHelpers, type: :controller
config.include PunditSpecViewHelper, type: :view
config.include ViewComponent::TestHelpers, type: :component

# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_paths = [
Expand Down
Loading