fix(a11y): review-wave fixes (P3-4)
- `?` 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 <noreply@anthropic.com>
This commit is contained in:
@@ -261,7 +261,6 @@ function Lightbox({
|
|||||||
escapeDeactivates: false,
|
escapeDeactivates: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{}
|
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal
|
aria-modal
|
||||||
|
|||||||
@@ -33,11 +33,10 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
|
|||||||
[typingMembers, myUserId, room],
|
[typingMembers, myUserId, room],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (typingNames.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A single, non-truncated string for assistive technology to announce.
|
// 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 = '';
|
let typingAnnouncement = '';
|
||||||
if (typingNames.length === 1) {
|
if (typingNames.length === 1) {
|
||||||
typingAnnouncement = `${typingNames[0]} is typing`;
|
typingAnnouncement = `${typingNames[0]} is typing`;
|
||||||
@@ -65,85 +64,88 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
|
{/* Persistently mounted so the FIRST "X is typing" is announced. */}
|
||||||
<span className={css.SrOnly} role="status" aria-live="polite" aria-atomic="true">
|
<span className={css.SrOnly} role="status" aria-live="polite" aria-atomic="true">
|
||||||
{typingAnnouncement}
|
{typingAnnouncement}
|
||||||
</span>
|
</span>
|
||||||
<Box
|
{typingNames.length > 0 && (
|
||||||
className={classNames(css.RoomViewTyping, className)}
|
<Box
|
||||||
alignItems="Center"
|
className={classNames(css.RoomViewTyping, className)}
|
||||||
gap="400"
|
alignItems="Center"
|
||||||
{...props}
|
gap="400"
|
||||||
ref={ref}
|
{...props}
|
||||||
>
|
ref={ref}
|
||||||
<TypingIndicator />
|
|
||||||
<Text className={css.TypingText} size="T300" truncate aria-hidden>
|
|
||||||
{typingNames.length === 1 && (
|
|
||||||
<>
|
|
||||||
<b>{typingNames[0]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{' is typing...'}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{typingNames.length === 2 && (
|
|
||||||
<>
|
|
||||||
<b>{typingNames[0]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{' and '}
|
|
||||||
</Text>
|
|
||||||
<b>{typingNames[1]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{' are typing...'}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{typingNames.length === 3 && (
|
|
||||||
<>
|
|
||||||
<b>{typingNames[0]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{', '}
|
|
||||||
</Text>
|
|
||||||
<b>{typingNames[1]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{' and '}
|
|
||||||
</Text>
|
|
||||||
<b>{typingNames[2]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{' are typing...'}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{typingNames.length > 3 && (
|
|
||||||
<>
|
|
||||||
<b>{typingNames[0]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{', '}
|
|
||||||
</Text>
|
|
||||||
<b>{typingNames[1]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{', '}
|
|
||||||
</Text>
|
|
||||||
<b>{typingNames[2]}</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{' and '}
|
|
||||||
</Text>
|
|
||||||
<b>{typingNames.length - 3} others</b>
|
|
||||||
<Text as="span" size="Inherit" priority="300">
|
|
||||||
{' are typing...'}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<IconButton
|
|
||||||
title="Drop Typing Status"
|
|
||||||
aria-label="Drop typing status"
|
|
||||||
size="300"
|
|
||||||
radii="Pill"
|
|
||||||
onClick={handleDropAll}
|
|
||||||
>
|
>
|
||||||
<Icon size="50" src={Icons.Cross} />
|
<TypingIndicator />
|
||||||
</IconButton>
|
<Text className={css.TypingText} size="T300" truncate aria-hidden>
|
||||||
</Box>
|
{typingNames.length === 1 && (
|
||||||
|
<>
|
||||||
|
<b>{typingNames[0]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' is typing...'}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{typingNames.length === 2 && (
|
||||||
|
<>
|
||||||
|
<b>{typingNames[0]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' and '}
|
||||||
|
</Text>
|
||||||
|
<b>{typingNames[1]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' are typing...'}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{typingNames.length === 3 && (
|
||||||
|
<>
|
||||||
|
<b>{typingNames[0]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{', '}
|
||||||
|
</Text>
|
||||||
|
<b>{typingNames[1]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' and '}
|
||||||
|
</Text>
|
||||||
|
<b>{typingNames[2]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' are typing...'}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{typingNames.length > 3 && (
|
||||||
|
<>
|
||||||
|
<b>{typingNames[0]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{', '}
|
||||||
|
</Text>
|
||||||
|
<b>{typingNames[1]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{', '}
|
||||||
|
</Text>
|
||||||
|
<b>{typingNames[2]}</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' and '}
|
||||||
|
</Text>
|
||||||
|
<b>{typingNames.length - 3} others</b>
|
||||||
|
<Text as="span" size="Inherit" priority="300">
|
||||||
|
{' are typing...'}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<IconButton
|
||||||
|
title="Drop Typing Status"
|
||||||
|
aria-label="Drop typing status"
|
||||||
|
size="300"
|
||||||
|
radii="Pill"
|
||||||
|
onClick={handleDropAll}
|
||||||
|
>
|
||||||
|
<Icon size="50" src={Icons.Cross} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ export function useKeyboardShortcutsTrigger() {
|
|||||||
// `?` is produced by Shift + `/` on the common layouts.
|
// `?` is produced by Shift + `/` on the common layouts.
|
||||||
if (evt.key === '?') {
|
if (evt.key === '?') {
|
||||||
evt.preventDefault();
|
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);
|
setOpen(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user