♿️(frontend) add skip link component for keyboard navigation
Improve a11y: skip to main heading, bypass header. RGAA 12.7.
This commit is contained in:
@@ -26,6 +26,7 @@ and this project adheres to
|
|||||||
- ♿️(frontend) upgrade join meeting modal accessibility #1027
|
- ♿️(frontend) upgrade join meeting modal accessibility #1027
|
||||||
- ⬆️(python) bump minimal required python version to 3.13 #1033
|
- ⬆️(python) bump minimal required python version to 3.13 #1033
|
||||||
- ♿️(frontend) improve accessibility of the IntroSlider carousel #1026
|
- ♿️(frontend) improve accessibility of the IntroSlider carousel #1026
|
||||||
|
- ♿️(frontend) add skip link component for keyboard navigation #1019
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { layoutStore } from '@/stores/layout'
|
|||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { Footer } from '@/layout/Footer'
|
import { Footer } from '@/layout/Footer'
|
||||||
import { ScreenReaderAnnouncer } from '@/primitives'
|
import { ScreenReaderAnnouncer } from '@/primitives'
|
||||||
|
import { SkipLink, MAIN_CONTENT_ID } from './SkipLink'
|
||||||
|
|
||||||
export type Layout = 'fullpage' | 'centered'
|
export type Layout = 'fullpage' | 'centered'
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export const Layout = ({ children }: { children: ReactNode }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{showHeader && <SkipLink />}
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -35,6 +37,7 @@ export const Layout = ({ children }: { children: ReactNode }) => {
|
|||||||
>
|
>
|
||||||
{showHeader && <Header />}
|
{showHeader && <Header />}
|
||||||
<main
|
<main
|
||||||
|
id={MAIN_CONTENT_ID}
|
||||||
className={css({
|
className={css({
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
|
|||||||
69
src/frontend/src/layout/SkipLink.tsx
Normal file
69
src/frontend/src/layout/SkipLink.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { type MouseEvent } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { styled } from '@/styled-system/jsx'
|
||||||
|
|
||||||
|
export const MAIN_CONTENT_ID = 'main-content'
|
||||||
|
|
||||||
|
// Visually hidden until focus (not sr-only). Must become visible on focus for keyboard users.
|
||||||
|
const StyledSkipLink = styled('a', {
|
||||||
|
base: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: '1px',
|
||||||
|
height: '1px',
|
||||||
|
margin: '-1px',
|
||||||
|
padding: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
clip: 'rect(0, 0, 0, 0)',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
border: 0,
|
||||||
|
textDecoration: 'none',
|
||||||
|
_focusVisible: {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '0.5rem',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
width: 'auto',
|
||||||
|
height: 'auto',
|
||||||
|
margin: 0,
|
||||||
|
padding: '0.625rem 1rem',
|
||||||
|
overflow: 'visible',
|
||||||
|
clip: 'auto',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
zIndex: 9999,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
color: 'primary.800',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'primary.800',
|
||||||
|
borderRadius: 4,
|
||||||
|
outline: '2px solid',
|
||||||
|
outlineColor: 'focusRing',
|
||||||
|
outlineOffset: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SkipLink = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
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 (
|
||||||
|
<StyledSkipLink href={`#${MAIN_CONTENT_ID}`} onClick={handleClick}>
|
||||||
|
{t('skipLink')}
|
||||||
|
</StyledSkipLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"label": "OK"
|
"label": "OK"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipLink": "Zum Hauptinhalt springen",
|
||||||
"clipboardContent": {
|
"clipboardContent": {
|
||||||
"url": "Um an der Videokonferenz teilzunehmen, klicken Sie auf diesen Link: {{roomUrl}}",
|
"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}}"
|
"numberAndPin": "Um telefonisch teilzunehmen, wählen Sie {{phoneNumber}} und geben Sie diesen Code ein: {{pinCode}}"
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"label": "OK"
|
"label": "OK"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipLink": "Skip to main content",
|
||||||
"clipboardContent": {
|
"clipboardContent": {
|
||||||
"url": "To join the video conference, click on this link: {{roomUrl}}",
|
"url": "To join the video conference, click on this link: {{roomUrl}}",
|
||||||
"numberAndPin": "To join by phone, dial {{phoneNumber}} and enter this code: {{pinCode}}"
|
"numberAndPin": "To join by phone, dial {{phoneNumber}} and enter this code: {{pinCode}}"
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"label": "OK"
|
"label": "OK"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipLink": "Aller au contenu principal",
|
||||||
"clipboardContent": {
|
"clipboardContent": {
|
||||||
"url": "Pour participer à la visioconférence, cliquez sur ce lien : {{roomUrl}}",
|
"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}}"
|
"numberAndPin": "Pour participer par téléphone, composez le {{phoneNumber}} et saisissez ce code : {{pinCode}}"
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"label": "OK"
|
"label": "OK"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skipLink": "Naar de hoofdinhoud gaan",
|
||||||
"clipboardContent": {
|
"clipboardContent": {
|
||||||
"url": "Klik op deze link om deel te nemen aan de videoconferentie: {{roomUrl}}",
|
"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}}"
|
"numberAndPin": "Bel {{phoneNumber}} en voer deze code in om telefonisch deel te nemen: {{pinCode}}"
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ body,
|
|||||||
outline: 2px solid transparent;
|
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),
|
[data-rac][data-focus-visible]:not(label, .react-aria-Select),
|
||||||
:is(a, button, input[type='text'], select, textarea):not(
|
:is(a, button, input[type='text'], select, textarea):not(
|
||||||
[data-rac]
|
[data-rac]
|
||||||
|
|||||||
Reference in New Issue
Block a user