From 6cf18c3bd97cfac731d455b768d5a37c0c4b16eb Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 7 Jul 2026 22:37:22 -0400 Subject: [PATCH] fix(a11y): correct call-control aria + picker/recorder/live-chip UX (DP7-DP12) DP7: fix inverted Deafen/Undeafen aria-label on the sound button so it matches the action and tooltip. DP8: add aria-pressed to Sound/Video/ScreenShare toggles and align the Microphone toggle to the same "pressed = feature on/active" semantic. DP9: return focus to the trigger when the GifPicker closes and cap its fixed width to min(312px, calc(100vw - 16px)) so it can't overflow narrow viewports. DP10: drop redundant mouse-only clear onClick nested inside the search filter Chip buttons (parent chip/menu already performs the clear); the cross icons are now purely decorative. DP11: constrain the voice recorder widget and let the waveform shrink so it fits a narrow composer, and expose the live duration as a role="timer" snapshot instead of spamming a screen reader every 100ms. DP12: announce the live-call participant count via a visually-hidden role="status" region. Co-Authored-By: Claude Opus 4.8 --- src/app/components/GifPicker.tsx | 9 +- src/app/components/VoiceMessageRecorder.tsx | 6 +- src/app/features/call-status/CallControl.tsx | 7 +- src/app/features/call-status/LiveChip.tsx | 173 +++++++++--------- src/app/features/call-status/styles.css.ts | 12 ++ .../features/message-search/SearchFilters.tsx | 52 +----- 6 files changed, 120 insertions(+), 139 deletions(-) diff --git a/src/app/components/GifPicker.tsx b/src/app/components/GifPicker.tsx index 199ec0468..91a7617f9 100644 --- a/src/app/components/GifPicker.tsx +++ b/src/app/components/GifPicker.tsx @@ -7,6 +7,7 @@ import { useSetting } from '../state/hooks/settings'; import { settingsAtom } from '../state/settings'; const PICKER_WIDTH = 312; +const PICKER_WIDTH_CSS = `min(${PICKER_WIDTH}px, calc(100vw - 16px))`; type GifPickerInnerProps = { onSelect: (url: string, width: number, height: number) => void; @@ -31,7 +32,7 @@ function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInne ); return ( - + {lotusTerminal && (
{waveformBars.map((h, i) => (
@@ -67,7 +67,8 @@ function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) { radii="300" size="300" onClick={() => onToggle()} - aria-label={enabled ? 'Undeafen' : 'Deafen'} + aria-label={enabled ? 'Deafen' : 'Undeafen'} + aria-pressed={enabled} outlined disabled={disabled} > @@ -109,6 +110,7 @@ function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) { size="300" onClick={toggleVideo} aria-label={enabled ? 'Stop Video' : 'Start Video'} + aria-pressed={enabled} outlined disabled={disabled || loading} > @@ -147,6 +149,7 @@ function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonPro size="300" onClick={onToggle} aria-label={enabled ? 'Stop Screenshare' : 'Start Screenshare'} + aria-pressed={enabled} outlined disabled={disabled} > diff --git a/src/app/features/call-status/LiveChip.tsx b/src/app/features/call-status/LiveChip.tsx index 0ee1eb4c1..a0a4a76e8 100644 --- a/src/app/features/call-status/LiveChip.tsx +++ b/src/app/features/call-status/LiveChip.tsx @@ -45,93 +45,98 @@ export function LiveChip({ count, room, members }: LiveChipProps) { }; return ( - setCords(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', - isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', - escapeDeactivates: stopPropagation, - }} - > - + + {`${count} in call`} + + setCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', + isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, }} > - - - - {members.map((callMember) => { - const userId = callMember.sender; - if (!userId) return null; - const name = - getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId; - const avatarMxc = getMemberAvatarMxc(room, userId); - const avatarUrl = avatarMxc - ? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined) - : undefined; + + + + + {members.map((callMember) => { + const userId = callMember.sender; + if (!userId) return null; + const name = + getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId; + const avatarMxc = getMemberAvatarMxc(room, userId); + const avatarUrl = avatarMxc + ? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96) ?? undefined) + : undefined; - return ( - - openUserProfile( - room.roomId, - undefined, - userId, - getMouseEventCords(evt.nativeEvent), - 'Right', - ) - } - before={ - - } - /> - - } - > - - {name} - - - ); - })} - - - - - - } - > - } - after={} - radii="Pill" - onClick={handleOpenMenu} + return ( + + openUserProfile( + room.roomId, + undefined, + userId, + getMouseEventCords(evt.nativeEvent), + 'Right', + ) + } + before={ + + } + /> + + } + > + + {name} + + + ); + })} + + + + + + } > - - {count} Live - - - + } + after={} + radii="Pill" + onClick={handleOpenMenu} + > + + {count} Live + + + + ); } diff --git a/src/app/features/call-status/styles.css.ts b/src/app/features/call-status/styles.css.ts index 88e0b71e5..e834ed9e9 100644 --- a/src/app/features/call-status/styles.css.ts +++ b/src/app/features/call-status/styles.css.ts @@ -5,6 +5,18 @@ export const LiveChipText = style({ color: color.Critical.Main, }); +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, +}); + export const CallStatus = style([ { padding: `${toRem(6)} ${config.space.S200}`, diff --git a/src/app/features/message-search/SearchFilters.tsx b/src/app/features/message-search/SearchFilters.tsx index ca5ffbb53..8a48399c7 100644 --- a/src/app/features/message-search/SearchFilters.tsx +++ b/src/app/features/message-search/SearchFilters.tsx @@ -643,18 +643,7 @@ function DateRangeButton({ fromTs, toTs, onChange }: DateRangeButtonProps) { variant={hasRange ? 'Primary' : 'SurfaceVariant'} radii="Pill" before={} - after={ - hasRange ? ( - { - e.stopPropagation(); - onChange(undefined, undefined); - }} - /> - ) : undefined - } + after={hasRange ? : undefined} onClick={(e: React.MouseEvent) => setMenuAnchor(e.currentTarget.getBoundingClientRect()) } @@ -795,18 +784,7 @@ export function SearchFilters({ radii="Pill" aria-pressed={!!containsUrl} before={} - after={ - containsUrl ? ( - { - e.stopPropagation(); - onContainsUrlChange(undefined); - }} - /> - ) : undefined - } + after={containsUrl ? : undefined} onClick={() => onContainsUrlChange(containsUrl ? undefined : true)} > Has link @@ -821,18 +799,7 @@ export function SearchFilters({ radii="Pill" aria-pressed={active} before={} - after={ - active ? ( - { - e.stopPropagation(); - onToggleMsgTypeFilter(msgType); - }} - /> - ) : undefined - } + after={active ? : undefined} onClick={() => onToggleMsgTypeFilter(msgType)} > {label} @@ -845,18 +812,7 @@ export function SearchFilters({ radii="Pill" aria-pressed={pinnedOnly} before={} - after={ - pinnedOnly ? ( - { - e.stopPropagation(); - onTogglePinnedOnly(); - }} - /> - ) : undefined - } + after={pinnedOnly ? : undefined} onClick={onTogglePinnedOnly} > Pinned