Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Accessibility: Improve Voiceover #12189

Closed
wants to merge 14 commits into from
12 changes: 12 additions & 0 deletions src/components/views/avatars/MemberAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" |
forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false.
hideTitle?: boolean;
children?: ReactNode;
// Will append a dot to the aria-label if true
includePauseInAriaLabel?: boolean;
}

function MemberAvatar(
Expand All @@ -51,6 +53,7 @@ function MemberAvatar(
fallbackUserId,
hideTitle,
member: propsMember,
includePauseInAriaLabel,
...props
}: IProps,
ref: Ref<HTMLElement>,
Expand Down Expand Up @@ -83,9 +86,17 @@ function MemberAvatar(
}
}

/**
* If this avatar is rendered in the event tile before the message, we append a dot to the
* aria-label so that assistive technologies read out `name <pause> message-content` instead
* of `name message-content`.
*/
const ariaLabel = includePauseInAriaLabel ? `${member?.name}. ` : member?.name;

return (
<BaseAvatar
{...props}
tabIndex={-1}
size={size}
name={name ?? ""}
title={hideTitle ? undefined : title}
Expand All @@ -102,6 +113,7 @@ function MemberAvatar(
}
: props.onClick
}
aria-label={ariaLabel ?? ""}
altText={_t("common|user_avatar")}
ref={ref}
/>
Expand Down
8 changes: 5 additions & 3 deletions src/components/views/messages/TextualBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { getParentEventId } from "../../../utils/Reply";
import { EditWysiwygComposer } from "../rooms/wysiwyg_composer";
import { IEventTileOps } from "../rooms/EventTile";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { getIdForBody } from "./shared/getIdForBody";

const MAX_HIGHLIGHT_LENGTH = 4096;

Expand Down Expand Up @@ -612,6 +613,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
}

