️(frontend) improve IntroSlider accessibility for screen readers

add aria-labels with slide position, carousel semantics, live region
This commit is contained in:
Cyril
2026-02-26 09:21:39 +01:00
committed by aleb_the_flash
parent 4b76e9571f
commit 116db1e697
6 changed files with 97 additions and 33 deletions

View File

@@ -2,7 +2,7 @@ import { styled } from '@/styled-system/jsx'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { Button } from '@/primitives' import { Button } from '@/primitives'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react' import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { useState } from 'react' import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const Heading = styled('h2', { const Heading = styled('h2', {
@@ -162,12 +162,37 @@ const SLIDES: Slide[] = [
export const IntroSlider = () => { export const IntroSlider = () => {
const [slideIndex, setSlideIndex] = useState(0) const [slideIndex, setSlideIndex] = useState(0)
const prevButtonRef = useRef<HTMLButtonElement>(null)
const nextButtonRef = useRef<HTMLButtonElement>(null)
const focusTargetRef = useRef<'prev' | 'next' | null>(null)
const { t } = useTranslation('home', { keyPrefix: 'introSlider' }) const { t } = useTranslation('home', { keyPrefix: 'introSlider' })
const NUMBER_SLIDES = SLIDES.length const NUMBER_SLIDES = SLIDES.length
const prevSlideIndex = slideIndex - 1
const nextSlideIndex = slideIndex + 1
const previousAriaLabel =
slideIndex > 0
? t('previous.withPosition', {
target: prevSlideIndex + 1,
total: NUMBER_SLIDES,
})
: t('previous.label')
const nextAriaLabel =
slideIndex < NUMBER_SLIDES - 1
? t('next.withPosition', {
target: nextSlideIndex + 1,
total: NUMBER_SLIDES,
})
: t('next.label')
return ( return (
<Container> <Container
role="region"
aria-roledescription="carousel"
aria-label={t('carouselLabel')}
>
<div <div
className={css({ className={css({
display: 'flex', display: 'flex',
@@ -178,11 +203,14 @@ export const IntroSlider = () => {
<ButtonContainer> <ButtonContainer>
<ButtonVerticalCenter> <ButtonVerticalCenter>
<Button <Button
ref={prevButtonRef}
variant="secondaryText" variant="secondaryText"
square square
aria-label={t('previous.label')} aria-label={previousAriaLabel}
tooltip={t('previous.tooltip')} onPress={() => {
onPress={() => setSlideIndex(slideIndex - 1)} focusTargetRef.current = 'prev'
setSlideIndex(prevSlideIndex)
}}
isDisabled={slideIndex == 0} isDisabled={slideIndex == 0}
> >
<RiArrowLeftSLine /> <RiArrowLeftSLine />
@@ -203,11 +231,14 @@ export const IntroSlider = () => {
<ButtonContainer> <ButtonContainer>
<ButtonVerticalCenter> <ButtonVerticalCenter>
<Button <Button
ref={nextButtonRef}
variant="secondaryText" variant="secondaryText"
square square
aria-label={t('next.label')} aria-label={nextAriaLabel}
tooltip={t('next.tooltip')} onPress={() => {
onPress={() => setSlideIndex(slideIndex + 1)} focusTargetRef.current = 'next'
setSlideIndex(nextSlideIndex)
}}
isDisabled={slideIndex == NUMBER_SLIDES - 1} isDisabled={slideIndex == NUMBER_SLIDES - 1}
> >
<RiArrowRightSLine /> <RiArrowRightSLine />
@@ -219,11 +250,21 @@ export const IntroSlider = () => {
className={css({ className={css({
marginTop: '0.5rem', marginTop: '0.5rem',
display: { base: 'none', xsm: 'block' }, display: { base: 'none', xsm: 'block' },
})} })}
role="status"
aria-live="polite"
aria-atomic="true"
> >
{SLIDES.map((_, index) => ( <span className="sr-only">
<Dot key={index} selected={index == slideIndex} /> {t('slidePosition', {
))} current: slideIndex + 1,
total: NUMBER_SLIDES,
})}
</span>
{SLIDES.map((_, index) => (
<Dot key={index} selected={index == slideIndex} />
))}
</div> </div>
</Container> </Container>
) )

View File

@@ -31,12 +31,14 @@
}, },
"introSlider": { "introSlider": {
"previous": { "previous": {
"label": "Zurück", "label": "Vorherige Folie",
"tooltip": "Zurück" "withPosition": "Vorherige Folie anzeigen ({{target}} von {{total}})",
"tooltip": "Vorherige Folie"
}, },
"next": { "next": {
"label": "Weiter", "label": "Nächste Folie",
"tooltip": "Weiter" "withPosition": "Nächste Folie anzeigen ({{target}} von {{total}})",
"tooltip": "Nächste Folie"
}, },
"beta": { "beta": {
"text": "An der Beta teilnehmen", "text": "An der Beta teilnehmen",
@@ -53,6 +55,8 @@
"slide3": { "slide3": {
"title": "Verwandeln Sie Ihre Meetings mit KI", "title": "Verwandeln Sie Ihre Meetings mit KI",
"body": "Erhalten Sie präzise und verwertbare Transkripte zur Steigerung Ihrer Produktivität. Funktion in der Beta jetzt testen!" "body": "Erhalten Sie präzise und verwertbare Transkripte zur Steigerung Ihrer Produktivität. Funktion in der Beta jetzt testen!"
} },
"carouselLabel": "Einführungs-Diashow",
"slidePosition": "Folie {{current}} von {{total}}"
} }
} }

View File

@@ -31,12 +31,14 @@
}, },
"introSlider": { "introSlider": {
"previous": { "previous": {
"label": "previous", "label": "Previous slide",
"tooltip": "previous" "withPosition": "Go to previous slide ({{target}} of {{total}})",
"tooltip": "Previous slide"
}, },
"next": { "next": {
"label": "next", "label": "Next slide",
"tooltip": "next" "withPosition": "Go to next slide ({{target}} of {{total}})",
"tooltip": "Next slide"
}, },
"beta": { "beta": {
"text": "Join the beta", "text": "Join the beta",
@@ -53,6 +55,8 @@
"slide3": { "slide3": {
"title": "Transform your meetings with AI", "title": "Transform your meetings with AI",
"body": "Get accurate and actionable transcripts to boost your productivity. Feature in beta—try it now!" "body": "Get accurate and actionable transcripts to boost your productivity. Feature in beta—try it now!"
} },
"carouselLabel": "Introduction slideshow",
"slidePosition": "Slide {{current}} of {{total}}"
} }
} }

View File

@@ -30,14 +30,18 @@
} }
}, },
"introSlider": { "introSlider": {
"carouselLabel": "Diaporama de présentation",
"previous": { "previous": {
"label": "précédent", "label": "Diapositive précédente",
"tooltip": "précédent" "withPosition": "Afficher la diapositive précédente ({{target}} sur {{total}})",
"tooltip": "Diapositive précédente"
}, },
"next": { "next": {
"label": "suivant", "label": "Diapositive suivante",
"tooltip": "suivant" "withPosition": "Afficher la diapositive suivante ({{target}} sur {{total}})",
"tooltip": "Diapositive suivante"
}, },
"slidePosition": "Diapositive {{current}} sur {{total}}",
"beta": { "beta": {
"text": "Essayer la beta", "text": "Essayer la beta",
"tooltip": "Accéder au formulaire" "tooltip": "Accéder au formulaire"

View File

@@ -31,12 +31,14 @@
}, },
"introSlider": { "introSlider": {
"previous": { "previous": {
"label": "vorige", "label": "Vorige dia",
"tooltip": "vorige" "withPosition": "Vorige dia tonen ({{target}} van {{total}})",
"tooltip": "Vorige dia"
}, },
"next": { "next": {
"label": "volgende", "label": "Volgende dia",
"tooltip": "volgende" "withPosition": "Volgende dia tonen ({{target}} van {{total}})",
"tooltip": "Volgende dia"
}, },
"beta": { "beta": {
"text": "Word lid van de bèta", "text": "Word lid van de bèta",
@@ -53,6 +55,8 @@
"slide3": { "slide3": {
"title": "Transformeer uw vergaderingen met AI", "title": "Transformeer uw vergaderingen met AI",
"body": "Krijg nauwkeurige en bruikbare transcripties om uw productiviteit te stimuleren. Deze mogelijkheid is in bèta, probeer het nu!" "body": "Krijg nauwkeurige en bruikbare transcripties om uw productiviteit te stimuleren. Deze mogelijkheid is in bèta, probeer het nu!"
} },
"carouselLabel": "Introductie-diavoorstelling",
"slidePosition": "Dia {{current}} van {{total}}"
} }
} }

View File

@@ -30,6 +30,13 @@ body,
outline-offset: 1px; outline-offset: 1px;
} }
/* Fallback for NVDA / screen readers: ensure focus ring on :focus when
focus-visible detection fails (e.g. carousel nav buttons) */
.carousel-nav-button:focus {
outline: 2px solid var(--colors-focus-ring) !important;
outline-offset: 1px;
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
* { * {
animation: none !important; animation: none !important;