diff --git a/ui/src/audioplayer/LyricsLineRows.jsx b/ui/src/audioplayer/LyricsLineRows.jsx index 27894020b..3e7bc79d5 100644 --- a/ui/src/audioplayer/LyricsLineRows.jsx +++ b/ui/src/audioplayer/LyricsLineRows.jsx @@ -51,6 +51,20 @@ const waveTextRootStyle = { const waveTextMeasureStyle = { visibility: 'hidden' } +// Keeps timed token text readable by screen readers, since the painted +// wave copy is aria-hidden and aria-label is ignored on role-less spans. +const visuallyHiddenStyle = { + border: 0, + clip: 'rect(0 0 0 0)', + height: 1, + margin: -1, + overflow: 'hidden', + padding: 0, + position: 'absolute', + whiteSpace: 'nowrap', + width: 1, +} + const waveTextOverlayStyle = { display: 'block', inset: 0, @@ -62,36 +76,39 @@ const renderWaveText = (text, eligible, enabled, emphasized, className) => { if (!eligible) return text return ( - + <> + {text} + + ) } @@ -270,7 +287,6 @@ export const KaraokeLineRow = memo( : undefined } style={tokenData.style} - aria-label={segment.text} > {renderWaveText( segment.text, @@ -516,7 +532,6 @@ export const KaraokeStackedLineRow = memo( : undefined } style={mainTokenData.style} - aria-label={segment.text} > {renderWaveText( segment.text, @@ -579,9 +594,6 @@ export const KaraokeStackedLineRow = memo( data-testid="lyrics-pronunciation-token" data-lyrics-state="future" data-timed={pronunciationWindow ? 'true' : 'false'} - aria-label={ - pronunciationWindow ? segment.pronunciation : undefined - } ref={ pronunciationWindow ? getTokenRef({ diff --git a/ui/src/audioplayer/LyricsPanel.test.jsx b/ui/src/audioplayer/LyricsPanel.test.jsx index bbc632178..bb4877396 100644 --- a/ui/src/audioplayer/LyricsPanel.test.jsx +++ b/ui/src/audioplayer/LyricsPanel.test.jsx @@ -254,6 +254,29 @@ describe('', () => { }) }) + it('exposes timed token text outside the aria-hidden wave copy', () => { + renderPanel({ + mainLyric: tokenizedMainLyric, + pronunciationLyric: tokenizedPronunciationLyric, + showPronunciation: true, + }) + + const tokens = [ + ...screen.getAllByTestId('lyrics-token'), + ...screen.getAllByTestId('lyrics-pronunciation-token'), + ] + expect(tokens).toHaveLength(4) + tokens.forEach((token) => { + expect(token).not.toHaveAttribute('aria-label') + const readable = Array.from(token.childNodes) + .filter((node) => node.getAttribute?.('aria-hidden') !== 'true') + .map((node) => node.textContent) + .join('') + expect(readable).not.toBe('') + expect(token.textContent).toContain(readable) + }) + }) + it('keeps token ref callbacks stable across equivalent rerenders', () => { const registerToken = vi.fn() const line = {