From 1b5e6a37f58c9d16b4ebc9f765cc3d39cf16ef05 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Sat, 26 Sep 2026 11:34:03 -0400 Subject: [PATCH] =?UTF-8?q?fix(a11y):=20stable=20reaction=20labels=20?= =?UTF-8?q?=E2=80=94=20emoji=20glyph,=20custom=20emoji=20shortcode=20(#179?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/components/message/Reaction.tsx | 18 +++++++++++++----- src/app/features/room/message/Reactions.tsx | 3 ++- .../room/reaction-viewer/ReactionViewer.tsx | 1 + 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/app/components/message/Reaction.tsx b/src/app/components/message/Reaction.tsx index b9e69788f..b106b233a 100644 --- a/src/app/components/message/Reaction.tsx +++ b/src/app/components/message/Reaction.tsx @@ -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 ( ( key={key} mx={mx} reaction={key} + shortcode={rEvents.find(eventWithShortcode)?.getContent().shortcode} count={events.size} onClick={canSendReaction ? () => onReactionToggle(mEventId, key) : undefined} onContextMenu={handleViewReaction} diff --git a/src/app/features/room/reaction-viewer/ReactionViewer.tsx b/src/app/features/room/reaction-viewer/ReactionViewer.tsx index eee010893..0517ca1df 100644 --- a/src/app/features/room/reaction-viewer/ReactionViewer.tsx +++ b/src/app/features/room/reaction-viewer/ReactionViewer.tsx @@ -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}