diff --git a/app/views/mfa_confirmation/show.html.erb b/app/views/mfa_confirmation/show.html.erb
index d88f2569c95..806ece8ff5e 100644
--- a/app/views/mfa_confirmation/show.html.erb
+++ b/app/views/mfa_confirmation/show.html.erb
@@ -1,6 +1,6 @@
<% self.title = @content.heading %>
-<%= image_tag asset_url('user-signup-ial1.svg'), width: 107, height: 119, alt: '', class: 'margin-bottom-4', role: 'img' %>
+<%= image_tag asset_url('user-signup-ial1.svg'), width: 107, height: 119, alt: '', class: 'margin-bottom-4', aria: { hidden: true } %>
<%= render PageHeadingComponent.new.with_content(@content.heading) %>
diff --git a/app/views/openid_connect/logout/confirm_logout.html.erb b/app/views/openid_connect/logout/confirm_logout.html.erb
index 0e474590447..4e9a2bd28b8 100644
--- a/app/views/openid_connect/logout/confirm_logout.html.erb
+++ b/app/views/openid_connect/logout/confirm_logout.html.erb
@@ -1,7 +1,7 @@
<% self.title = t('openid_connect.logout.heading', app_name: APP_NAME) %>
- <%= image_tag(asset_url('user-access.svg'), width: '280', height: '91', alt: '', role: 'img') %>
+ <%= image_tag(asset_url('user-access.svg'), width: '280', height: '91', alt: '', aria: { hidden: true }) %>
<% if @service_provider_name.present? %>
<%= render PageHeadingComponent.new.with_content(t('openid_connect.logout.heading_with_sp', app_name: APP_NAME, service_provider_name: @service_provider_name)) %>
<% else %>
diff --git a/app/views/partials/multi_factor_authentication/_mfa_selection.html.erb b/app/views/partials/multi_factor_authentication/_mfa_selection.html.erb
index a9fcb0a3697..e8888d7e892 100644
--- a/app/views/partials/multi_factor_authentication/_mfa_selection.html.erb
+++ b/app/views/partials/multi_factor_authentication/_mfa_selection.html.erb
@@ -17,7 +17,7 @@
"two_factor_options_form_selection_#{option.type}",
class: 'usa-checkbox__label usa-checkbox__label--illustrated',
) do %>
- <%= image_tag(asset_url("mfa-options/#{option.type}.svg"), alt: '', class: 'usa-checkbox__image', role: 'img') %>
+ <%= image_tag(asset_url("mfa-options/#{option.type}.svg"), alt: '', class: 'usa-checkbox__image', aria: { hidden: true }) %>
<%= option.label %>
<%= content_tag(
diff --git a/app/views/shared/_banner.html.erb b/app/views/shared/_banner.html.erb
index eb51448c7ef..90966378a74 100644
--- a/app/views/shared/_banner.html.erb
+++ b/app/views/shared/_banner.html.erb
@@ -28,7 +28,6 @@
asset_url('icon-dot-gov.svg'),
size: 40,
alt: '',
- role: 'img',
aria: { hidden: true },
class: 'usa-banner__icon usa-media-block__img',
) %>
@@ -44,7 +43,6 @@
asset_url('icon-https.svg'),
size: 40,
alt: '',
- role: 'img',
aria: { hidden: true },
class: 'usa-banner__icon usa-media-block__img',
) %>
diff --git a/app/views/shared/_footer_lite.html.erb b/app/views/shared/_footer_lite.html.erb
index 0229d88350d..a2e8d100e9a 100644
--- a/app/views/shared/_footer_lite.html.erb
+++ b/app/views/shared/_footer_lite.html.erb
@@ -1,6 +1,6 @@
diff --git a/app/views/sign_up/registrations/_sp_registration_heading.html.erb b/app/views/sign_up/registrations/_sp_registration_heading.html.erb
index 4de0f5ba94f..6b693ef2e1e 100644
--- a/app/views/sign_up/registrations/_sp_registration_heading.html.erb
+++ b/app/views/sign_up/registrations/_sp_registration_heading.html.erb
@@ -1,5 +1,5 @@
- <%= image_tag(asset_url('user-access.svg'), width: '280', height: '91', alt: '', role: 'img') %>
+ <%= image_tag(asset_url('user-access.svg'), width: '280', height: '91', alt: '', aria: { hidden: true }) %>
<%= decorated_sp_session.sp_name %>
<%= t('headings.create_account_with_sp.sp_text', app_name: APP_NAME) %>
diff --git a/app/views/users/backup_code_setup/reminder.html.erb b/app/views/users/backup_code_setup/reminder.html.erb
index 3a12ba1af86..c36c6d72b1d 100644
--- a/app/views/users/backup_code_setup/reminder.html.erb
+++ b/app/views/users/backup_code_setup/reminder.html.erb
@@ -1,6 +1,6 @@
<% self.title = t('forms.backup_code.title') %>
-<%= image_tag asset_url('user-signup-ial1.svg'), width: 107, height: 119, alt: '', class: 'margin-bottom-4', role: 'img' %>
+<%= image_tag asset_url('user-signup-ial1.svg'), width: 107, height: 119, alt: '', class: 'margin-bottom-4', aria: { hidden: true } %>
<%= render PageHeadingComponent.new.with_content(t('forms.backup_code_reminder.heading')) %>
diff --git a/app/views/users/webauthn_setup/new.html.erb b/app/views/users/webauthn_setup/new.html.erb
index 4b2e391980b..1323e2fe05d 100644
--- a/app/views/users/webauthn_setup/new.html.erb
+++ b/app/views/users/webauthn_setup/new.html.erb
@@ -1,7 +1,7 @@
<% self.title = @presenter.page_title %>
<% if @platform_authenticator %>
- <%= image_tag asset_url('platform-authenticator.svg'), alt: '', width: 84, height: 95, class: 'margin-left-1 margin-bottom-2', role: 'img' %>
+ <%= image_tag asset_url('platform-authenticator.svg'), alt: '', width: 84, height: 95, class: 'margin-left-1 margin-bottom-2', aria: { hidden: true } %>
<% end %>
<%= render PageHeadingComponent.new.with_content(@presenter.heading) %>
diff --git a/spec/support/matchers/accessibility.rb b/spec/support/matchers/accessibility.rb
index e398cebcb3a..f11ae053a58 100644
--- a/spec/support/matchers/accessibility.rb
+++ b/spec/support/matchers/accessibility.rb
@@ -187,22 +187,32 @@ def ids(page)
end
end
-RSpec::Matchers.define :tag_decorative_svgs_with_role do
+RSpec::Matchers.define :tag_decorative_svgs_with_aria_hidden do
+ # VoiceOver on Safari will erroneously announce SVG
elements with a null text alternative,
+ # even with `role="presentation"` (see LG-12465). Assigning `aria-hidden` is equivalent to
+ # `role="presentation"` for image elements, and prevents the images from being announced. This
+ # should be removed if and when the bug is fixed in a future version of Safari.
+ #
+ # "Consequently, using role="presentation" or role="none" on an HTML img is equivalent to using
+ # aria-hidden="true"."
+ #
+ # See: https://www.w3.org/TR/wai-aria-1.2/#presentation
+
def decorative_svgs(page)
page.all(:css, 'img[alt=""][src$=".svg" i]')
end
match do |page|
- expect(decorative_svgs(page)).to all satisfy { |img| img[:role] == 'img' }
+ expect(decorative_svgs(page)).to all satisfy { |img| !img[:'aria-hidden'].nil? }
end
failure_message do |page|
- img_tags = decorative_svgs(page).reject { |img| img[:role] == 'img' }.
+ img_tags = decorative_svgs(page).select { |img| img[:'aria-hidden'].nil? }.
map { |img| %(
) }.
join("\n")
<<~STR
- Expect all decorative SVGs to have role="img", but found ones without:
+ Expect all decorative SVGs to have aria-hidden, but found ones without:
#{img_tags}
STR
end
@@ -327,15 +337,12 @@ def fieldset_legend_name(element)
end
def expect_page_to_have_no_accessibility_violations(page, validate_markup: true)
- expect(page).to be_axe_clean.according_to(:wcag22aa, :"best-practice").
- # Axe flags redundant img role on img elements, but is necessary for a Safari bug
- # See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#identifying_svg_as_an_image
- excluding('img[alt=""][src$=".svg" i]')
+ expect(page).to be_axe_clean.according_to(:wcag22aa, :"best-practice")
expect(page).to have_unique_ids
expect(page).to have_valid_idrefs
expect(page).to label_required_fields
expect(page).to have_valid_markup if validate_markup
- expect(page).to tag_decorative_svgs_with_role
+ expect(page).to tag_decorative_svgs_with_aria_hidden
end
def activate_skip_link