From 4d7a05c0f11282c61a521b2ca729046c9c630225 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 2 Jul 2026 11:57:32 -0400 Subject: [PATCH] fix(a11y): review-wave fixes (P3-4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `?` shortcut now stopImmediatePropagation so RoomView's type-to-focus handler doesn't steal focus into the composer behind the dialog (and swallow Escape) — CONFIRMED review finding. - Typing live region stays mounted (empty when idle) so the FIRST "X is typing" is reliably announced (a status region added with its text isn't always read). - Removed a stray empty `{}` JSX expression in MediaGallery (leftover from an auto-fix). Reviewer verified the rest: collapsed-message labels, focus-return classification (4 dialogs fixed, popouts correctly left), and all aria fixes. Co-Authored-By: Claude Opus 4.8 --- src/app/features/room/MediaGallery.tsx | 1 - src/app/features/room/RoomViewTyping.tsx | 160 +++++++++--------- .../shortcuts/KeyboardShortcutsDialog.tsx | 4 + 3 files changed, 85 insertions(+), 80 deletions(-) diff --git a/src/app/features/room/MediaGallery.tsx b/src/app/features/room/MediaGallery.tsx index 44138016e..ff213cc7e 100644 --- a/src/app/features/room/MediaGallery.tsx +++ b/src/app/features/room/MediaGallery.tsx @@ -261,7 +261,6 @@ function Lightbox({ escapeDeactivates: false, }} > - {}
( [typingMembers, myUserId, room], ); - if (typingNames.length === 0) { - return null; - } - // A single, non-truncated string for assistive technology to announce. + // Computed even when empty so the live region can stay mounted (below) — + // a `role="status"` region added to the DOM together with its first text + // is not reliably announced by some screen readers. let typingAnnouncement = ''; if (typingNames.length === 1) { typingAnnouncement = `${typingNames[0]} is typing`; @@ -65,85 +64,88 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>( return (
+ {/* Persistently mounted so the FIRST "X is typing" is announced. */} {typingAnnouncement} - - - - {typingNames.length === 1 && ( - <> - {typingNames[0]} - - {' is typing...'} - - - )} - {typingNames.length === 2 && ( - <> - {typingNames[0]} - - {' and '} - - {typingNames[1]} - - {' are typing...'} - - - )} - {typingNames.length === 3 && ( - <> - {typingNames[0]} - - {', '} - - {typingNames[1]} - - {' and '} - - {typingNames[2]} - - {' are typing...'} - - - )} - {typingNames.length > 3 && ( - <> - {typingNames[0]} - - {', '} - - {typingNames[1]} - - {', '} - - {typingNames[2]} - - {' and '} - - {typingNames.length - 3} others - - {' are typing...'} - - - )} - - 0 && ( + - - - + + + {typingNames.length === 1 && ( + <> + {typingNames[0]} + + {' is typing...'} + + + )} + {typingNames.length === 2 && ( + <> + {typingNames[0]} + + {' and '} + + {typingNames[1]} + + {' are typing...'} + + + )} + {typingNames.length === 3 && ( + <> + {typingNames[0]} + + {', '} + + {typingNames[1]} + + {' and '} + + {typingNames[2]} + + {' are typing...'} + + + )} + {typingNames.length > 3 && ( + <> + {typingNames[0]} + + {', '} + + {typingNames[1]} + + {', '} + + {typingNames[2]} + + {' and '} + + {typingNames.length - 3} others + + {' are typing...'} + + + )} + + + + + + )}
); }, diff --git a/src/app/features/shortcuts/KeyboardShortcutsDialog.tsx b/src/app/features/shortcuts/KeyboardShortcutsDialog.tsx index e89f2e9b9..c10c454f4 100644 --- a/src/app/features/shortcuts/KeyboardShortcutsDialog.tsx +++ b/src/app/features/shortcuts/KeyboardShortcutsDialog.tsx @@ -55,6 +55,10 @@ export function useKeyboardShortcutsTrigger() { // `?` is produced by Shift + `/` on the common layouts. if (evt.key === '?') { evt.preventDefault(); + // Stop RoomView's window-level "type any char → focus composer" + // handler from also firing — otherwise focus lands in the composer + // behind the dialog and Escape gets swallowed by the contenteditable. + evt.stopImmediatePropagation(); setOpen(true); } },