Improve voiceover experience

- As well as stylng cells, set the tabIndex(roving)
- Natively focus the div with .focus() so screen reader actually moves over the cells
- improve labels and roles
This commit is contained in:
David Langley 2025-05-08 18:46:36 +01:00
parent 919b5ee452
commit ce68db5c20
5 changed files with 26 additions and 7 deletions

View File

@ -117,10 +117,14 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
<MemberListHeaderView vm={vm} />
</Form.Root>
<Virtuoso
aria-label={_t("room_list|list_title")}
role="grid"
ref={ref}
style={{ height: "100%" }}
scrollerRef={scrollerRef}
context={{ focusedIndex }}
// Don't focus on the table as a whole go straight to the first item in the list
tabIndex={undefined}
data={vm.members}
onFocus={onFocus}
itemContent={(index, member) => getRowComponent(member, index === focusedIndex)}

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import React, { useEffect, type JSX } from "react";
import DisambiguatedProfile from "../../../messages/DisambiguatedProfile";
import { type RoomMember } from "../../../../../models/rooms/RoomMember";
@ -37,7 +37,7 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
/>
);
const name = vm.name;
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
const nameJSX = <DisambiguatedProfile withTooltip member={member} fallbackName={name || ""} />;
const presenceState = member.presenceState;
let presenceJSX: JSX.Element | undefined;
@ -60,6 +60,7 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
presenceJsx={presenceJSX}
nameJsx={nameJSX}
userLabel={vm.userLabel}
ariaLabel={_t("member_list|open_profile", { memberName: name })}
iconJsx={iconJsx}
focused={props.focused}
/>

View File

@ -12,6 +12,7 @@ import { type ThreePIDInvite } from "../../../../../models/rooms/ThreePIDInvite"
import BaseAvatar from "../../../avatars/BaseAvatar";
import { MemberTileView } from "./common/MemberTileView";
import { InvitedIconView } from "./common/InvitedIconView";
import { _t } from "../../../../../languageHandler";
interface Props {
threePidInvite: ThreePIDInvite;
@ -22,12 +23,14 @@ export function ThreePidInviteTileView(props: Props): JSX.Element {
const vm = useThreePidTileViewModel(props);
const av = <BaseAvatar name={vm.name} size="32px" aria-hidden="true" />;
const iconJsx = <InvitedIconView isThreePid={true} />;
const name = vm.name;
return (
<MemberTileView
nameJsx={vm.name}
nameJsx={name}
avatarJsx={av}
onClick={vm.onClick}
ariaLabel={_t("member_list|open_profile", { memberName: name })}
userLabel={vm.userLabel}
iconJsx={iconJsx}
focused={props.focused}

View File

@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/
import classNames from "classnames";
import React, { type JSX } from "react";
import React, { useEffect, useRef, type JSX } from "react";
import AccessibleButton from "../../../../elements/AccessibleButton";
@ -14,6 +14,7 @@ interface Props {
avatarJsx: JSX.Element;
nameJsx: JSX.Element | string;
onClick: () => void;
ariaLabel: string;
presenceJsx?: JSX.Element;
userLabel?: React.ReactNode;
iconJsx?: JSX.Element;
@ -25,16 +26,24 @@ export function MemberTileView(props: Props): JSX.Element {
if (props.userLabel) {
userLabelJsx = <div className="mx_MemberTileView_userLabel">{props.userLabel}</div>;
}
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (props.focused) {
ref.current?.focus();
}
}, [props.focused]);
return (
// The wrapping div is required to make the magic mouse listener work, for some reason.
<div>
<AccessibleButton
ref={ref}
className={classNames("mx_MemberTileView", {
mx_MemberTileView_hover: props.focused,
})}
onClick={props.onClick}
tabIndex={-1}
aria-label={props.ariaLabel}
tabIndex={props.focused ? 0 : -1}
role="gridcell"
>
<div className="mx_MemberTileView_left">
<div className="mx_MemberTileView_avatar">

View File

@ -1645,7 +1645,9 @@
"invite_button_no_perms_tooltip": "You do not have permission to invite users",
"invited_label": "Invited",
"no_matches": "No matches",
"power_label": "%(userName)s (power %(powerLevelNumber)s)"
"power_label": "%(userName)s (power %(powerLevelNumber)s)",
"list_title": "Member list",
"open_profile": "Open profile %(memberName)s"
},
"member_list_back_action_label": "Room members",
"message_edit_dialog_title": "Message edits",