Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure tooltip contents is linked via aria to the target element #10729

Merged
merged 8 commits into from
May 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading