diff --git a/CHANGELOG.md b/CHANGELOG.md index 47178b5f..233091e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to - ♿️(frontend) fix focus ring on tab container components #1012 - ♿️(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 ### Fixed diff --git a/src/frontend/src/features/home/components/IntroSlider.tsx b/src/frontend/src/features/home/components/IntroSlider.tsx index 309acd61..51dd5be8 100644 --- a/src/frontend/src/features/home/components/IntroSlider.tsx +++ b/src/frontend/src/features/home/components/IntroSlider.tsx @@ -2,8 +2,9 @@ import { styled } from '@/styled-system/jsx' import { css } from '@/styled-system/css' import { Button } from '@/primitives' import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react' -import { useRef, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce' const Heading = styled('h2', { base: { @@ -144,6 +145,21 @@ type Slide = { isAvailableInBeta?: boolean } +const carouselNavButton = css({ + _focusVisible: { + outline: '2px solid var(--colors-focus-ring) !important', + outlineOffset: '1px', + }, + _disabled: { + color: 'greyscale.400', + cursor: 'default', + pointerEvents: 'none', + _pressed: { + backgroundColor: 'transparent', + }, + }, +}) + // todo - optimize how images are imported const SLIDES: Slide[] = [ { @@ -162,30 +178,39 @@ const SLIDES: Slide[] = [ export const IntroSlider = () => { const [slideIndex, setSlideIndex] = useState(0) - const prevButtonRef = useRef(null) - const nextButtonRef = useRef(null) - const focusTargetRef = useRef<'prev' | 'next' | null>(null) const { t } = useTranslation('home', { keyPrefix: 'introSlider' }) + const announce = useScreenReaderAnnounce() const NUMBER_SLIDES = SLIDES.length - const prevSlideIndex = slideIndex - 1 - const nextSlideIndex = slideIndex + 1 + const goPrev = () => { + if (slideIndex === 0) return + const newIndex = slideIndex - 1 + setSlideIndex(newIndex) + announce( + t('slidePosition', { current: newIndex + 1, total: NUMBER_SLIDES }), + 'polite', + 'global' + ) + } - 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') + const goNext = () => { + if (slideIndex === NUMBER_SLIDES - 1) return + const newIndex = slideIndex + 1 + setSlideIndex(newIndex) + announce( + t('slidePosition', { current: newIndex + 1, total: NUMBER_SLIDES }), + 'polite', + 'global' + ) + } + + const ariaLabelParams = { + current: slideIndex + 1, + total: NUMBER_SLIDES, + } + const previousAriaLabel = t('previous.labelWithPosition', ariaLabelParams) + const nextAriaLabel = t('next.labelWithPosition', ariaLabelParams) return ( { @@ -220,7 +241,11 @@ export const IntroSlider = () => { {SLIDES.map((slide, index) => ( - + {t(`${slide.key}.title`)} @@ -232,16 +257,12 @@ export const IntroSlider = () => { @@ -252,21 +273,11 @@ export const IntroSlider = () => { className={css({ marginTop: '0.5rem', display: { base: 'none', xsm: 'block' }, - })} - role="status" - aria-live="polite" - aria-atomic="true" > - - {t('slidePosition', { - current: slideIndex + 1, - total: NUMBER_SLIDES, - })} - - {SLIDES.map((_, index) => ( - - ))} + {SLIDES.map((_, index) => ( + + ))} ) diff --git a/src/frontend/src/locales/de/home.json b/src/frontend/src/locales/de/home.json index 20199fba..bdc3839e 100644 --- a/src/frontend/src/locales/de/home.json +++ b/src/frontend/src/locales/de/home.json @@ -32,12 +32,12 @@ "introSlider": { "previous": { "label": "Vorherige Folie", - "withPosition": "Vorherige Folie anzeigen ({{target}} von {{total}})", + "labelWithPosition": "Vorherige Folie ({{current}} von {{total}})", "tooltip": "Vorherige Folie" }, "next": { "label": "Nächste Folie", - "withPosition": "Nächste Folie anzeigen ({{target}} von {{total}})", + "labelWithPosition": "Nächste Folie ({{current}} von {{total}})", "tooltip": "Nächste Folie" }, "beta": { @@ -59,4 +59,4 @@ "carouselLabel": "Einführungs-Diashow", "slidePosition": "Folie {{current}} von {{total}}" } -} \ No newline at end of file +} diff --git a/src/frontend/src/locales/en/home.json b/src/frontend/src/locales/en/home.json index 511fa890..7cc6543a 100644 --- a/src/frontend/src/locales/en/home.json +++ b/src/frontend/src/locales/en/home.json @@ -32,12 +32,12 @@ "introSlider": { "previous": { "label": "Previous slide", - "withPosition": "Go to previous slide ({{target}} of {{total}})", + "labelWithPosition": "Previous slide ({{current}} of {{total}})", "tooltip": "Previous slide" }, "next": { "label": "Next slide", - "withPosition": "Go to next slide ({{target}} of {{total}})", + "labelWithPosition": "Next slide ({{current}} of {{total}})", "tooltip": "Next slide" }, "beta": { @@ -59,4 +59,4 @@ "carouselLabel": "Introduction slideshow", "slidePosition": "Slide {{current}} of {{total}}" } -} \ No newline at end of file +} diff --git a/src/frontend/src/locales/fr/home.json b/src/frontend/src/locales/fr/home.json index de897476..73e8ccae 100644 --- a/src/frontend/src/locales/fr/home.json +++ b/src/frontend/src/locales/fr/home.json @@ -33,12 +33,12 @@ "carouselLabel": "Diaporama de présentation", "previous": { "label": "Diapositive précédente", - "withPosition": "Afficher la diapositive précédente ({{target}} sur {{total}})", + "labelWithPosition": "Diapositive précédente ({{current}} sur {{total}})", "tooltip": "Diapositive précédente" }, "next": { "label": "Diapositive suivante", - "withPosition": "Afficher la diapositive suivante ({{target}} sur {{total}})", + "labelWithPosition": "Diapositive suivante ({{current}} sur {{total}})", "tooltip": "Diapositive suivante" }, "slidePosition": "Diapositive {{current}} sur {{total}}", diff --git a/src/frontend/src/locales/nl/home.json b/src/frontend/src/locales/nl/home.json index ef5013c4..63199cd8 100644 --- a/src/frontend/src/locales/nl/home.json +++ b/src/frontend/src/locales/nl/home.json @@ -32,12 +32,12 @@ "introSlider": { "previous": { "label": "Vorige dia", - "withPosition": "Vorige dia tonen ({{target}} van {{total}})", + "labelWithPosition": "Vorige dia ({{current}} van {{total}})", "tooltip": "Vorige dia" }, "next": { "label": "Volgende dia", - "withPosition": "Volgende dia tonen ({{target}} van {{total}})", + "labelWithPosition": "Volgende dia ({{current}} van {{total}})", "tooltip": "Volgende dia" }, "beta": { @@ -59,4 +59,4 @@ "carouselLabel": "Introductie-diavoorstelling", "slidePosition": "Dia {{current}} van {{total}}" } -} \ No newline at end of file +} diff --git a/src/frontend/src/styles/index.css b/src/frontend/src/styles/index.css index 14da58f1..2aabeb8d 100644 --- a/src/frontend/src/styles/index.css +++ b/src/frontend/src/styles/index.css @@ -30,13 +30,6 @@ 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;