♿️(frontend) improve IntroSlider accessibility for screen readers
add aria-labels with slide position, carousel semantics, live region
This commit is contained in:
@@ -2,7 +2,7 @@ import { styled } from '@/styled-system/jsx'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Button } from '@/primitives'
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Heading = styled('h2', {
|
||||
@@ -162,12 +162,37 @@ const SLIDES: Slide[] = [
|
||||
|
||||
export const IntroSlider = () => {
|
||||
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 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 (
|
||||
<Container>
|
||||
<Container
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
aria-label={t('carouselLabel')}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
@@ -178,11 +203,14 @@ export const IntroSlider = () => {
|
||||
<ButtonContainer>
|
||||
<ButtonVerticalCenter>
|
||||
<Button
|
||||
ref={prevButtonRef}
|
||||
variant="secondaryText"
|
||||
square
|
||||
aria-label={t('previous.label')}
|
||||
tooltip={t('previous.tooltip')}
|
||||
onPress={() => setSlideIndex(slideIndex - 1)}
|
||||
aria-label={previousAriaLabel}
|
||||
onPress={() => {
|
||||
focusTargetRef.current = 'prev'
|
||||
setSlideIndex(prevSlideIndex)
|
||||
}}
|
||||
isDisabled={slideIndex == 0}
|
||||
>
|
||||
<RiArrowLeftSLine />
|
||||
@@ -203,11 +231,14 @@ export const IntroSlider = () => {
|
||||
<ButtonContainer>
|
||||
<ButtonVerticalCenter>
|
||||
<Button
|
||||
ref={nextButtonRef}
|
||||
variant="secondaryText"
|
||||
square
|
||||
aria-label={t('next.label')}
|
||||
tooltip={t('next.tooltip')}
|
||||
onPress={() => setSlideIndex(slideIndex + 1)}
|
||||
aria-label={nextAriaLabel}
|
||||
onPress={() => {
|
||||
focusTargetRef.current = 'next'
|
||||
setSlideIndex(nextSlideIndex)
|
||||
}}
|
||||
isDisabled={slideIndex == NUMBER_SLIDES - 1}
|
||||
>
|
||||
<RiArrowRightSLine />
|
||||
@@ -219,8 +250,18 @@ export const IntroSlider = () => {
|
||||
className={css({
|
||||
marginTop: '0.5rem',
|
||||
display: { base: 'none', xsm: 'block' },
|
||||
|
||||
})}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t('slidePosition', {
|
||||
current: slideIndex + 1,
|
||||
total: NUMBER_SLIDES,
|
||||
})}
|
||||
</span>
|
||||
{SLIDES.map((_, index) => (
|
||||
<Dot key={index} selected={index == slideIndex} />
|
||||
))}
|
||||
|
||||
@@ -31,12 +31,14 @@
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "Zurück",
|
||||
"tooltip": "Zurück"
|
||||
"label": "Vorherige Folie",
|
||||
"withPosition": "Vorherige Folie anzeigen ({{target}} von {{total}})",
|
||||
"tooltip": "Vorherige Folie"
|
||||
},
|
||||
"next": {
|
||||
"label": "Weiter",
|
||||
"tooltip": "Weiter"
|
||||
"label": "Nächste Folie",
|
||||
"withPosition": "Nächste Folie anzeigen ({{target}} von {{total}})",
|
||||
"tooltip": "Nächste Folie"
|
||||
},
|
||||
"beta": {
|
||||
"text": "An der Beta teilnehmen",
|
||||
@@ -53,6 +55,8 @@
|
||||
"slide3": {
|
||||
"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!"
|
||||
}
|
||||
},
|
||||
"carouselLabel": "Einführungs-Diashow",
|
||||
"slidePosition": "Folie {{current}} von {{total}}"
|
||||
}
|
||||
}
|
||||
@@ -31,12 +31,14 @@
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "previous",
|
||||
"tooltip": "previous"
|
||||
"label": "Previous slide",
|
||||
"withPosition": "Go to previous slide ({{target}} of {{total}})",
|
||||
"tooltip": "Previous slide"
|
||||
},
|
||||
"next": {
|
||||
"label": "next",
|
||||
"tooltip": "next"
|
||||
"label": "Next slide",
|
||||
"withPosition": "Go to next slide ({{target}} of {{total}})",
|
||||
"tooltip": "Next slide"
|
||||
},
|
||||
"beta": {
|
||||
"text": "Join the beta",
|
||||
@@ -53,6 +55,8 @@
|
||||
"slide3": {
|
||||
"title": "Transform your meetings with AI",
|
||||
"body": "Get accurate and actionable transcripts to boost your productivity. Feature in beta—try it now!"
|
||||
}
|
||||
},
|
||||
"carouselLabel": "Introduction slideshow",
|
||||
"slidePosition": "Slide {{current}} of {{total}}"
|
||||
}
|
||||
}
|
||||
@@ -30,14 +30,18 @@
|
||||
}
|
||||
},
|
||||
"introSlider": {
|
||||
"carouselLabel": "Diaporama de présentation",
|
||||
"previous": {
|
||||
"label": "précédent",
|
||||
"tooltip": "précédent"
|
||||
"label": "Diapositive précédente",
|
||||
"withPosition": "Afficher la diapositive précédente ({{target}} sur {{total}})",
|
||||
"tooltip": "Diapositive précédente"
|
||||
},
|
||||
"next": {
|
||||
"label": "suivant",
|
||||
"tooltip": "suivant"
|
||||
"label": "Diapositive suivante",
|
||||
"withPosition": "Afficher la diapositive suivante ({{target}} sur {{total}})",
|
||||
"tooltip": "Diapositive suivante"
|
||||
},
|
||||
"slidePosition": "Diapositive {{current}} sur {{total}}",
|
||||
"beta": {
|
||||
"text": "Essayer la beta",
|
||||
"tooltip": "Accéder au formulaire"
|
||||
|
||||
@@ -31,12 +31,14 @@
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "vorige",
|
||||
"tooltip": "vorige"
|
||||
"label": "Vorige dia",
|
||||
"withPosition": "Vorige dia tonen ({{target}} van {{total}})",
|
||||
"tooltip": "Vorige dia"
|
||||
},
|
||||
"next": {
|
||||
"label": "volgende",
|
||||
"tooltip": "volgende"
|
||||
"label": "Volgende dia",
|
||||
"withPosition": "Volgende dia tonen ({{target}} van {{total}})",
|
||||
"tooltip": "Volgende dia"
|
||||
},
|
||||
"beta": {
|
||||
"text": "Word lid van de bèta",
|
||||
@@ -53,6 +55,8 @@
|
||||
"slide3": {
|
||||
"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!"
|
||||
}
|
||||
},
|
||||
"carouselLabel": "Introductie-diavoorstelling",
|
||||
"slidePosition": "Dia {{current}} van {{total}}"
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,13 @@ body,
|
||||
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) {
|
||||
* {
|
||||
animation: none !important;
|
||||
|
||||
Reference in New Issue
Block a user