ux: reply null state, location error feedback, retry send, reaction keyboard nav
CI / Build & Quality Checks (push) Successful in 10m17s
CI / Build & Quality Checks (push) Successful in 10m17s
- Reply: distinguish loading (placeholder) from not-found (null) — show "Original message not available" instead of a stuck loading bar - RoomInput: geolocation errors now surface inline (denied / timed out / unsupported); location button shows Spinner during fetch and is disabled - Message menu: Retry Send + Cancel Message items appear when a message is in NOT_SENT or CANCELLED state, calling mx.resendEvent / cancelPendingEvent - ReactionViewer: sidebar gains role=listbox / role=option and ArrowUp/Down keyboard navigation between reactions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Avatar,
|
||||
@@ -51,6 +51,21 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
|
||||
const defaultReaction = reactions.find((reaction) => typeof reaction[0] === 'string');
|
||||
return defaultReaction ? defaultReaction[0] : '';
|
||||
});
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleSidebarKeyDown = (e: React.KeyboardEvent) => {
|
||||
const keys = reactions.map(([k]) => k).filter((k): k is string => typeof k === 'string');
|
||||
const currentIdx = keys.indexOf(selectedKey);
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
const next = keys[(currentIdx + 1) % keys.length];
|
||||
if (next) setSelectedKey(next);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
const prev = keys[(currentIdx - 1 + keys.length) % keys.length];
|
||||
if (prev) setSelectedKey(prev);
|
||||
}
|
||||
};
|
||||
|
||||
const getName = (member: RoomMember) =>
|
||||
getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId;
|
||||
@@ -74,7 +89,16 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<Box shrink="No" className={css.Sidebar}>
|
||||
<Box
|
||||
shrink="No"
|
||||
className={css.Sidebar}
|
||||
ref={sidebarRef}
|
||||
role="listbox"
|
||||
aria-label="Reactions"
|
||||
tabIndex={0}
|
||||
onKeyDown={handleSidebarKeyDown}
|
||||
style={{ outline: 'none' }}
|
||||
>
|
||||
<Scroll visibility="Hover" hideTrack size="300">
|
||||
<Box className={css.SidebarContent} direction="Column" gap="200">
|
||||
{reactions.map(([key, evts]) => {
|
||||
@@ -85,6 +109,7 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
|
||||
mx={mx}
|
||||
reaction={key}
|
||||
count={evts.size}
|
||||
role="option"
|
||||
aria-selected={key === selectedKey}
|
||||
onClick={() => setSelectedKey(key)}
|
||||
useAuthentication={useAuthentication}
|
||||
|
||||
Reference in New Issue
Block a user