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:
Michael Telatynski 2023-05-05 09:26:11 +01:00 committed by GitHub
parent 8e962f6897
commit 99ac9e5029
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 133 additions and 43 deletions

View file

@ -163,6 +163,8 @@ limitations under the License.
line-height: $font-25px; line-height: $font-25px;
flex: 1; flex: 1;
justify-content: center; justify-content: center;
// We reverse things here so for accessible technologies the name comes before the e2e shield
flex-direction: row-reverse;
span { span {
/* limit to 2 lines, show an ellipsis if it overflows */ /* limit to 2 lines, show an ellipsis if it overflows */

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ReactNode } from "react"; import React, { ReactNode, useRef } from "react";
import AccessibleButton from "../../../views/elements/AccessibleButton"; import AccessibleButton from "../../../views/elements/AccessibleButton";
import { Icon as EMailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg"; import { Icon as EMailPromptIcon } from "../../../../../res/img/element-icons/email-prompt.svg";
@ -42,6 +42,7 @@ export const CheckEmail: React.FC<CheckEmailProps> = ({
onSubmitForm, onSubmitForm,
onResendClick, onResendClick,
}) => { }) => {
const tooltipId = useRef(`mx_CheckEmail_${Math.random()}`).current;
const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500); const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500);
const onResendClickFn = async (): Promise<void> => { 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")} /> <input onClick={onSubmitForm} type="button" className="mx_Login_submit" value={_t("Next")} />
<div className="mx_AuthBody_did-not-receive"> <div className="mx_AuthBody_did-not-receive">
<span className="mx_VerifyEMailDialog_text-light">{_t("Did not receive it?")}</span> <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" /> <RetryIcon className="mx_Icon mx_Icon_16" />
{_t("Resend")} {_t("Resend")}
<Tooltip <Tooltip
id={tooltipId}
label={_t("Verification link email resent!")} label={_t("Verification link email resent!")}
alignment={Alignment.Top} alignment={Alignment.Top}
visible={tooltipVisible} visible={tooltipVisible}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ReactNode } from "react"; import React, { ReactNode, useRef } from "react";
import { _t } from "../../../../languageHandler"; import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../../views/elements/AccessibleButton"; import AccessibleButton from "../../../views/elements/AccessibleButton";
@ -40,6 +40,7 @@ export const VerifyEmailModal: React.FC<Props> = ({
onReEnterEmailClick, onReEnterEmailClick,
onResendClick, onResendClick,
}) => { }) => {
const tooltipId = useRef(`mx_VerifyEmailModal_${Math.random()}`).current;
const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500); const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500);
const onResendClickFn = async (): Promise<void> => { const onResendClickFn = async (): Promise<void> => {
@ -66,10 +67,16 @@ export const VerifyEmailModal: React.FC<Props> = ({
<div className="mx_AuthBody_did-not-receive"> <div className="mx_AuthBody_did-not-receive">
<span className="mx_VerifyEMailDialog_text-light">{_t("Did not receive it?")}</span> <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" /> <RetryIcon className="mx_Icon mx_Icon_16" />
{_t("Resend")} {_t("Resend")}
<Tooltip <Tooltip
id={tooltipId}
label={_t("Verification link email resent!")} label={_t("Verification link email resent!")}
alignment={Alignment.Top} alignment={Alignment.Top}
visible={tooltipVisible} visible={tooltipVisible}

View file

@ -31,8 +31,8 @@ interface IProps {
} }
const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) => { const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) => {
let askToVerifyText; let askToVerifyText: string;
let newSessionText; let newSessionText: string;
if (MatrixClientPeg.get().getUserId() === user.userId) { if (MatrixClientPeg.get().getUserId() === user.userId) {
newSessionText = _t("You signed in to a new session without verifying it:"); 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" className="mx_UntrustedDeviceDialog"
title={ title={
<> <>
<E2EIcon status={E2EState.Warning} size={24} hideTooltip={true} /> <E2EIcon status={E2EState.Warning} isUser size={24} hideTooltip={true} />
{_t("Not Trusted")} {_t("Not Trusted")}
</> </>
} }

View file

@ -18,7 +18,9 @@ import React from "react";
import TextWithTooltip from "./TextWithTooltip"; 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> { export default class LinkWithTooltip extends React.Component<IProps> {
public constructor(props: IProps) { public constructor(props: IProps) {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ReactElement, useState } from "react"; import React, { ReactElement, useRef, useState } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/matrix"; 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 }) => { 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 [hover, setHover] = useState(false);
const { event, member, onClick, resourceId, targetRoom, text, type } = usePermalink({ const { event, member, onClick, resourceId, targetRoom, text, type } = usePermalink({
room, room,
@ -117,7 +118,7 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
setHover(false); 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 avatar: ReactElement | null = null;
let pillText: string | null = text; let pillText: string | null = text;
@ -165,13 +166,19 @@ export const Pill: React.FC<PillProps> = ({ type: propType, url, inMessage, room
onClick={onClick} onClick={onClick}
onMouseOver={onMouseOver} onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
aria-describedby={tooltipId}
> >
{avatar} {avatar}
<span className="mx_Pill_text">{pillText}</span> <span className="mx_Pill_text">{pillText}</span>
{tip} {tip}
</a> </a>
) : ( ) : (
<span className={classes} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave}> <span
className={classes}
onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave}
aria-describedby={tooltipId}
>
{avatar} {avatar}
<span className="mx_Pill_text">{pillText}</span> <span className="mx_Pill_text">{pillText}</span>
{tip} {tip}

View file

@ -35,6 +35,10 @@ export default class TextWithTooltip extends React.Component<IProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props; const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;
if (typeof tooltip === "string") {
props["aria-label"] = tooltip;
}
return ( return (
<TooltipTarget <TooltipTarget
onClick={this.props.onClick} onClick={this.props.onClick}

View file

@ -188,7 +188,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
style.display = this.props.visible ? "block" : "none"; style.display = this.props.visible ? "block" : "none";
const tooltip = ( 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" /> <div className="mx_Tooltip_chevron" />
{this.props.label} {this.props.label}
</div> </div>

View file

@ -92,7 +92,7 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
mx_ReactionsRowButton_selected: !!myReactionEvent, mx_ReactionsRowButton_selected: !!myReactionEvent,
}); });
let tooltip; let tooltip: JSX.Element | undefined;
if (this.state.tooltipRendered) { if (this.state.tooltipRendered) {
tooltip = ( tooltip = (
<ReactionsRowButtonTooltip <ReactionsRowButtonTooltip

View file

@ -40,7 +40,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
const { content, reactionEvents, mxEvent, visible } = this.props; const { content, reactionEvents, mxEvent, visible } = this.props;
const room = this.context.getRoom(mxEvent.getRoomId()); const room = this.context.getRoom(mxEvent.getRoomId());
let tooltipLabel; let tooltipLabel: JSX.Element | undefined;
if (room) { if (room) {
const senders: string[] = []; const senders: string[] = [];
for (const reactionEvent of reactionEvents) { 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) { if (tooltipLabel) {
tooltip = <Tooltip visible={visible} label={tooltipLabel} />; tooltip = <Tooltip visible={visible} label={tooltipLabel} />;
} }

View file

@ -1560,9 +1560,9 @@ export const UserInfoHeader: React.FC<{
</div> </div>
); );
let presenceState; let presenceState: string | undefined;
let presenceLastActiveAgo; let presenceLastActiveAgo: number | undefined;
let presenceCurrentlyActive; let presenceCurrentlyActive: boolean | undefined;
if (member instanceof RoomMember && member.user) { if (member instanceof RoomMember && member.user) {
presenceState = member.user.presence; presenceState = member.user.presence;
presenceLastActiveAgo = member.user.lastActiveAgo; presenceLastActiveAgo = member.user.lastActiveAgo;
@ -1597,10 +1597,10 @@ export const UserInfoHeader: React.FC<{
<div className="mx_UserInfo_profile"> <div className="mx_UserInfo_profile">
<div> <div>
<h2> <h2>
{e2eIcon}
<span title={displayName} aria-label={displayName} dir="auto"> <span title={displayName} aria-label={displayName} dir="auto">
{displayName} {displayName}
</span> </span>
{e2eIcon}
</h2> </h2>
</div> </div>
<div className="mx_UserInfo_profile_mxid"> <div className="mx_UserInfo_profile_mxid">

View file

@ -22,6 +22,7 @@ import { _t, _td } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import Tooltip, { Alignment } from "../elements/Tooltip"; import Tooltip, { Alignment } from "../elements/Tooltip";
import { E2EStatus } from "../../../utils/ShieldUtils"; import { E2EStatus } from "../../../utils/ShieldUtils";
import { XOR } from "../../../@types/common";
export enum E2EState { export enum E2EState {
Verified = "verified", Verified = "verified",
@ -42,9 +43,7 @@ const crossSigningRoomTitles: { [key in E2EState]?: string } = {
[E2EState.Verified]: _td("Everyone in this room is verified"), [E2EState.Verified]: _td("Everyone in this room is verified"),
}; };
interface IProps { interface Props {
isUser?: boolean;
status?: E2EState | E2EStatus;
className?: string; className?: string;
size?: number; size?: number;
onClick?: () => void; onClick?: () => void;
@ -53,7 +52,17 @@ interface IProps {
bordered?: boolean; 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, isUser,
status, status,
className, className,
@ -77,12 +86,10 @@ const E2EIcon: React.FC<IProps> = ({
); );
let e2eTitle: string | undefined; let e2eTitle: string | undefined;
if (status) { if (isUser) {
if (isUser) { e2eTitle = crossSigningUserTitles[status];
e2eTitle = crossSigningUserTitles[status]; } else {
} else { e2eTitle = crossSigningRoomTitles[status];
e2eTitle = crossSigningRoomTitles[status];
}
} }
let style: CSSProperties | undefined; let style: CSSProperties | undefined;
@ -93,9 +100,11 @@ const E2EIcon: React.FC<IProps> = ({
const onMouseOver = (): void => setHover(true); const onMouseOver = (): void => setHover(true);
const onMouseLeave = (): void => setHover(false); const onMouseLeave = (): void => setHover(false);
const label = e2eTitle ? _t(e2eTitle) : "";
let tip: JSX.Element | undefined; let tip: JSX.Element | undefined;
if (hover && !hideTooltip) { if (hover && !hideTooltip && label) {
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} alignment={tooltipAlignment} />; tip = <Tooltip label={label} alignment={tooltipAlignment} />;
} }
if (onClick) { if (onClick) {
@ -106,6 +115,7 @@ const E2EIcon: React.FC<IProps> = ({
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
className={classes} className={classes}
style={style} style={style}
aria-label={label}
> >
{tip} {tip}
</AccessibleButton> </AccessibleButton>
@ -113,7 +123,7 @@ const E2EIcon: React.FC<IProps> = ({
} }
return ( return (
<div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style}> <div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style} aria-label={label}>
{tip} {tip}
</div> </div>
); );

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 classNames from "classnames";
import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event"; import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
import { EventStatus, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/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}`; const classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
return ( 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} {tooltip}
</div> </div>
); );
@ -1525,6 +1530,7 @@ interface ISentReceiptProps {
} }
function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element { function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
const tooltipId = useRef(`mx_SentReceipt_${Math.random()}`).current;
const isSent = !messageState || messageState === "sent"; const isSent = !messageState || messageState === "sent";
const isFailed = messageState === "not_sent"; const isFailed = messageState === "not_sent";
const receiptClasses = classNames({ const receiptClasses = classNames({
@ -1546,6 +1552,7 @@ function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
label = _t("Failed to send"); label = _t("Failed to send");
} }
const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({ const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({
id: tooltipId,
label: label, label: label,
alignment: Alignment.TopRight, alignment: Alignment.TopRight,
}); });
@ -1559,6 +1566,7 @@ function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element {
onMouseLeave={hideTooltip} onMouseLeave={hideTooltip}
onFocus={showTooltip} onFocus={showTooltip}
onBlur={hideTooltip} onBlur={hideTooltip}
aria-describedby={tooltipId}
> >
<span className="mx_ReadReceiptGroup_container"> <span className="mx_ReadReceiptGroup_container">
<span className={receiptClasses}>{nonCssBadge}</span> <span className={receiptClasses}>{nonCssBadge}</span>

View file

@ -107,6 +107,7 @@ interface IState {
} }
export class MessageComposer extends React.Component<IProps, IState> { export class MessageComposer extends React.Component<IProps, IState> {
private tooltipId = `mx_MessageComposer_${Math.random()}`;
private dispatcherRef?: string; private dispatcherRef?: string;
private messageComposerInput = createRef<SendMessageComposerClass>(); private messageComposerInput = createRef<SendMessageComposerClass>();
private voiceRecordingButton = createRef<VoiceRecordComposerTile>(); private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
@ -470,7 +471,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
public render(): React.ReactNode { public render(): React.ReactNode {
const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus); const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus);
const e2eIcon = hasE2EIcon && ( 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[] = []; 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) { if (this.state.recordingTimeLeftSeconds) {
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds); const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
recordingTooltip = ( 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 ( return (
<div className={classes} ref={this.ref}> <div
className={classes}
ref={this.ref}
aria-describedby={this.state.recordingTimeLeftSeconds ? this.tooltipId : undefined}
>
{recordingTooltip} {recordingTooltip}
<div className="mx_MessageComposer_wrapper"> <div className="mx_MessageComposer_wrapper">
<ReplyPreview <ReplyPreview

View file

@ -579,6 +579,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</div> </div>
</div> </div>
<div <div
aria-label="This room is end-to-end encrypted"
class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon" class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"
/> />
<div <div

View file

@ -117,6 +117,12 @@ describe("<Pill>", () => {
jest.spyOn(dis, "dispatch"); jest.spyOn(dis, "dispatch");
pillParentClickHandler = jest.fn(); pillParentClickHandler = jest.fn();
jest.spyOn(global.Math, "random").mockReturnValue(0.123456);
});
afterEach(() => {
jest.spyOn(global.Math, "random").mockRestore();
}); });
describe("when rendering a pill for a room", () => { describe("when rendering a pill for a room", () => {

View file

@ -11,6 +11,7 @@ exports[`<Pill> should not render an avatar or link when called with inMessage =
<div> <div>
<bdi> <bdi>
<span <span
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_RoomPill" class="mx_Pill mx_RoomPill"
> >
<span <span
@ -29,6 +30,7 @@ exports[`<Pill> should render the expected pill for @room 1`] = `
<div> <div>
<bdi> <bdi>
<span <span
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_AtRoomPill" class="mx_Pill mx_AtRoomPill"
> >
<span <span
@ -68,6 +70,7 @@ exports[`<Pill> should render the expected pill for a known user not in the room
<div> <div>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_UserPill" class="mx_Pill mx_UserPill"
href="https://matrix.to/#/@user2:example.com" href="https://matrix.to/#/@user2:example.com"
> >
@ -108,6 +111,7 @@ exports[`<Pill> should render the expected pill for a message in another room 1`
<div> <div>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_EventPill" class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room1:example.com/$123-456" href="https://matrix.to/#/!room1:example.com/$123-456"
> >
@ -148,6 +152,7 @@ exports[`<Pill> should render the expected pill for a message in the same room 1
<div> <div>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_EventPill" class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room1:example.com/$123-456" href="https://matrix.to/#/!room1:example.com/$123-456"
> >
@ -188,6 +193,7 @@ exports[`<Pill> should render the expected pill for a room alias 1`] = `
<div> <div>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_RoomPill" class="mx_Pill mx_RoomPill"
href="https://matrix.to/#/#room1:example.com" href="https://matrix.to/#/#room1:example.com"
> >
@ -228,6 +234,7 @@ exports[`<Pill> should render the expected pill for a space 1`] = `
<div> <div>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_RoomPill" class="mx_Pill mx_RoomPill"
href="https://matrix.to/#/!space1:example.com" href="https://matrix.to/#/!space1:example.com"
> >
@ -268,6 +275,7 @@ exports[`<Pill> should render the expected pill for an uknown user not in the ro
<div> <div>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_UserPill" class="mx_Pill mx_UserPill"
href="https://matrix.to/#/@user3:example.com" href="https://matrix.to/#/@user3:example.com"
> >
@ -290,6 +298,7 @@ exports[`<Pill> when rendering a pill for a room should render the expected pill
<div> <div>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_RoomPill" class="mx_Pill mx_RoomPill"
href="https://matrix.to/#/!room1:example.com" href="https://matrix.to/#/!room1:example.com"
> >
@ -330,6 +339,7 @@ exports[`<Pill> when rendering a pill for a user in the room should render as ex
<div> <div>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_UserPill" class="mx_Pill mx_UserPill"
href="https://matrix.to/#/@user1:example.com" href="https://matrix.to/#/@user1:example.com"
> >

View file

@ -3,6 +3,7 @@
exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = ` exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = `
<div <div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible" class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
id="test id"
role="tooltip" role="tooltip"
style="display: block; top: 6px; transform: translate(max(10px, min(calc(0px - 50%), calc(100vw - 100% - 10px))));" style="display: block; top: 6px; transform: translate(max(10px, min(calc(0px - 50%), calc(100vw - 100% - 10px))));"
> >
@ -16,6 +17,7 @@ exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = `
exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`] = ` exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`] = `
<div <div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible" class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
id="test id"
role="tooltip" role="tooltip"
style="display: block; top: -50px; transform: translate(max(10px, min(calc(0px - 50%), calc(100vw - 100% - 10px))));" style="display: block; top: -50px; transform: translate(max(10px, min(calc(0px - 50%), calc(100vw - 100% - 10px))));"
> >
@ -29,6 +31,7 @@ exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`]
exports[`<TooltipTarget /> displays Left aligned tooltip on mouseover 1`] = ` exports[`<TooltipTarget /> displays Left aligned tooltip on mouseover 1`] = `
<div <div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible" class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
id="test id"
role="tooltip" role="tooltip"
style="display: block; right: 1030px; top: 0px; transform: translateY(-50%);" style="display: block; right: 1030px; top: 0px; transform: translateY(-50%);"
> >
@ -42,6 +45,7 @@ exports[`<TooltipTarget /> displays Left aligned tooltip on mouseover 1`] = `
exports[`<TooltipTarget /> displays Natural aligned tooltip on mouseover 1`] = ` exports[`<TooltipTarget /> displays Natural aligned tooltip on mouseover 1`] = `
<div <div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible" class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
id="test id"
role="tooltip" role="tooltip"
style="display: block; left: 6px; top: 0px; transform: translateY(-50%);" style="display: block; left: 6px; top: 0px; transform: translateY(-50%);"
> >
@ -55,6 +59,7 @@ exports[`<TooltipTarget /> displays Natural aligned tooltip on mouseover 1`] = `
exports[`<TooltipTarget /> displays Right aligned tooltip on mouseover 1`] = ` exports[`<TooltipTarget /> displays Right aligned tooltip on mouseover 1`] = `
<div <div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible" class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
id="test id"
role="tooltip" role="tooltip"
style="display: block; left: 6px; top: 0px; transform: translateY(-50%);" style="display: block; left: 6px; top: 0px; transform: translateY(-50%);"
> >
@ -68,6 +73,7 @@ exports[`<TooltipTarget /> displays Right aligned tooltip on mouseover 1`] = `
exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = ` exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = `
<div <div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible" class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
id="test id"
role="tooltip" role="tooltip"
style="display: block; top: -6px; transform: translate(max(10px, min(calc(0px - 50%), calc(100vw - 100% - 10px))), -100%);" style="display: block; top: -6px; transform: translate(max(10px, min(calc(0px - 50%), calc(100vw - 100% - 10px))), -100%);"
> >
@ -81,6 +87,7 @@ exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = `
exports[`<TooltipTarget /> displays TopRight aligned tooltip on mouseover 1`] = ` exports[`<TooltipTarget /> displays TopRight aligned tooltip on mouseover 1`] = `
<div <div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible" class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
id="test id"
role="tooltip" role="tooltip"
style="display: block; top: -6px; right: 1024px; transform: translateY(-100%);" style="display: block; top: -6px; right: 1024px; transform: translateY(-100%);"
> >

View file

@ -60,6 +60,7 @@ const mkFormattedMessage = (body: string, formattedBody: string): MatrixEvent =>
describe("<TextualBody />", () => { describe("<TextualBody />", () => {
afterEach(() => { afterEach(() => {
jest.spyOn(MatrixClientPeg, "get").mockRestore(); jest.spyOn(MatrixClientPeg, "get").mockRestore();
jest.spyOn(global.Math, "random").mockRestore();
}); });
const defaultRoom = mkStubRoom(room1Id, "test room", undefined); const defaultRoom = mkStubRoom(room1Id, "test room", undefined);
@ -98,6 +99,7 @@ describe("<TextualBody />", () => {
if (eventId === defaultEvent.getId()) return defaultEvent; if (eventId === defaultEvent.getId()) return defaultEvent;
return undefined; return undefined;
}); });
jest.spyOn(global.Math, "random").mockReturnValue(0.123456);
}); });
const defaultProps = { const defaultProps = {
@ -197,7 +199,7 @@ describe("<TextualBody />", () => {
const { container } = getComponent({ mxEvent: ev }); const { container } = getComponent({ mxEvent: ev });
const content = container.querySelector(".mx_EventTile_body"); const content = container.querySelector(".mx_EventTile_body");
expect(content.innerHTML).toMatchInlineSnapshot( expect(content.innerHTML).toMatchInlineSnapshot(
`"Chat with <span><bdi><a class="mx_Pill mx_UserPill mx_UserPill_me" href="https://matrix.to/#/@user:example.com"><img class="mx_BaseAvatar mx_BaseAvatar_image" src="mxc://avatar.url/image.png" style="width: 16px; height: 16px;" alt="" data-testid="avatar-img" aria-hidden="true"><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" aria-describedby="mx_Pill_0.123456"><img class="mx_BaseAvatar mx_BaseAvatar_image" src="mxc://avatar.url/image.png" style="width: 16px; height: 16px;" alt="" data-testid="avatar-img" aria-hidden="true"><span class="mx_Pill_text">Member</span></a></bdi></span>"`,
); );
}); });
@ -215,7 +217,7 @@ describe("<TextualBody />", () => {
const { container } = getComponent({ mxEvent: ev }); const { container } = getComponent({ mxEvent: ev });
const content = container.querySelector(".mx_EventTile_body"); const content = container.querySelector(".mx_EventTile_body");
expect(content.innerHTML).toMatchInlineSnapshot( expect(content.innerHTML).toMatchInlineSnapshot(
`"Visit <span><bdi><a class="mx_Pill mx_RoomPill" href="https://matrix.to/#/#room:example.com"><div class="mx_Pill_LinkIcon mx_BaseAvatar mx_BaseAvatar_image"></div><span class="mx_Pill_text">#room:example.com</span></a></bdi></span>"`, `"Visit <span><bdi><a class="mx_Pill mx_RoomPill" href="https://matrix.to/#/#room:example.com" aria-describedby="mx_Pill_0.123456"><div class="mx_Pill_LinkIcon mx_BaseAvatar mx_BaseAvatar_image"></div><span class="mx_Pill_text">#room:example.com</span></a></bdi></span>"`,
); );
}); });

View file

@ -50,6 +50,7 @@ exports[`<TextualBody /> renders formatted m.text correctly pills appear for an
<span> <span>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_UserPill" class="mx_Pill mx_UserPill"
href="https://matrix.to/#/@user:example.com" href="https://matrix.to/#/@user:example.com"
> >
@ -85,6 +86,7 @@ exports[`<TextualBody /> renders formatted m.text correctly pills appear for eve
<span> <span>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_EventPill" class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com" href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com"
> >
@ -122,6 +124,7 @@ exports[`<TextualBody /> renders formatted m.text correctly pills appear for roo
<span> <span>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_RoomPill" class="mx_Pill mx_RoomPill"
href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com?via=example.com&via=bob.com" href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com?via=example.com&via=bob.com"
> >
@ -218,6 +221,7 @@ exports[`<TextualBody /> renders formatted m.text correctly pills get injected c
<span> <span>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_UserPill" class="mx_Pill mx_UserPill"
href="https://matrix.to/#/@user:server" href="https://matrix.to/#/@user:server"
> >
@ -247,6 +251,7 @@ exports[`<TextualBody /> renders plain-text m.text correctly should pillify a pe
><a ><a
class="mx_Pill mx_EventPill" class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room1:example.com/%event_id%" href="https://matrix.to/#/!room1:example.com/%event_id%"
aria-describedby="mx_Pill_0.123456"
><img ><img
class="mx_BaseAvatar mx_BaseAvatar_image" class="mx_BaseAvatar mx_BaseAvatar_image"
src="mxc://avatar.url/image.png" src="mxc://avatar.url/image.png"
@ -268,6 +273,7 @@ exports[`<TextualBody /> renders plain-text m.text correctly should pillify a pe
><a ><a
class="mx_Pill mx_EventPill" class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room2:example.com/%event_id%" href="https://matrix.to/#/!room2:example.com/%event_id%"
aria-describedby="mx_Pill_0.123456"
><img ><img
class="mx_BaseAvatar mx_BaseAvatar_image" class="mx_BaseAvatar mx_BaseAvatar_image"
src="mxc://avatar.url/room.png" src="mxc://avatar.url/room.png"
@ -291,6 +297,7 @@ exports[`<TextualBody /> renders plain-text m.text correctly should pillify a pe
<span> <span>
<bdi> <bdi>
<a <a
aria-describedby="mx_Pill_0.123456"
class="mx_Pill mx_EventPill" class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room1:example.com/!abc123" href="https://matrix.to/#/!room1:example.com/!abc123"
> >

View file

@ -322,7 +322,7 @@ describe("<UserInfo />", () => {
renderComponent({ room: mockRoom }); renderComponent({ room: mockRoom });
await act(flushPromises); await act(flushPromises);
const userHeading = screen.getByRole("heading", { name: defaultUserId }); const userHeading = screen.getByRole("heading", { name: /@user:example.com/ });
// there should be a "normal" E2E padlock // there should be a "normal" E2E padlock
expect(userHeading.getElementsByClassName("mx_E2EIcon_normal")).toHaveLength(1); expect(userHeading.getElementsByClassName("mx_E2EIcon_normal")).toHaveLength(1);
@ -333,7 +333,7 @@ describe("<UserInfo />", () => {
renderComponent({ room: mockRoom }); renderComponent({ room: mockRoom });
await act(flushPromises); await act(flushPromises);
const userHeading = screen.getByRole("heading", { name: defaultUserId }); const userHeading = screen.getByRole("heading", { name: /@user:example.com/ });
// there should be a "verified" E2E padlock // there should be a "verified" E2E padlock
expect(userHeading.getElementsByClassName("mx_E2EIcon_verified")).toHaveLength(1); expect(userHeading.getElementsByClassName("mx_E2EIcon_verified")).toHaveLength(1);
@ -368,7 +368,7 @@ describe("<UserInfoHeader />", () => {
it("renders an e2e icon in the header if e2eStatus prop is defined", () => { it("renders an e2e icon in the header if e2eStatus prop is defined", () => {
renderComponent({ e2eStatus: E2EStatus.Normal }); renderComponent({ e2eStatus: E2EStatus.Normal });
const header = screen.getByRole("heading", { name: defaultUserId }); const header = screen.getByRole("heading");
expect(header.getElementsByClassName("mx_E2EIcon")).toHaveLength(1); expect(header.getElementsByClassName("mx_E2EIcon")).toHaveLength(1);
}); });

View file

@ -40,6 +40,7 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
/> />
</span> </span>
<div <div
aria-label="Not encrypted"
class="mx_TextWithTooltip_target mx_RoomSummaryCard_e2ee" class="mx_TextWithTooltip_target mx_RoomSummaryCard_e2ee"
tabindex="0" tabindex="0"
/> />