From 8d5488c3335fed57ae127ce0e86c395ad76b2328 Mon Sep 17 00:00:00 2001 From: Cyril Date: Wed, 25 Feb 2026 15:57:01 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BF=EF=B8=8F(frontend)=20add=20skip=20lin?= =?UTF-8?q?k=20component=20for=20keyboard=20navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve a11y: skip to main heading, bypass header. RGAA 12.7. --- CHANGELOG.md | 1 + src/frontend/src/layout/Layout.tsx | 3 ++ src/frontend/src/layout/SkipLink.tsx | 69 +++++++++++++++++++++++++ src/frontend/src/locales/de/global.json | 1 + src/frontend/src/locales/en/global.json | 1 + src/frontend/src/locales/fr/global.json | 1 + src/frontend/src/locales/nl/global.json | 1 + src/frontend/src/styles/index.css | 5 ++ 8 files changed, 82 insertions(+) create mode 100644 src/frontend/src/layout/SkipLink.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 233091e1..a9c38d00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to - ♿️(frontend) upgrade join meeting modal accessibility #1027 - ⬆️(python) bump minimal required python version to 3.13 #1033 - ♿️(frontend) improve accessibility of the IntroSlider carousel #1026 +- ♿️(frontend) add skip link component for keyboard navigation #1019 ### Fixed diff --git a/src/frontend/src/layout/Layout.tsx b/src/frontend/src/layout/Layout.tsx index ce2235d8..38e657ea 100644 --- a/src/frontend/src/layout/Layout.tsx +++ b/src/frontend/src/layout/Layout.tsx @@ -5,6 +5,7 @@ import { layoutStore } from '@/stores/layout' import { useSnapshot } from 'valtio' import { Footer } from '@/layout/Footer' import { ScreenReaderAnnouncer } from '@/primitives' +import { SkipLink, MAIN_CONTENT_ID } from './SkipLink' export type Layout = 'fullpage' | 'centered' @@ -21,6 +22,7 @@ export const Layout = ({ children }: { children: ReactNode }) => { return ( <> + {showHeader && }
{ > {showHeader &&
}
{ + const { t } = useTranslation() + + const handleClick = (e: MouseEvent) => { + e.preventDefault() + const main = document.getElementById(MAIN_CONTENT_ID) + if (!main) return + + const heading = main.querySelector('h1, h2, h3') as HTMLElement | null + const target = heading ?? main + + if (!target.hasAttribute('tabindex')) { + target.setAttribute('tabindex', '-1') + } + target.focus() + } + + return ( + + {t('skipLink')} + + ) +} diff --git a/src/frontend/src/locales/de/global.json b/src/frontend/src/locales/de/global.json index ac8e3e02..3baa362d 100644 --- a/src/frontend/src/locales/de/global.json +++ b/src/frontend/src/locales/de/global.json @@ -52,6 +52,7 @@ "label": "OK" } }, + "skipLink": "Zum Hauptinhalt springen", "clipboardContent": { "url": "Um an der Videokonferenz teilzunehmen, klicken Sie auf diesen Link: {{roomUrl}}", "numberAndPin": "Um telefonisch teilzunehmen, wählen Sie {{phoneNumber}} und geben Sie diesen Code ein: {{pinCode}}" diff --git a/src/frontend/src/locales/en/global.json b/src/frontend/src/locales/en/global.json index c9adbd13..229e0d16 100644 --- a/src/frontend/src/locales/en/global.json +++ b/src/frontend/src/locales/en/global.json @@ -52,6 +52,7 @@ "label": "OK" } }, + "skipLink": "Skip to main content", "clipboardContent": { "url": "To join the video conference, click on this link: {{roomUrl}}", "numberAndPin": "To join by phone, dial {{phoneNumber}} and enter this code: {{pinCode}}" diff --git a/src/frontend/src/locales/fr/global.json b/src/frontend/src/locales/fr/global.json index 051e95ee..8eb6ccf4 100644 --- a/src/frontend/src/locales/fr/global.json +++ b/src/frontend/src/locales/fr/global.json @@ -52,6 +52,7 @@ "label": "OK" } }, + "skipLink": "Aller au contenu principal", "clipboardContent": { "url": "Pour participer à la visioconférence, cliquez sur ce lien : {{roomUrl}}", "numberAndPin": "Pour participer par téléphone, composez le {{phoneNumber}} et saisissez ce code : {{pinCode}}" diff --git a/src/frontend/src/locales/nl/global.json b/src/frontend/src/locales/nl/global.json index 292d97a3..4cb66436 100644 --- a/src/frontend/src/locales/nl/global.json +++ b/src/frontend/src/locales/nl/global.json @@ -51,6 +51,7 @@ "label": "OK" } }, + "skipLink": "Naar de hoofdinhoud gaan", "clipboardContent": { "url": "Klik op deze link om deel te nemen aan de videoconferentie: {{roomUrl}}", "numberAndPin": "Bel {{phoneNumber}} en voer deze code in om telefonisch deel te nemen: {{pinCode}}" diff --git a/src/frontend/src/styles/index.css b/src/frontend/src/styles/index.css index 2aabeb8d..900d7a3a 100644 --- a/src/frontend/src/styles/index.css +++ b/src/frontend/src/styles/index.css @@ -22,6 +22,11 @@ body, outline: 2px solid transparent; } +main#main-content :is(h1, h2, h3)[tabindex='-1']:focus { + outline: 2px solid var(--colors-focus-ring); + outline-offset: 2px; +} + [data-rac][data-focus-visible]:not(label, .react-aria-Select), :is(a, button, input[type='text'], select, textarea):not( [data-rac]