fix(a11y): add skip-link and aria-current="page" to application layout (#1781)

* fix(a11y): add skip-link and aria-current="page" to application layout

* test(a11y): cover application layout skip-link and #main anchor

* fix(a11y): extend skip-link and #main anchor to settings layout
This commit is contained in:
0xτensor
2026-05-14 12:53:31 -07:00
committed by GitHub
parent c106aaf10d
commit 0ad1e59165
19 changed files with 77 additions and 3 deletions

View File

@@ -26,6 +26,9 @@ end %>
data-app-layout-expanded-sidebar-class="<%= expanded_sidebar_class %>"
data-app-layout-collapsed-sidebar-class="<%= collapsed_sidebar_class %>"
data-app-layout-user-id-value="<%= Current.user.id %>">
<%= link_to t("layouts.application.skip_to_main"), "#main",
class: "sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:px-3 focus:py-2 focus:rounded-lg focus:bg-container focus:text-primary focus:shadow-border-xs" %>
<div
class="hidden fixed inset-0 bg-surface z-20 h-full w-full pt-[calc(env(safe-area-inset-top)+0.75rem)] pr-3 pb-[calc(env(safe-area-inset-bottom)+0.75rem)] pl-3 overflow-y-auto transition-all duration-300"
data-app-layout-target="mobileSidebar">
@@ -139,7 +142,7 @@ end %>
<% end %>
<%# SHARED - Main content %>
<%= tag.main class: class_names("grow overflow-y-auto px-3 lg:px-10 w-full mx-auto pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-0"), data: { app_layout_target: "content", viewport_target: "content" } do %>
<%= tag.main id: "main", class: class_names("grow overflow-y-auto px-3 lg:px-10 w-full mx-auto pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-0"), data: { app_layout_target: "content", viewport_target: "content" } do %>
<% unless intro_mode %>
<div class="hidden lg:flex gap-2 items-center justify-between mb-6 sticky top-0 z-10 -mx-3 lg:-mx-10 px-3 lg:px-10 py-4 bg-surface border-b border-tertiary">
<div class="flex items-center gap-2">

View File

@@ -1,12 +1,15 @@
<%= render "layouts/shared/htmldoc" do %>
<div class="flex flex-col md:flex-row h-full bg-surface pt-[env(safe-area-inset-top)]">
<%= link_to t("layouts.application.skip_to_main"), "#main",
class: "sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:px-3 focus:py-2 focus:rounded-lg focus:bg-container focus:text-primary focus:shadow-border-xs" %>
<div class="w-full md:w-auto md:min-w-64 shrink-0 md:h-full md:overflow-y-auto border-divider md:border-r">
<div class="p-4">
<%= render "settings/settings_nav" %>
</div>
</div>
<main class="grow flex h-full">
<main id="main" class="grow flex h-full">
<div class="relative max-w-4xl mx-auto flex flex-col w-full h-full">
<div class="grow flex flex-col overflow-y-auto overflow-x-hidden overscroll-contain [-webkit-overflow-scrolling:touch]">
<div class="sticky top-0 z-10 px-3 md:px-10 pt-1.5 md:pt-3 pb-3 bg-surface border-b border-tertiary shrink-0">

View File

@@ -1,6 +1,6 @@
<%# locals:(name:, path:, icon:, icon_custom:, active:, mobile_only: false) %>
<%= link_to path, class: "space-y-1 group block relative pb-1" do %>
<%= link_to path, class: "space-y-1 group block relative pb-1", aria: { current: ("page" if active) } do %>
<div class="grow flex flex-col lg:flex-row gap-1 items-center">
<%= tag.div class: class_names("w-4 h-1 lg:w-1 lg:h-4 rounded-bl-sm rounded-br-sm lg:rounded-tr-sm lg:rounded-br-sm lg:rounded-bl-none", "bg-nav-indicator" => active) %>

View File

@@ -3,6 +3,7 @@ ca:
layouts:
application:
privacy_mode: Alternar mode de privadesa
skip_to_main: Saltar al contingut principal
nav:
assistant: Assistent
budgets: Pressupostos

View File

@@ -3,6 +3,7 @@ de:
layouts:
application:
privacy_mode: Datenschutzmodus umschalten
skip_to_main: Zum Hauptinhalt springen
nav:
assistant: Assistent
budgets: Budgets

View File

@@ -3,6 +3,7 @@ en:
layouts:
application:
privacy_mode: Toggle privacy mode
skip_to_main: Skip to main content
nav:
assistant: Assistant
budgets: Budgets

View File

@@ -3,6 +3,7 @@ es:
layouts:
application:
privacy_mode: Alternar modo de privacidad
skip_to_main: Saltar al contenido principal
nav:
assistant: Asistente
budgets: Presupuestos

View File

@@ -3,6 +3,7 @@ fr:
layouts:
application:
privacy_mode: Activer/désactiver le mode confidentialité
skip_to_main: Aller au contenu principal
nav:
assistant: Assistant
budgets: Budgets

View File

@@ -3,6 +3,7 @@ hu:
layouts:
application:
privacy_mode: Adatvédelmi mód váltása
skip_to_main: Ugrás a fő tartalomhoz
nav:
assistant: Asszisztens
budgets: Költségvetések

View File

@@ -3,6 +3,7 @@ nb:
layouts:
application:
privacy_mode: Veksle personvernmodus
skip_to_main: Hopp til hovedinnhold
nav:
assistant: Assistent
budgets: Budsjett

View File

@@ -3,6 +3,7 @@ nl:
layouts:
application:
privacy_mode: Privacymodus in-/uitschakelen
skip_to_main: Ga naar hoofdinhoud
nav:
assistant: Assistent
budgets: Budgetten

View File

@@ -3,6 +3,7 @@ pl:
layouts:
application:
privacy_mode: Przełącz tryb prywatności
skip_to_main: Przejdź do głównej treści
nav:
assistant: Asystent
budgets: Budżety

View File

@@ -3,6 +3,7 @@ pt-BR:
layouts:
application:
privacy_mode: Alternar modo de privacidade
skip_to_main: Pular para o conteúdo principal
nav:
assistant: Assistente
budgets: Orçamentos

View File

@@ -3,6 +3,7 @@ ro:
layouts:
application:
privacy_mode: Comutare mod confidențialitate
skip_to_main: Sari la conținutul principal
nav:
assistant: Asistent
budgets: Bugete

View File

@@ -3,6 +3,7 @@ tr:
layouts:
application:
privacy_mode: Gizlilik modunu değiştir
skip_to_main: Ana içeriğe atla
nav:
assistant: Asistan
budgets: Bütçeler

View File

@@ -4,6 +4,7 @@ zh-CN:
layouts:
application:
privacy_mode: 切换隐私模式
skip_to_main: 跳到主要内容
nav:
assistant: 智能助手
budgets: 预算管理

View File

@@ -3,6 +3,7 @@ zh-TW:
layouts:
application:
privacy_mode: 切換隱私模式
skip_to_main: 跳至主要內容
nav:
assistant: 助手
budgets: 預算

View File

@@ -0,0 +1,27 @@
require "test_helper"
class LayoutAccessibilityTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:family_admin)
end
test "application layout renders skip-link pointing at #main and a <main> with id=\"main\"" do
get root_path
assert_response :ok
skip_text = I18n.t("layouts.application.skip_to_main")
assert_select "a[href=\"#main\"]", text: skip_text
assert_select "main#main"
end
test "settings layout renders skip-link pointing at #main and a <main> with id=\"main\"" do
get settings_profile_path
assert_response :ok
skip_text = I18n.t("layouts.application.skip_to_main")
assert_select "a[href=\"#main\"]", text: skip_text
assert_select "main#main"
end
end

View File

@@ -0,0 +1,27 @@
require "test_helper"
class NavItemViewTest < ActionView::TestCase
test "active nav item carries aria-current=\"page\"" do
html = render(partial: "layouts/shared/nav_item", locals: {
name: "Transactions",
path: "/transactions",
icon: "credit-card",
icon_custom: false,
active: true
})
assert_includes html, "aria-current=\"page\""
end
test "inactive nav item omits aria-current" do
html = render(partial: "layouts/shared/nav_item", locals: {
name: "Transactions",
path: "/transactions",
icon: "credit-card",
icon_custom: false,
active: false
})
assert_not_includes html, "aria-current"
end
end