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
This commit is contained in:
parent
8e962f6897
commit
99ac9e5029
22 changed files with 133 additions and 43 deletions
|
@ -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";
|
||||
|
@ -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> => {
|
||||
|
@ -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}
|
||||
|
|
|
@ -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";
|
||||
|
@ -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> => {
|
||||
|
@ -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}
|
||||
|
|
|
@ -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:");
|
||||
|
@ -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")}
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -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">
|
||||
|
|
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -106,6 +115,7 @@ const E2EIcon: React.FC<IProps> = ({
|
|||
onMouseLeave={onMouseLeave}
|
||||
className={classes}
|
||||
style={style}
|
||||
aria-label={label}
|
||||
>
|
||||
{tip}
|
||||
</AccessibleButton>
|
||||
|
@ -113,7 +123,7 @@ const E2EIcon: React.FC<IProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style}>
|
||||
<div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style} aria-label={label}>
|
||||
{tip}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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";
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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>
|
||||
|
|
|
@ -107,6 +107,7 @@ interface IState {
|
|||
}
|
||||
|
||||
export class MessageComposer extends React.Component<IProps, IState> {
|
||||
private tooltipId = `mx_MessageComposer_${Math.random()}`;
|
||||
private dispatcherRef?: string;
|
||||
private messageComposerInput = createRef<SendMessageComposerClass>();
|
||||
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
||||
|
@ -470,7 +471,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
public render(): React.ReactNode {
|
||||
const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus);
|
||||
const e2eIcon = hasE2EIcon && (
|
||||
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" />
|
||||
<E2EIcon key="e2eIcon" status={this.props.e2eStatus!} className="mx_MessageComposer_e2eIcon" />
|
||||
);
|
||||
|
||||
const controls: ReactNode[] = [];
|
||||
|
@ -561,11 +562,15 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let recordingTooltip;
|
||||
let recordingTooltip: JSX.Element | undefined;
|
||||
if (this.state.recordingTimeLeftSeconds) {
|
||||
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
|
||||
recordingTooltip = (
|
||||
<Tooltip label={_t("%(seconds)ss left", { seconds: secondsLeft })} alignment={Alignment.Top} />
|
||||
<Tooltip
|
||||
id={this.tooltipId}
|
||||
label={_t("%(seconds)ss left", { seconds: secondsLeft })}
|
||||
alignment={Alignment.Top}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -593,7 +598,11 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
});
|
||||
|
||||
return (
|
||||
<div className={classes} ref={this.ref}>
|
||||
<div
|
||||
className={classes}
|
||||
ref={this.ref}
|
||||
aria-describedby={this.state.recordingTimeLeftSeconds ? this.tooltipId : undefined}
|
||||
>
|
||||
{recordingTooltip}
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<ReplyPreview
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue