Skip to content

Commit

Permalink
Ensure tooltip contents is linked via aria to the target element (#10729
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
t3chguy committed May 5, 2023
1 parent 8e962f6 commit 99ac9e5
Show file tree
Hide file tree
Showing 22 changed files with 133 additions and 43 deletions.
2 changes: 2 additions & 0 deletions res/css/views/right_panel/_UserInfo.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ limitations under the License.
line-height: $font-25px;
flex: 1;
justify-content: center;
// We reverse things here so for accessible technologies the name comes before the e2e shield
flex-direction: row-reverse;

span {
/* limit to 2 lines, show an ellipsis if it overflows */
Expand Down
11 changes: 9 additions & 2 deletions src/components/structures/auth/forgot-password/CheckEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { ReactNode } from "react";
import React, { ReactNode, useRef } from "react";

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

const onResendClickFn = async (): Promise<void> => {
Expand All @@ -68,10 +69,16 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
<input onClick={onSubmitForm} type="button" className="mx_Login_submit" value={_t("Next")} />
<div className="mx_AuthBody_did-not-receive">
<span className="mx_VerifyEMailDialog_text-light">{_t("Did not receive it?")}</span>
<AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onResendClickFn}>
<AccessibleButton
className="mx_AuthBody_resend-button"
kind="link"
onClick={onResendClickFn}
aria-describedby={tooltipVisible ? tooltipId : undefined}
>
<RetryIcon className="mx_Icon mx_Icon_16" />
{_t("Resend")}
<Tooltip
id={tooltipId}
label={_t("Verification link email resent!")}
alignment={Alignment.Top}
visible={tooltipVisible}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { ReactNode } from "react";
import React, { ReactNode, useRef } from "react";

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

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

<div className="mx_AuthBody_did-not-receive">
<span className="mx_VerifyEMailDialog_text-light">{_t("Did not receive it?")}</span>
<AccessibleButton className="mx_AuthBody_resend-button" kind="link" onClick={onResendClickFn}>
<AccessibleButton
className="mx_AuthBody_resend-button"
kind="link"
onClick={onResendClickFn}
aria-describedby={tooltipVisible ? tooltipId : undefined}
>
<RetryIcon className="mx_Icon mx_Icon_16" />
{_t("Resend")}
<Tooltip
id={tooltipId}
label={_t("Verification link email resent!")}
alignment={Alignment.Top}
visible={tooltipVisible}
Expand Down
6 changes: 3 additions & 3 deletions src/components/views/dialogs/UntrustedDeviceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ interface IProps {
}

const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) => {
let askToVerifyText;
let newSessionText;
let askToVerifyText: string;
let newSessionText: string;

if (MatrixClientPeg.get().getUserId() === user.userId) {
newSessionText = _t("You signed in to a new session without verifying it:");
Expand All @@ -51,7 +51,7 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) =
className="mx_UntrustedDeviceDialog"
title={
<>
<E2EIcon status={E2EState.Warning} size={24} hideTooltip={true} />
<E2EIcon status={E2EState.Warning} isUser size={24} hideTooltip={true} />
{_t("Not Trusted")}
</>
}
Expand Down
4 changes: 3 additions & 1 deletion src/components/views/elements/LinkWithTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import React from "react";

import TextWithTooltip from "./TextWithTooltip";

interface IProps extends Omit<React.ComponentProps<typeof TextWithTooltip>, "tabIndex" | "onClick"> {}
interface IProps extends Omit<React.ComponentProps<typeof TextWithTooltip>, "tabIndex" | "onClick" | "tooltip"> {
tooltip: string;
}

export default class LinkWithTooltip extends React.Component<IProps> {
public constructor(props: IProps) {
Expand Down
13 changes: 10 additions & 3 deletions src/components/views/elements/Pill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { ReactElement, useState } from "react";
import React, { ReactElement, useRef, useState } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/matrix";
Expand Down Expand Up @@ -89,6 +89,7 @@ export interface PillProps {
}

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

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

Expand Down Expand Up @@ -165,13 +166,19 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
onClick={onClick}
onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave}
aria-describedby={tooltipId}
>
{avatar}
<span className="mx_Pill_text">{pillText}</span>
{tip}
</a>
) : (
<span className={classes} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave}>
<span
className={classes}
onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave}
aria-describedby={tooltipId}
>
{avatar}
<span className="mx_Pill_text">{pillText}</span>
{tip}
Expand Down
4 changes: 4 additions & 0 deletions src/components/views/elements/TextWithTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export default class TextWithTooltip extends React.Component<IProps> {
public render(): React.ReactNode {
const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;

if (typeof tooltip === "string") {
props["aria-label"] = tooltip;
}

return (
<TooltipTarget
onClick={this.props.onClick}
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/elements/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
style.display = this.props.visible ? "block" : "none";

const tooltip = (
<div role={this.props.role || "tooltip"} className={tooltipClasses} style={style}>
<div id={this.props.id} role={this.props.role || "tooltip"} className={tooltipClasses} style={style}>
<div className="mx_Tooltip_chevron" />
{this.props.label}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/ReactionsRowButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
mx_ReactionsRowButton_selected: !!myReactionEvent,
});

let tooltip;
let tooltip: JSX.Element | undefined;
if (this.state.tooltipRendered) {
tooltip = (
<ReactionsRowButtonTooltip
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/messages/ReactionsRowButtonTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
const { content, reactionEvents, mxEvent, visible } = this.props;

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

let tooltip;
let tooltip: JSX.Element | undefined;
if (tooltipLabel) {
tooltip = <Tooltip visible={visible} label={tooltipLabel} />;
}
Expand Down
8 changes: 4 additions & 4 deletions src/components/views/right_panel/UserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1560,9 +1560,9 @@ export const UserInfoHeader: React.FC<{
</div>
);

let presenceState;
let presenceLastActiveAgo;
let presenceCurrentlyActive;
let presenceState: string | undefined;
let presenceLastActiveAgo: number | undefined;
let presenceCurrentlyActive: boolean | undefined;
if (member instanceof RoomMember && member.user) {
presenceState = member.user.presence;
presenceLastActiveAgo = member.user.lastActiveAgo;
Expand Down Expand Up @@ -1597,10 +1597,10 @@ export const UserInfoHeader: React.FC<{
<div className="mx_UserInfo_profile">
<div>
<h2>
{e2eIcon}
<span title={displayName} aria-label={displayName} dir="auto">
{displayName}
</span>
{e2eIcon}
</h2>
</div>
<div className="mx_UserInfo_profile_mxid">
Expand Down
36 changes: 23 additions & 13 deletions src/components/views/rooms/E2EIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { _t, _td } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import Tooltip, { Alignment } from "../elements/Tooltip";
import { E2EStatus } from "../../../utils/ShieldUtils";
import { XOR } from "../../../@types/common";

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

interface IProps {
isUser?: boolean;
status?: E2EState | E2EStatus;
interface Props {
className?: string;
size?: number;
onClick?: () => void;
Expand All @@ -53,7 +52,17 @@ interface IProps {
bordered?: boolean;
}

const E2EIcon: React.FC<IProps> = ({
interface UserProps extends Props {
isUser: true;
status: E2EState | E2EStatus;
}

interface RoomProps extends Props {
isUser?: false;
status: E2EStatus;
}

const E2EIcon: React.FC<XOR<UserProps, RoomProps>> = ({
isUser,
status,
className,
Expand All @@ -77,12 +86,10 @@ const E2EIcon: React.FC<IProps> = ({
);

let e2eTitle: string | undefined;
if (status) {
if (isUser) {
e2eTitle = crossSigningUserTitles[status];
} else {
e2eTitle = crossSigningRoomTitles[status];
}
if (isUser) {
e2eTitle = crossSigningUserTitles[status];
} else {
e2eTitle = crossSigningRoomTitles[status];
}

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

const label = e2eTitle ? _t(e2eTitle) : "";

let tip: JSX.Element | undefined;
if (hover && !hideTooltip) {
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} alignment={tooltipAlignment} />;
if (hover && !hideTooltip && label) {
tip = <Tooltip label={label} alignment={tooltipAlignment} />;
}

if (onClick) {
Expand All @@ -106,14 +115,15 @@ const E2EIcon: React.FC<IProps> = ({
onMouseLeave={onMouseLeave}
className={classes}
style={style}
aria-label={label}
>
{tip}
</AccessibleButton>
);
}

return (
<div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style}>
<div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style} aria-label={label}>
{tip}
</div>
);
Expand Down
12 changes: 10 additions & 2 deletions src/components/views/rooms/EventTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

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

const classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
return (
<div className={classes} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
<div
className={classes}
onMouseEnter={this.onHoverStart}
onMouseLeave={this.onHoverEnd}
aria-label={this.props.title}
>
{tooltip}
</div>
);
Expand All @@ -1525,6 +1530,7 @@ interface ISentReceiptProps {
}

function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
const tooltipId = useRef(`mx_SentReceipt_${Math.random()}`).current;
const isSent = !messageState || messageState === "sent";
const isFailed = messageState === "not_sent";
const receiptClasses = classNames({
Expand All @@ -1546,6 +1552,7 @@ function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
label = _t("Failed to send");
}
const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({
id: tooltipId,
label: label,
alignment: Alignment.TopRight,
});
Expand All @@ -1559,6 +1566,7 @@ function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
onMouseLeave={hideTooltip}
onFocus={showTooltip}
onBlur={hideTooltip}
aria-describedby={tooltipId}
>
<span className="mx_ReadReceiptGroup_container">
<span className={receiptClasses}>{nonCssBadge}</span>
Expand Down
Loading

0 comments on commit 99ac9e5

Please sign in to comment.