fix(a11y): descriptive aria-label on reaction buttons (P3-4)

Reaction.tsx now computes aria-label='{shortcode} reaction, N people'
using getShortcodeFor so screen readers announce emoji name and count
instead of an ambiguous button. Custom (mxc://) emoji falls back to
'custom emoji reaction'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 18:50:19 -04:00
parent f50e14d7a5
commit cb3d2c40e5
2 changed files with 37 additions and 29 deletions
+1 -1
View File
@@ -184,7 +184,7 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
| Performance | Numerous event handlers (e.g., handleUserClick, handleReplyClick) lack `useCallback`, leading to unnecessary re-renders of message components. | `cinny/src/app/features/room/RoomTimeline.tsx` | OPEN | | Performance | Numerous event handlers (e.g., handleUserClick, handleReplyClick) lack `useCallback`, leading to unnecessary re-renders of message components. | `cinny/src/app/features/room/RoomTimeline.tsx` | OPEN |
| Performance | The `submit` function and file handling callbacks (e.g., handleSendUpload) are re-created on every render, causing re-renders of the editor and toolbar components. | `cinny/src/app/features/room/RoomInput.tsx` | OPEN | | Performance | The `submit` function and file handling callbacks (e.g., handleSendUpload) are re-created on every render, causing re-renders of the editor and toolbar components. | `cinny/src/app/features/room/RoomInput.tsx` | OPEN |
| Accessibility | `button` for edit history lacks `aria-label` | `cinny/src/app/components/message/content/FallbackContent.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="View edit history"` | | Accessibility | `button` for edit history lacks `aria-label` | `cinny/src/app/components/message/content/FallbackContent.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="View edit history"` |
| Accessibility | `button` for reaction lacks `aria-label` | `cinny/src/app/components/message/Reaction.tsx` | OPEN — emoji content is already screen-reader-accessible via alt text; parent caller would need to set aria-label per reaction | | Accessibility | `button` for reaction lacks `aria-label` | `cinny/src/app/components/message/Reaction.tsx` | **FIXED ⚠️ UNTESTED**`Reaction` component now computes `aria-label="{shortcode} reaction, N people"` internally using `getShortcodeFor`; custom (mxc://) emoji falls back to "custom emoji reaction". |
| Accessibility | `button` for ThreadIndicator lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="View thread"` | | Accessibility | `button` for ThreadIndicator lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="View thread"` |
| Accessibility | `button` for ReplyLayout lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="Jump to original message"` | | Accessibility | `button` for ReplyLayout lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="Jump to original message"` |
+36 -28
View File
@@ -15,34 +15,42 @@ export const Reaction = as<
reaction: string; reaction: string;
useAuthentication?: boolean; useAuthentication?: boolean;
} }
>(({ className, mx, count, reaction, useAuthentication, ...props }, ref) => ( >(({ className, mx, count, reaction, useAuthentication, ...props }, ref) => {
<Box const shortcode = reaction.startsWith('mxc://')
as="button" ? 'custom emoji'
className={classNames(css.Reaction, className)} : (getShortcodeFor(getHexcodeForEmoji(reaction)) ?? reaction);
alignItems="Center" const label = `${shortcode} reaction, ${count} ${count === 1 ? 'person' : 'people'}`;
shrink="No"
gap="200" return (
{...props} <Box
ref={ref} as="button"
> className={classNames(css.Reaction, className)}
<Text className={css.ReactionText} as="span" size="T400"> alignItems="Center"
{reaction.startsWith('mxc://') ? ( shrink="No"
<img gap="200"
className={css.ReactionImg} aria-label={label}
src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction} {...props}
alt={reaction} ref={ref}
/> >
) : ( <Text className={css.ReactionText} as="span" size="T400">
<Text as="span" size="Inherit" truncate> {reaction.startsWith('mxc://') ? (
{reaction} <img
</Text> className={css.ReactionImg}
)} src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction}
</Text> alt={reaction}
<Text as="span" size="T300"> />
{count} ) : (
</Text> <Text as="span" size="Inherit" truncate>
</Box> {reaction}
)); </Text>
)}
</Text>
<Text as="span" size="T300">
{count}
</Text>
</Box>
);
});
type ReactionTooltipMsgProps = { type ReactionTooltipMsgProps = {
room: Room; room: Room;