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

Commit 99ac9e5

Browse files
authored
Ensure tooltip contents is linked via aria to the target element (#10729)
* Ensure tooltip contents is linked via aria to the target element * Iterate * Fix tests * Fix tests * Update snapshot * Fix missing aria labels for more tooltips * Iterate * Update snapshots
1 parent 8e962f6 commit 99ac9e5

File tree

22 files changed

+133
-43
lines changed

22 files changed

+133
-43
lines changed

res/css/views/right_panel/_UserInfo.pcss

+2
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ limitations under the License.
163163
line-height: $font-25px;
164164
flex: 1;
165165
justify-content: center;
166+
// We reverse things here so for accessible technologies the name comes before the e2e shield
167+
flex-direction: row-reverse;
166168

167169
span {
168170
/* limit to 2 lines, show an ellipsis if it overflows */

src/components/structures/auth/forgot-password/CheckEmail.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { ReactNode } from "react";
17+
import React, { ReactNode, useRef } from "react";
1818

1919
import AccessibleButton from "../../../views/elements/AccessibleButton";
2020
import { Icon as EMailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg";
@@ -42,6 +42,7 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
4242
onSubmitForm,
4343
onResendClick,
4444
}) => {
45+
const tooltipId = useRef(`mx_CheckEmail_${Math.random()}`).current;
4546
const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500);
4647

4748
const onResendClickFn = async (): Promise<void> => {
@@ -68,10 +69,16 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
6869
<input onClick={onSubmitForm} type="button" className="mx_Login_submit" value={_t("Next")} />
6970
<div className="mx_AuthBody_did-not-receive">
7071
<span className="mx_VerifyEMailDialog_text-light">{_t("Did not receive it?")}</span>
71-
<AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onResendClickFn}>
72+
<AccessibleButton
73+
className="mx_AuthBody_resend-button"
74+
kind="link"
75+
onClick={onResendClickFn}
76+
aria-describedby={tooltipVisible ? tooltipId : undefined}
77+
>
7278
<RetryIcon className="mx_Icon mx_Icon_16" />
7379
{_t("Resend")}
7480
<Tooltip
81+
id={tooltipId}
7582
label={_t("Verification link email resent!")}
7683
alignment={Alignment.Top}
7784
visible={tooltipVisible}

src/components/structures/auth/forgot-password/VerifyEmailModal.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { ReactNode } from "react";
17+
import React, { ReactNode, useRef } from "react";
1818

1919
import { _t } from "../../../../languageHandler";
2020
import AccessibleButton from "../../../views/elements/AccessibleButton";
@@ -40,6 +40,7 @@ export const VerifyEmailModal: React.FC<Props> = ({
4040
onReEnterEmailClick,
4141
onResendClick,
4242
}) => {
43+
const tooltipId = useRef(`mx_VerifyEmailModal_${Math.random()}`).current;
4344
const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500);
4445

4546
const onResendClickFn = async (): Promise<void> => {
@@ -66,10 +67,16 @@ export const VerifyEmailModal: React.FC<Props> = ({
6667

6768
<div className="mx_AuthBody_did-not-receive">
6869
<span className="mx_VerifyEMailDialog_text-light">{_t("Did not receive it?")}</span>
69-
<AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onResendClickFn}>
70+
<AccessibleButton
71+
className="mx_AuthBody_resend-button"
72+
kind="link"
73+
onClick={onResendClickFn}
74+
aria-describedby={tooltipVisible ? tooltipId : undefined}
75+
>
7076
<RetryIcon className="mx_Icon mx_Icon_16" />
7177
{_t("Resend")}
7278
<Tooltip
79+
id={tooltipId}
7380
label={_t("Verification link email resent!")}
7481
alignment={Alignment.Top}
7582
visible={tooltipVisible}

src/components/views/dialogs/UntrustedDeviceDialog.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ interface IProps {
3131
}
3232

3333
const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) => {
34-
let askToVerifyText;
35-
let newSessionText;
34+
let askToVerifyText: string;
35+
let newSessionText: string;
3636

3737
if (MatrixClientPeg.get().getUserId() === user.userId) {
3838
newSessionText = _t("You signed in to a new session without verifying it:");
@@ -51,7 +51,7 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) =
5151
className="mx_UntrustedDeviceDialog"
5252
title={
5353
<>
54-
<E2EIcon status={E2EState.Warning} size={24} hideTooltip={true} />
54+
<E2EIcon status={E2EState.Warning} isUser size={24} hideTooltip={true} />
5555
{_t("Not Trusted")}
5656
</>
5757
}

src/components/views/elements/LinkWithTooltip.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import React from "react";
1818

1919
import TextWithTooltip from "./TextWithTooltip";
2020

21-
interface IProps extends Omit<React.ComponentProps<typeof TextWithTooltip>, "tabIndex" | "onClick"> {}
21+
interface IProps extends Omit<React.ComponentProps<typeof TextWithTooltip>, "tabIndex" | "onClick" | "tooltip"> {
22+
tooltip: string;
23+
}
2224

2325
export default class LinkWithTooltip extends React.Component<IProps> {
2426
public constructor(props: IProps) {

src/components/views/elements/Pill.tsx

+10-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { ReactElement, useState } from "react";
17+
import React, { ReactElement, useRef, useState } from "react";
1818
import classNames from "classnames";
1919
import { Room } from "matrix-js-sdk/src/models/room";
2020
import { RoomMember } from "matrix-js-sdk/src/matrix";
@@ -89,6 +89,7 @@ export interface PillProps {
8989
}
9090

9191
export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room, shouldShowPillAvatar = true }) => {
92+
const tooltipId = useRef(`mx_Pill_${Math.random()}`).current;
9293
const [hover, setHover] = useState(false);
9394
const { event, member, onClick, resourceId, targetRoom, text, type } = usePermalink({
9495
room,
@@ -117,7 +118,7 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
117118
setHover(false);
118119
};
119120

120-
const tip = hover && resourceId ? <Tooltip label={resourceId} alignment={Alignment.Right} /> : null;
121+
const tip = hover && resourceId ? <Tooltip id={tooltipId} label={resourceId} alignment={Alignment.Right} /> : null;
121122
let avatar: ReactElement | null = null;
122123
let pillText: string | null = text;
123124

@@ -165,13 +166,19 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
165166
onClick={onClick}
166167
onMouseOver={onMouseOver}
167168
onMouseLeave={onMouseLeave}
169+
aria-describedby={tooltipId}
168170
>
169171
{avatar}
170172
<span className="mx_Pill_text">{pillText}</span>
171173
{tip}
172174
</a>
173175
) : (
174-
<span className={classes} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave}>
176+
<span
177+
className={classes}
178+
onMouseOver={onMouseOver}
179+
onMouseLeave={onMouseLeave}
180+
aria-describedby={tooltipId}
181+
>
175182
{avatar}
176183
<span className="mx_Pill_text">{pillText}</span>
177184
{tip}

src/components/views/elements/TextWithTooltip.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export default class TextWithTooltip extends React.Component<IProps> {
3535
public render(): React.ReactNode {
3636
const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;
3737

38+
if (typeof tooltip === "string") {
39+
props["aria-label"] = tooltip;
40+
}
41+
3842
return (
3943
<TooltipTarget
4044
onClick={this.props.onClick}

src/components/views/elements/Tooltip.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
188188
style.display = this.props.visible ? "block" : "none";
189189

190190
const tooltip = (
191-
<div role={this.props.role || "tooltip"} className={tooltipClasses} style={style}>
191+
<div id={this.props.id} role={this.props.role || "tooltip"} className={tooltipClasses} style={style}>
192192
<div className="mx_Tooltip_chevron" />
193193
{this.props.label}
194194
</div>

src/components/views/messages/ReactionsRowButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
9292
mx_ReactionsRowButton_selected: !!myReactionEvent,
9393
});
9494

95-
let tooltip;
95+
let tooltip: JSX.Element | undefined;
9696
if (this.state.tooltipRendered) {
9797
tooltip = (
9898
<ReactionsRowButtonTooltip

src/components/views/messages/ReactionsRowButtonTooltip.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
4040
const { content, reactionEvents, mxEvent, visible } = this.props;
4141

4242
const room = this.context.getRoom(mxEvent.getRoomId());
43-
let tooltipLabel;
43+
let tooltipLabel: JSX.Element | undefined;
4444
if (room) {
4545
const senders: string[] = [];
4646
for (const reactionEvent of reactionEvents) {
@@ -72,7 +72,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
7272
);
7373
}
7474

75-
let tooltip;
75+
let tooltip: JSX.Element | undefined;
7676
if (tooltipLabel) {
7777
tooltip = <Tooltip visible={visible} label={tooltipLabel} />;
7878
}

src/components/views/right_panel/UserInfo.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -1560,9 +1560,9 @@ export const UserInfoHeader: React.FC<{
15601560
</div>
15611561
);
15621562

1563-
let presenceState;
1564-
let presenceLastActiveAgo;
1565-
let presenceCurrentlyActive;
1563+
let presenceState: string | undefined;
1564+
let presenceLastActiveAgo: number | undefined;
1565+
let presenceCurrentlyActive: boolean | undefined;
15661566
if (member instanceof RoomMember && member.user) {
15671567
presenceState = member.user.presence;
15681568
presenceLastActiveAgo = member.user.lastActiveAgo;
@@ -1597,10 +1597,10 @@ export const UserInfoHeader: React.FC<{
15971597
<div className="mx_UserInfo_profile">
15981598
<div>
15991599
<h2>
1600-
{e2eIcon}
16011600
<span title={displayName} aria-label={displayName} dir="auto">
16021601
{displayName}
16031602
</span>
1603+
{e2eIcon}
16041604
</h2>
16051605
</div>
16061606
<div className="mx_UserInfo_profile_mxid">

src/components/views/rooms/E2EIcon.tsx

+23-13
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { _t, _td } from "../../../languageHandler";
2222
import AccessibleButton from "../elements/AccessibleButton";
2323
import Tooltip, { Alignment } from "../elements/Tooltip";
2424
import { E2EStatus } from "../../../utils/ShieldUtils";
25+
import { XOR } from "../../../@types/common";
2526

2627
export enum E2EState {
2728
Verified = "verified",
@@ -42,9 +43,7 @@ const crossSigningRoomTitles: { [key in E2EState]?: string } = {
4243
[E2EState.Verified]: _td("Everyone in this room is verified"),
4344
};
4445

45-
interface IProps {
46-
isUser?: boolean;
47-
status?: E2EState | E2EStatus;
46+
interface Props {
4847
className?: string;
4948
size?: number;
5049
onClick?: () => void;
@@ -53,7 +52,17 @@ interface IProps {
5352
bordered?: boolean;
5453
}
5554

56-
const E2EIcon: React.FC<IProps> = ({
55+
interface UserProps extends Props {
56+
isUser: true;
57+
status: E2EState | E2EStatus;
58+
}
59+
60+
interface RoomProps extends Props {
61+
isUser?: false;
62+
status: E2EStatus;
63+
}
64+
65+
const E2EIcon: React.FC<XOR<UserProps, RoomProps>> = ({
5766
isUser,
5867
status,
5968
className,
@@ -77,12 +86,10 @@ const E2EIcon: React.FC<IProps> = ({
7786
);
7887

7988
let e2eTitle: string | undefined;
80-
if (status) {
81-
if (isUser) {
82-
e2eTitle = crossSigningUserTitles[status];
83-
} else {
84-
e2eTitle = crossSigningRoomTitles[status];
85-
}
89+
if (isUser) {
90+
e2eTitle = crossSigningUserTitles[status];
91+
} else {
92+
e2eTitle = crossSigningRoomTitles[status];
8693
}
8794

8895
let style: CSSProperties | undefined;
@@ -93,9 +100,11 @@ const E2EIcon: React.FC<IProps> = ({
93100
const onMouseOver = (): void => setHover(true);
94101
const onMouseLeave = (): void => setHover(false);
95102

103+
const label = e2eTitle ? _t(e2eTitle) : "";
104+
96105
let tip: JSX.Element | undefined;
97-
if (hover && !hideTooltip) {
98-
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} alignment={tooltipAlignment} />;
106+
if (hover && !hideTooltip && label) {
107+
tip = <Tooltip label={label} alignment={tooltipAlignment} />;
99108
}
100109

101110
if (onClick) {
@@ -106,14 +115,15 @@ const E2EIcon: React.FC<IProps> = ({
106115
onMouseLeave={onMouseLeave}
107116
className={classes}
108117
style={style}
118+
aria-label={label}
109119
>
110120
{tip}
111121
</AccessibleButton>
112122
);
113123
}
114124

115125
return (
116-
<div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style}>
126+
<div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style} aria-label={label}>
117127
{tip}
118128
</div>
119129
);

src/components/views/rooms/EventTile.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18-
import React, { createRef, forwardRef, MouseEvent, ReactNode, RefObject } from "react";
18+
import React, { createRef, forwardRef, MouseEvent, ReactNode, RefObject, useRef } from "react";
1919
import classNames from "classnames";
2020
import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
2121
import { EventStatus, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
@@ -1513,7 +1513,12 @@ class E2ePadlock extends React.Component<IE2ePadlockProps, IE2ePadlockState> {
15131513

15141514
const classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
15151515
return (
1516-
<div className={classes} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
1516+
<div
1517+
className={classes}
1518+
onMouseEnter={this.onHoverStart}
1519+
onMouseLeave={this.onHoverEnd}
1520+
aria-label={this.props.title}
1521+
>
15171522
{tooltip}
15181523
</div>
15191524
);
@@ -1525,6 +1530,7 @@ interface ISentReceiptProps {
15251530
}
15261531

15271532
function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
1533+
const tooltipId = useRef(`mx_SentReceipt_${Math.random()}`).current;
15281534
const isSent = !messageState || messageState === "sent";
15291535
const isFailed = messageState === "not_sent";
15301536
const receiptClasses = classNames({
@@ -1546,6 +1552,7 @@ function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
15461552
label = _t("Failed to send");
15471553
}
15481554
const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({
1555+
id: tooltipId,
15491556
label: label,
15501557
alignment: Alignment.TopRight,
15511558
});
@@ -1559,6 +1566,7 @@ function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
15591566
onMouseLeave={hideTooltip}
15601567
onFocus={showTooltip}
15611568
onBlur={hideTooltip}
1569+
aria-describedby={tooltipId}
15621570
>
15631571
<span className="mx_ReadReceiptGroup_container">
15641572
<span className={receiptClasses}>{nonCssBadge}</span>

0 commit comments

Comments
 (0)