From f5343647d0cfe888cfc212f9239ab7f5fd0629a0 Mon Sep 17 00:00:00 2001 From: UlisesMilani Date: Fri, 12 Jun 2026 00:58:53 -0300 Subject: [PATCH] accessibility: improve screen reader support for audio scrubber --- .../CompositionRecordingDraft.dom.tsx | 4 +++ .../conversation/MessageAudio.dom.tsx | 1 + .../conversation/WaveformScrubber.dom.tsx | 29 +++++++++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/ts/components/CompositionRecordingDraft.dom.tsx b/ts/components/CompositionRecordingDraft.dom.tsx index aa7495fad29..44f710ff746 100644 --- a/ts/components/CompositionRecordingDraft.dom.tsx +++ b/ts/components/CompositionRecordingDraft.dom.tsx @@ -95,6 +95,7 @@ export function CompositionRecordingDraft({ audioUrl={audioUrl} activeDuration={active?.duration} currentTime={active?.currentTime ?? 0} + isPlaying={active?.playing ?? false} width={state.width} onClick={onScrub} onScrub={onScrub} @@ -132,6 +133,7 @@ type SizedWaveformScrubberProps = { // defined if we are playing activeDuration: number | undefined; currentTime: number; + isPlaying: boolean; onScrub: (progressAsRatio: number) => void; onClick: (progressAsRatio: number) => void; }; @@ -140,6 +142,7 @@ function SizedWaveformScrubber({ audioUrl, activeDuration, currentTime, + isPlaying, onClick, onScrub, width, @@ -165,6 +168,7 @@ function SizedWaveformScrubber({ duration={duration} onClick={onClick} onScrub={onScrub} + isPlaying={isPlaying} /> ); } diff --git a/ts/components/conversation/MessageAudio.dom.tsx b/ts/components/conversation/MessageAudio.dom.tsx index cf83dab6f01..d243d23b39f 100644 --- a/ts/components/conversation/MessageAudio.dom.tsx +++ b/ts/components/conversation/MessageAudio.dom.tsx @@ -281,6 +281,7 @@ export function MessageAudio(props: Props): JSX.Element { barMaxHeight={BAR_MAX_HEIGHT} onClick={handleWaveformClick} onScrub={handleWaveformScrub} + isPlaying={isPlaying} /> ); diff --git a/ts/components/conversation/WaveformScrubber.dom.tsx b/ts/components/conversation/WaveformScrubber.dom.tsx index 918843190d7..c61adead1cc 100644 --- a/ts/components/conversation/WaveformScrubber.dom.tsx +++ b/ts/components/conversation/WaveformScrubber.dom.tsx @@ -5,6 +5,8 @@ import { useCallback, useRef, forwardRef, + useState, + useEffect, type JSX, type MouseEvent, type KeyboardEvent, @@ -25,6 +27,7 @@ type Props = Readonly<{ barMaxHeight: number; onClick: (positionAsRatio: number) => void; onScrub: (positionAsRatio: number) => void; + isPlaying?: boolean; }>; const BAR_COUNT = 47; @@ -45,6 +48,7 @@ export const WaveformScrubber = forwardRef(function WaveformScrubber( duration, onClick, onScrub, + isPlaying = false, }: Props, ref ): JSX.Element { @@ -52,6 +56,18 @@ export const WaveformScrubber = forwardRef(function WaveformScrubber( const waveformRef = useRef(null); + const [announcedTime, setAnnouncedTime] = useState(currentTime); + + useEffect(() => { + if (!isPlaying) { + setAnnouncedTime(currentTime); + } + }, [currentTime, isPlaying]); + + const handleFocus = useCallback(() => { + setAnnouncedTime(currentTime); + }, [currentTime]); + // Clicking waveform moves playback head position and starts playback. const handleClick = useCallback( (event: MouseEvent) => { @@ -69,9 +85,12 @@ export const WaveformScrubber = forwardRef(function WaveformScrubber( progress = 0; } + if (duration) { + setAnnouncedTime(progress * duration); + } onClick(progress); }, - [waveformRef, onClick] + [waveformRef, onClick, duration] ); // Keyboard navigation for waveform. Pressing keys moves playback head @@ -103,6 +122,9 @@ export const WaveformScrubber = forwardRef(function WaveformScrubber( const positionIncrement = increment / duration; const newPosition = currentPosition + positionIncrement; + const targetPosition = Math.min(Math.max(0, newPosition), 1) * duration; + setAnnouncedTime(targetPosition); + onScrub(newPosition); }; @@ -112,14 +134,15 @@ export const WaveformScrubber = forwardRef(function WaveformScrubber( className="WaveformScrubber" onClick={handleClick} onKeyDown={handleKeyDown} + onFocus={handleFocus} tabIndex={0} role="slider" aria-label={i18n('icu:MessageAudio--slider')} aria-orientation="horizontal" - aria-valuenow={currentTime} + aria-valuenow={announcedTime} aria-valuemin={0} aria-valuemax={duration} - aria-valuetext={durationToPlaybackText(currentTime)} + aria-valuetext={durationToPlaybackText(announcedTime)} >