diff --git a/res/css/views/right_panel/_UserInfo.pcss b/res/css/views/right_panel/_UserInfo.pcss index fd017d8a07c..1287e74f067 100644 --- a/res/css/views/right_panel/_UserInfo.pcss +++ b/res/css/views/right_panel/_UserInfo.pcss @@ -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 */ diff --git a/src/components/structures/auth/forgot-password/CheckEmail.tsx b/src/components/structures/auth/forgot-password/CheckEmail.tsx index be976e4e58f..7e4679a1dcf 100644 --- a/src/components/structures/auth/forgot-password/CheckEmail.tsx +++ b/src/components/structures/auth/forgot-password/CheckEmail.tsx @@ -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 = ({ onSubmitForm, onResendClick, }) => { + const tooltipId = useRef(`mx_CheckEmail_${Math.random()}`).current; const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500); const onResendClickFn = async (): Promise => { @@ -68,10 +69,16 @@ export const CheckEmail: React.FC = ({
{_t("Did not receive it?")} - + {_t("Resend")} = ({ onReEnterEmailClick, onResendClick, }) => { + const tooltipId = useRef(`mx_VerifyEmailModal_${Math.random()}`).current; const { toggle: toggleTooltipVisible, value: tooltipVisible } = useTimeoutToggle(false, 2500); const onResendClickFn = async (): Promise => { @@ -66,10 +67,16 @@ export const VerifyEmailModal: React.FC = ({
{_t("Did not receive it?")} - + {_t("Resend")} = ({ 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 = ({ device, user, onFinished }) = className="mx_UntrustedDeviceDialog" title={ <> - + {_t("Not Trusted")} } diff --git a/src/components/views/elements/LinkWithTooltip.tsx b/src/components/views/elements/LinkWithTooltip.tsx index 88d29e4a861..854f01e0e57 100644 --- a/src/components/views/elements/LinkWithTooltip.tsx +++ b/src/components/views/elements/LinkWithTooltip.tsx @@ -18,7 +18,9 @@ import React from "react"; import TextWithTooltip from "./TextWithTooltip"; -interface IProps extends Omit, "tabIndex" | "onClick"> {} +interface IProps extends Omit, "tabIndex" | "onClick" | "tooltip"> { + tooltip: string; +} export default class LinkWithTooltip extends React.Component { public constructor(props: IProps) { diff --git a/src/components/views/elements/Pill.tsx b/src/components/views/elements/Pill.tsx index 965205e71fe..49c6c1bfd2d 100644 --- a/src/components/views/elements/Pill.tsx +++ b/src/components/views/elements/Pill.tsx @@ -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 = ({ 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 = ({ type: propType, url, inMessage, room setHover(false); }; - const tip = hover && resourceId ? : null; + const tip = hover && resourceId ? : null; let avatar: ReactElement | null = null; let pillText: string | null = text; @@ -165,13 +166,19 @@ export const Pill: React.FC = ({ type: propType, url, inMessage, room onClick={onClick} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} + aria-describedby={tooltipId} > {avatar} {pillText} {tip} ) : ( - + {avatar} {pillText} {tip} diff --git a/src/components/views/elements/TextWithTooltip.tsx b/src/components/views/elements/TextWithTooltip.tsx index 3ebdac55113..a72cd82faa2 100644 --- a/src/components/views/elements/TextWithTooltip.tsx +++ b/src/components/views/elements/TextWithTooltip.tsx @@ -35,6 +35,10 @@ export default class TextWithTooltip extends React.Component { public render(): React.ReactNode { const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props; + if (typeof tooltip === "string") { + props["aria-label"] = tooltip; + } + return ( { style.display = this.props.visible ? "block" : "none"; const tooltip = ( -
+
{this.props.label}
diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx index 3b165b9a61f..8dd144eff68 100644 --- a/src/components/views/messages/ReactionsRowButton.tsx +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -92,7 +92,7 @@ export default class ReactionsRowButton extends React.PureComponent; } diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index b87c8ab76c1..99498c1fe9d 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -1560,9 +1560,9 @@ export const UserInfoHeader: React.FC<{
); - 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<{

- {e2eIcon} {displayName} + {e2eIcon}

diff --git a/src/components/views/rooms/E2EIcon.tsx b/src/components/views/rooms/E2EIcon.tsx index 0103c308ddd..01a6b8a4985 100644 --- a/src/components/views/rooms/E2EIcon.tsx +++ b/src/components/views/rooms/E2EIcon.tsx @@ -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 = ({ +interface UserProps extends Props { + isUser: true; + status: E2EState | E2EStatus; +} + +interface RoomProps extends Props { + isUser?: false; + status: E2EStatus; +} + +const E2EIcon: React.FC> = ({ isUser, status, className, @@ -77,12 +86,10 @@ const E2EIcon: React.FC = ({ ); 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 = ({ const onMouseOver = (): void => setHover(true); const onMouseLeave = (): void => setHover(false); + const label = e2eTitle ? _t(e2eTitle) : ""; + let tip: JSX.Element | undefined; - if (hover && !hideTooltip) { - tip = ; + if (hover && !hideTooltip && label) { + tip = ; } if (onClick) { @@ -106,6 +115,7 @@ const E2EIcon: React.FC = ({ onMouseLeave={onMouseLeave} className={classes} style={style} + aria-label={label} > {tip} @@ -113,7 +123,7 @@ const E2EIcon: React.FC = ({ } return ( -
+
{tip}
); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index a6393b7c825..c822d8c9980 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -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 { const classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`; return ( -
+
{tooltip}
); @@ -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} > {nonCssBadge} diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index bf5f6b42bb7..35728598b63 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -106,6 +106,7 @@ interface IState { } export class MessageComposer extends React.Component { + private tooltipId = `mx_MessageComposer_${Math.random()}`; private dispatcherRef?: string; private messageComposerInput = createRef(); private voiceRecordingButton = createRef(); @@ -469,7 +470,7 @@ export class MessageComposer extends React.Component { public render(): React.ReactNode { const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus); const e2eIcon = hasE2EIcon && ( - + ); const controls: ReactNode[] = []; @@ -560,11 +561,15 @@ export class MessageComposer extends React.Component { ); } - let recordingTooltip; + let recordingTooltip: JSX.Element | undefined; if (this.state.recordingTimeLeftSeconds) { const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds); recordingTooltip = ( - + ); } @@ -592,7 +597,11 @@ export class MessageComposer extends React.Component { }); return ( -
+
{recordingTooltip}
", () => { jest.spyOn(dis, "dispatch"); 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", () => { diff --git a/test/components/views/elements/__snapshots__/Pill-test.tsx.snap b/test/components/views/elements/__snapshots__/Pill-test.tsx.snap index 8b945b0c159..6644fea7cb0 100644 --- a/test/components/views/elements/__snapshots__/Pill-test.tsx.snap +++ b/test/components/views/elements/__snapshots__/Pill-test.tsx.snap @@ -11,6 +11,7 @@ exports[` should not render an avatar or link when called with inMessage =
should render the expected pill for @room 1`] = `
should render the expected pill for a known user not in the room
@@ -108,6 +111,7 @@ exports[` should render the expected pill for a message in another room 1`
@@ -148,6 +152,7 @@ exports[` should render the expected pill for a message in the same room 1
@@ -188,6 +193,7 @@ exports[` should render the expected pill for a room alias 1`] = `
@@ -228,6 +234,7 @@ exports[` should render the expected pill for a space 1`] = `
@@ -268,6 +275,7 @@ exports[` should render the expected pill for an uknown user not in the ro
@@ -290,6 +298,7 @@ exports[` when rendering a pill for a room should render the expected pill
@@ -330,6 +339,7 @@ exports[` when rendering a pill for a user in the room should render as ex
diff --git a/test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap b/test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap index bcfbe5effd0..ad5f3791653 100644 --- a/test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap +++ b/test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap @@ -3,6 +3,7 @@ exports[` displays Bottom aligned tooltip on mouseover 1`] = `