feat(a11y): focus return, typing announcement, shortcuts help (P3-4)
- Focus returns to the trigger when closing 4 genuine dialogs (room-topic viewer, reaction viewer, header topic, Search) — 20 inline popouts/menus correctly left as-is (returning focus to a hover target would be wrong). - Typing indicator announced via a visually-hidden role="status" region; the visual text is aria-hidden to avoid double announcement. - New keyboard-shortcuts help dialog (press ?, ignored while typing), mounted in ClientNonUIFeatures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -583,7 +583,6 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
returnFocusOnDeactivate: false,
|
||||
clickOutsideDeactivates: true,
|
||||
onDeactivate: () => setViewTopic(false),
|
||||
escapeDeactivates: stopPropagation,
|
||||
|
||||
@@ -25,3 +25,16 @@ export const RoomViewTyping = style([
|
||||
export const TypingText = style({
|
||||
flexGrow: 1,
|
||||
});
|
||||
|
||||
// Visually hidden but available to assistive technology.
|
||||
export const SrOnly = style({
|
||||
position: 'absolute',
|
||||
width: 1,
|
||||
height: 1,
|
||||
padding: 0,
|
||||
margin: -1,
|
||||
overflow: 'hidden',
|
||||
clip: 'rect(0, 0, 0, 0)',
|
||||
whiteSpace: 'nowrap',
|
||||
border: 0,
|
||||
});
|
||||
|
||||
@@ -37,6 +37,20 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
|
||||
return null;
|
||||
}
|
||||
|
||||
// A single, non-truncated string for assistive technology to announce.
|
||||
let typingAnnouncement = '';
|
||||
if (typingNames.length === 1) {
|
||||
typingAnnouncement = `${typingNames[0]} is typing`;
|
||||
} else if (typingNames.length === 2) {
|
||||
typingAnnouncement = `${typingNames[0]} and ${typingNames[1]} are typing`;
|
||||
} else if (typingNames.length === 3) {
|
||||
typingAnnouncement = `${typingNames[0]}, ${typingNames[1]} and ${typingNames[2]} are typing`;
|
||||
} else {
|
||||
typingAnnouncement = `${typingNames[0]}, ${typingNames[1]}, ${typingNames[2]} and ${
|
||||
typingNames.length - 3
|
||||
} others are typing`;
|
||||
}
|
||||
|
||||
const handleDropAll = () => {
|
||||
// some homeserver does not timeout typing status
|
||||
// we have given option so user can drop their typing status
|
||||
@@ -50,7 +64,10 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }} aria-live="polite" aria-atomic="false">
|
||||
<div style={{ position: 'relative' }}>
|
||||
<span className={css.SrOnly} role="status" aria-live="polite" aria-atomic="true">
|
||||
{typingAnnouncement}
|
||||
</span>
|
||||
<Box
|
||||
className={classNames(css.RoomViewTyping, className)}
|
||||
alignItems="Center"
|
||||
@@ -59,7 +76,7 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
|
||||
ref={ref}
|
||||
>
|
||||
<TypingIndicator />
|
||||
<Text className={css.TypingText} size="T300" truncate>
|
||||
<Text className={css.TypingText} size="T300" truncate aria-hidden>
|
||||
{typingNames.length === 1 && (
|
||||
<>
|
||||
<b>{typingNames[0]}</b>
|
||||
|
||||
@@ -106,7 +106,6 @@ export const Reactions = as<'div', ReactionsProps>(
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
returnFocusOnDeactivate: false,
|
||||
onDeactivate: () => setViewer(false),
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
|
||||
Reference in New Issue
Block a user