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
57 lines
2.1 KiB
TypeScript
57 lines
2.1 KiB
TypeScript
import React from 'react';
|
|
import { Badge, Box, Chip, Icon, Icons, Text, config } from 'folds';
|
|
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
|
import { MobileTouchTarget } from '../../../styles/mobile.css';
|
|
import { useThreadSummary } from '../../../hooks/useThreadSummary';
|
|
import { useTimestampFormatter } from '../../../hooks/useTimestampFormatter';
|
|
import { ThreadNotificationMode } from '../../../utils/threadNotifications';
|
|
|
|
type ThreadSummaryProps = {
|
|
rootEvent: MatrixEvent;
|
|
room: Room;
|
|
onOpen: (threadId: string) => void;
|
|
};
|
|
export function ThreadSummary({ rootEvent, room, onOpen }: ThreadSummaryProps) {
|
|
const { summary, unread, mode } = useThreadSummary(rootEvent, room);
|
|
const { format } = useTimestampFormatter();
|
|
|
|
if (!summary || summary.count === 0) return null;
|
|
|
|
const { count, latestTs } = summary;
|
|
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 (
|
|
<Box style={{ marginTop: config.space.S200 }}>
|
|
<Chip
|
|
variant="SurfaceVariant"
|
|
radii="300"
|
|
className={MobileTouchTarget}
|
|
aria-label={ariaLabel}
|
|
before={<Icon size="50" src={Icons.Thread} />}
|
|
after={
|
|
unread > 0 ? <Badge variant="Success" fill="Solid" radii="Pill" size="200" /> : undefined
|
|
}
|
|
onClick={() => {
|
|
const threadId = rootEvent.getId();
|
|
if (threadId) onOpen(threadId);
|
|
}}
|
|
>
|
|
<Text size="T200">{visibleText}</Text>
|
|
{mode === ThreadNotificationMode.Mute && <Icon size="50" src={Icons.BellMute} />}
|
|
</Chip>
|
|
</Box>
|
|
);
|
|
}
|