fix(a11y): thread chip says it opens the thread, and whether it's unread/muted (#179)
CI / Build & Quality Checks (push) Successful in 2m15s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 14s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
CI / Build & Quality Checks (push) Successful in 2m15s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 14s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
The "N replies · <time>" chip under a thread root was named only by its visible text, so a screen reader never said it opens the thread, and the unread dot / muted bell were visual-only. Its accessible name is now the visible text first (WCAG 2.5.3) plus "view thread", "unread replies" and "muted" as applicable, e.g. "1 reply · 12:16 PM, view thread". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d5aa18e3ab
commit
206a3e933a
@@ -19,6 +19,18 @@ export function ThreadSummary({ rootEvent, room, onOpen }: ThreadSummaryProps) {
|
|||||||
|
|
||||||
const { count, latestTs } = summary;
|
const { count, latestTs } = summary;
|
||||||
const latestStr = latestTs !== undefined ? format(latestTs) : undefined;
|
const latestStr = latestTs !== undefined ? format(latestTs) : undefined;
|
||||||
|
const visibleText = `${count === 1 ? '1 reply' : `${count} replies`}${latestStr ? ` · ${latestStr}` : ''}`;
|
||||||
|
// #179: the chip was named only by its visible text, so a screen reader never
|
||||||
|
// heard that it opens the thread, nor about the unread dot / muted bell. Keep
|
||||||
|
// the visible text first (WCAG 2.5.3, label in name) and add the rest.
|
||||||
|
const ariaLabel = [
|
||||||
|
visibleText,
|
||||||
|
'view thread',
|
||||||
|
unread > 0 ? 'unread replies' : undefined,
|
||||||
|
mode === ThreadNotificationMode.Mute ? 'muted' : undefined,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={{ marginTop: config.space.S200 }}>
|
<Box style={{ marginTop: config.space.S200 }}>
|
||||||
@@ -26,6 +38,7 @@ export function ThreadSummary({ rootEvent, room, onOpen }: ThreadSummaryProps) {
|
|||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
radii="300"
|
radii="300"
|
||||||
className={MobileTouchTarget}
|
className={MobileTouchTarget}
|
||||||
|
aria-label={ariaLabel}
|
||||||
before={<Icon size="50" src={Icons.Thread} />}
|
before={<Icon size="50" src={Icons.Thread} />}
|
||||||
after={
|
after={
|
||||||
unread > 0 ? <Badge variant="Success" fill="Solid" radii="Pill" size="200" /> : undefined
|
unread > 0 ? <Badge variant="Success" fill="Solid" radii="Pill" size="200" /> : undefined
|
||||||
@@ -35,10 +48,7 @@ export function ThreadSummary({ rootEvent, room, onOpen }: ThreadSummaryProps) {
|
|||||||
if (threadId) onOpen(threadId);
|
if (threadId) onOpen(threadId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="T200">
|
<Text size="T200">{visibleText}</Text>
|
||||||
{count === 1 ? '1 reply' : `${count} replies`}
|
|
||||||
{latestStr ? ` · ${latestStr}` : ''}
|
|
||||||
</Text>
|
|
||||||
{mode === ThreadNotificationMode.Mute && <Icon size="50" src={Icons.BellMute} />}
|
{mode === ThreadNotificationMode.Mute && <Icon size="50" src={Icons.BellMute} />}
|
||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user