let widgets;
const id = getIdForBody(mxEvent);
if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
widgets = (
<LinkPreviewGroup
Expand All @@ -625,7 +627,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {

if (isEmote) {
return (
<div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
<div className="mx_MEmoteBody mx_EventTile_content" id={id} onClick={this.onBodyLinkClick}>
*&nbsp;
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}
Expand All @@ -638,14 +640,14 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
}
if (isNotice) {
return (
<div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
<div className="mx_MNoticeBody mx_EventTile_content" id={id} onClick={this.onBodyLinkClick}>
{body}
{widgets}
</div>
);
}
return (
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
<div className="mx_MTextBody mx_EventTile_content" id={id} onClick={this.onBodyLinkClick}>
{body}
{widgets}
</div>
Expand Down
26 changes: 26 additions & 0 deletions src/components/views/messages/shared/getIdForBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
Copyright 2024 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { MatrixEvent } from "matrix-js-sdk/src/matrix";

/**
* Generate a html element id from this event
* @param mxEvent Event from which the id is derived
* @returns id to give the element
*/
export function getIdForBody(mxEvent: MatrixEvent): string {
return `mx_EventTile_content_${mxEvent.getTxnId() ?? mxEvent.getId()}`;
}
29 changes: 25 additions & 4 deletions src/components/views/rooms/EventTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import { ElementCall } from "../../../models/Call";
import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge";
import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar";
import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper";
import { getIdForBody } from "../messages/shared/getIdForBody";

export type GetRelationsForEvent = (
eventId: string,
Expand Down Expand Up @@ -281,6 +282,15 @@ export function isEligibleForSpecialReceipt(event: MatrixEvent): boolean {
return true;
}

/**
* Get an id for the avatar div
* @param event Event from which id is derived
* @returns The id that was created
*/
function getIdForAvatar(event: MatrixEvent): string {
return `mx_EventTile_avatar_${event.getTxnId() ?? event.getId()}`;
}

// MUST be rendered within a RoomContext with a set timelineRenderingType
export class UnwrappedEventTile extends React.Component<EventTileProps, IState> {
private suppressReadReceiptAnimation: boolean;
Expand Down Expand Up @@ -1061,12 +1071,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
this.context.timelineRenderingType,
);
avatar = (
<div className="mx_EventTile_avatar">
<div className="mx_EventTile_avatar" id={getIdForAvatar(this.props.mxEvent)} tabIndex={-1}>
<MemberAvatar
member={member}
size={avatarSize}
viewUserOnClick={viewUserOnClick}
forceHistorical={this.props.mxEvent.getType() === EventType.RoomMember}
includePauseInAriaLabel={true}
/>
</div>
);
Expand Down Expand Up @@ -1147,6 +1158,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
<a
href={permalink}
onClick={this.onPermalinkClicked}
tabIndex={-1}
aria-label={formatTime(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour)}
onContextMenu={this.onTimestampContextMenu}
>
Expand Down Expand Up @@ -1207,7 +1219,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
{
"ref": this.ref,
"className": classes,
"tabIndex": 0,
"aria-live": ariaLive,
"aria-labelledby": `${getIdForAvatar(this.props.mxEvent)} ${getIdForBody(this.props.mxEvent)}`,
"aria-atomic": true,
"data-scroll-tokens": scrollToken,
"data-has-reply": !!replyChain,
Expand Down Expand Up @@ -1261,8 +1275,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
{
"ref": this.ref,
"className": classes,
"tabIndex": -1,
"tabindex": 0,
"aria-live": ariaLive,
"aria-labelledby": `${getIdForAvatar(this.props.mxEvent)} ${getIdForBody(this.props.mxEvent)}`,
"aria-atomic": "true",
"data-scroll-tokens": scrollToken,
"data-layout": this.props.layout,
Expand Down Expand Up @@ -1395,8 +1410,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
{
"ref": this.ref,
"className": classes,
"tabIndex": -1,
"tabindex": 0,
"aria-live": ariaLive,
"aria-labelledby": `${getIdForAvatar(this.props.mxEvent)} ${getIdForBody(this.props.mxEvent)}`,
"aria-atomic": "true",
"data-scroll-tokens": scrollToken,
"data-layout": this.props.layout,
Expand All @@ -1411,7 +1427,12 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
{sender}
{ircPadlock}
{avatar}
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
<div
className={lineClasses}
key="mx_EventTile_line"
onContextMenu={this.onContextMenu}
aria-labelledby={"mx_EventTile_content_" + this.props.mxEvent.getId()}
>
{this.renderContextMenu()}
{groupTimestamp}
{groupPadlock}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,14 @@ exports[`MessagePanel should handle lots of membership events quickly 1`] = `
class="mx_GenericEventListSummary_avatars"
>
<span
aria-label="@user:id"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 14px;"
tabindex="-1"
title="@user:id"
>
u
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
class="mx_Marker_border"
>
<span
aria-label="@alice:server"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="6"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 36px;"
tabindex="-1"
title="@alice:server"
>
a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ exports[`<BeaconViewDialog /> renders own beacon status when user is live sharin
class="mx_DialogOwnBeaconStatus"
>
<span
aria-label="@alice:server"
class="_avatar_mcap2_17 mx_BaseAvatar mx_DialogOwnBeaconStatus_avatar _avatar-imageless_mcap2_61"
data-color="6"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
tabindex="-1"
title="@alice:server"
>
a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ exports[`<DialogSidebar /> renders sidebar correctly with beacons 1`] = `
class="mx_BeaconListItem"
>
<span
aria-label=""
class="_avatar_mcap2_17 mx_BaseAvatar mx_BeaconListItem_avatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
tabindex="-1"
>

</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ exports[`ConfirmUserActionDialog renders 1`] = `
class="mx_ConfirmUserActionDialog_avatar"
>
<span
aria-label="@user:test.com"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="3"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 48px;"
tabindex="-1"
title="@user:test.com"
>
u
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ exports[`<FacePile /> renders with a tooltip 1`] = `
class="_stacked-avatars_mcap2_111"
>
<span
aria-label="456"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 36px;"
tabindex="-1"
>
4
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ exports[`<Pill> should render the expected pill for a known user not in the room
>
<span
aria-hidden="true"
aria-label="User 2"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="5"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
tabindex="-1"
>
U
</span>
Expand Down Expand Up @@ -131,12 +133,14 @@ exports[`<Pill> should render the expected pill for a message in the same room 1
>
<span
aria-hidden="true"
aria-label="User 1"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
tabindex="-1"
>
U
</span>
Expand Down Expand Up @@ -273,12 +277,14 @@ exports[`<Pill> when rendering a pill for a user in the room should render as ex
>
<span
aria-hidden="true"
aria-label="User 1"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
tabindex="-1"
>
U
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ exports[`<RoomFacePile /> renders 1`] = `
class="_stacked-avatars_mcap2_111"
>
<span
aria-label="@bob:example.org"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 28px;"
tabindex="-1"
>
b
</span>
Expand Down
2 changes: 1 addition & 1 deletion test/components/views/messages/TextualBody-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ describe("<TextualBody />", () => {
const { container } = getComponent({ mxEvent: ev });
const content = container.querySelector(".mx_EventTile_body");
expect(content.innerHTML).toMatchInlineSnapshot(
`"Chat with <span><bdi><a class="mx_Pill mx_UserPill mx_UserPill_me" href="https://matrix.to/#/@user:example.com"><span aria-label="Profile picture" aria-hidden="true" data-testid="avatar-img" data-type="round" data-color="2" class="_avatar_mcap2_17 mx_BaseAvatar" style="--cpd-avatar-size: 16px;"><img loading="lazy" alt="" src="mxc://avatar.url/image.png" referrerpolicy="no-referrer" class="_image_mcap2_50" data-type="round" width="16px" height="16px"></span><span class="mx_Pill_text">Member</span></a></bdi></span>"`,
`"Chat with <span><bdi><a class="mx_Pill mx_UserPill mx_UserPill_me" href="https://matrix.to/#/@user:example.com"><span aria-label="Member" aria-hidden="true" tabindex="-1" data-testid="avatar-img" data-type="round" data-color="2" class="_avatar_mcap2_17 mx_BaseAvatar" style="--cpd-avatar-size: 16px;"><img loading="lazy" alt="" src="mxc://avatar.url/image.png" referrerpolicy="no-referrer" class="_image_mcap2_50" data-type="round" width="16px" height="16px"></span><span class="mx_Pill_text">Member</span></a></bdi></span>"`,
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,14 @@ exports[`MLocationBody <MLocationBody> without error renders marker correctly fo
class="mx_Marker_border"
>
<span
aria-label="@user:server"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="3"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 36px;"
tabindex="-1"
title="@user:server"
>
u
Expand Down
Loading
Loading