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 (
-
- {enabled ? (
- <>
-
- {text}
-
-
- {splitGraphemes(text).map((character, index) => {
- if (/^\s+$/.test(character)) return character
- return (
-
- {character}
-
- )
- })}
-
- >
- ) : (
- text
- )}
-
+ <>
+ {text}
+
+ {enabled ? (
+ <>
+
+ {text}
+
+
+ {splitGraphemes(text).map((character, index) => {
+ if (/^\s+$/.test(character)) return character
+ return (
+
+ {character}
+
+ )
+ })}
+
+ >
+ ) : (
+ 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 = {