From 6420b59483193344379f9dcdbaa3619322f399d2 Mon Sep 17 00:00:00 2001 From: David Sexton Date: Sat, 21 Feb 2026 21:41:08 -0800 Subject: [PATCH 1/2] fix(a11y): prevent double screen reader announcements in output log Remove role="log" which has implicit aria-live="polite" per WAI-ARIA spec. Messages are announced via explicit announce() calls, so the implicit live region caused duplicates. --- src/components/output.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/output.tsx b/src/components/output.tsx index 09fc773..ae909ac 100644 --- a/src/components/output.tsx +++ b/src/components/output.tsx @@ -700,10 +700,7 @@ scrollToBottom = () => { const output = this.outputRef.current; if (output) { onFocus={this.handleOutputFocus} onBlur={this.handleOutputBlur} tabIndex={0} - role="log" aria-label="Game output log - use arrow keys to navigate" - aria-live="polite" - aria-atomic="false" > {visibleOutput.map((line, index) => (
Date: Sat, 21 Feb 2026 21:45:51 -0800 Subject: [PATCH 2/2] fix(a11y): prevent double announce on arrow key navigation Move announceOutputLine() call from inside setState updater to the callback. React StrictMode double-invokes updater functions to detect impure reducers, causing duplicate announcements. --- src/components/output.tsx | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/output.tsx b/src/components/output.tsx index ae909ac..628cef3 100644 --- a/src/components/output.tsx +++ b/src/components/output.tsx @@ -598,25 +598,27 @@ scrollToBottom = () => { const output = this.outputRef.current; if (output) { if (visibleOutput.length === 0) return; switch (e.key) { - case 'ArrowDown': + case 'ArrowDown': { e.preventDefault(); - this.setState(prevState => { - const currentIndex = prevState.focusedLineIndex ?? -1; - const nextIndex = Math.min(currentIndex + 1, visibleOutput.length - 1); + const currentIndex = this.state.focusedLineIndex ?? -1; + const nextIndex = Math.min(currentIndex + 1, visibleOutput.length - 1); + this.setState({ focusedLineIndex: nextIndex }, () => { this.announceOutputLine(visibleOutput[nextIndex]); - return { focusedLineIndex: nextIndex }; - }, this.scrollFocusedLineIntoView); + this.scrollFocusedLineIntoView(); + }); break; + } - case 'ArrowUp': + case 'ArrowUp': { e.preventDefault(); - this.setState(prevState => { - const currentIndex = prevState.focusedLineIndex ?? visibleOutput.length; - const prevIndex = Math.max(currentIndex - 1, 0); + const currentIndex = this.state.focusedLineIndex ?? visibleOutput.length; + const prevIndex = Math.max(currentIndex - 1, 0); + this.setState({ focusedLineIndex: prevIndex }, () => { this.announceOutputLine(visibleOutput[prevIndex]); - return { focusedLineIndex: prevIndex }; - }, this.scrollFocusedLineIntoView); + this.scrollFocusedLineIntoView(); + }); break; + } case 'Home': e.preventDefault();