fix(a11y): stable reaction labels — emoji glyph, custom emoji shortcode (#179)
CI / Secret scan (gitleaks) (push) Successful in 52s
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s

The reaction button's aria-label used getShortcodeFor(), which returns
undefined until the lazily loaded emoji data arrives. The same button read
"🎉 reaction, 1 person" on first render and "tada reaction, 2 people" after
any later re-render. It now always uses the emoji itself (screen readers
speak it by its proper name, e.g. "party popper").

Custom (mxc) emoji were labelled just "custom emoji"; they now use the
shortcode carried on the reaction event (":lotus_blob: reaction").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-26 11:34:03 -04:00
co-authored by Claude Opus 5.5
parent 52d94058cf
commit 1b5e6a37f5
3 changed files with 16 additions and 6 deletions
+13 -5
View File
@@ -13,13 +13,21 @@ export const Reaction = as<
mx: MatrixClient;
count: number;
reaction: string;
/** Custom-emoji shortcode from the reaction event, used for its label. */
shortcode?: string;
useAuthentication?: boolean;
}
>(({ className, mx, count, reaction, useAuthentication, ...props }, ref) => {
const shortcode = reaction.startsWith('mxc://')
? 'custom emoji'
: (getShortcodeFor(getHexcodeForEmoji(reaction)) ?? reaction);
const label = `${shortcode} reaction, ${count} ${count === 1 ? 'person' : 'people'}`;
>(({ className, mx, count, reaction, shortcode, useAuthentication, ...props }, ref) => {
// Name a unicode reaction by the emoji itself: screen readers speak it by its
// proper name ("party popper"). Don't use getShortcodeFor here — it returns
// undefined until the lazy emoji data loads, so the same button would flip
// between "🎉 reaction" and "tada reaction" depending on timing (#179).
const name = reaction.startsWith('mxc://')
? shortcode
? `:${shortcode}:`
: 'custom emoji'
: reaction;
const label = `${name} reaction, ${count} ${count === 1 ? 'person' : 'people'}`;
return (
<Box
+2 -1
View File
@@ -23,7 +23,7 @@ import { Room } from 'matrix-js-sdk';
import { type Relations } from 'matrix-js-sdk/lib/models/relations';
import FocusTrap from 'focus-trap-react';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { factoryEventSentBy } from '../../../utils/matrix';
import { eventWithShortcode, factoryEventSentBy } from '../../../utils/matrix';
import { Reaction, ReactionTooltipMsg } from '../../../components/message';
import { useRelations } from '../../../hooks/useRelations';
import * as css from './styles.css';
@@ -138,6 +138,7 @@ export const Reactions = as<'div', ReactionsProps>(
key={key}
mx={mx}
reaction={key}
shortcode={rEvents.find(eventWithShortcode)?.getContent().shortcode}
count={events.size}
onClick={canSendReaction ? () => onReactionToggle(mEventId, key) : undefined}
onContextMenu={handleViewReaction}
@@ -107,6 +107,7 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
key={key}
mx={mx}
reaction={key}
shortcode={Array.from(evts).find(eventWithShortcode)?.getContent().shortcode}
count={evts.size}
role="option"
aria-selected={key === selectedKey}