Improve icon rendering accessibility (#31791)

* Switch to rendered svg for Field select decoration instead of SVG mask

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Replace warning.svg with Compound icon

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Replace device kind icons with Compound

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Draw notification badge decoration using SVG rather than CSS masks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Replace {collapse,expand}-message icons with Compound

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove stale icon prefetch

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Render icons in AddExistingToSpaceDialog using SVGs rather than CSS masks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Render icons in Jitsi landing page using SVGs rather than CSS masks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update screenshot

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix field label

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Revert icon colour

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering icons as SVG over CSS masks in PollOption

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering icons as SVG over CSS masks in AnalyticsLearnMoreDialog

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unused class

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering icons as SVG over CSS masks in customisedCancelButton mixin

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering icons as SVG over CSS masks in ThreadSummaryIcon mixin

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Switch to rendering icons as SVG over CSS masks in LegacyCallButton mixin

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unused classes in TabbedView

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update snapshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix `[Object object]`

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix layout issue

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2026-01-21 13:33:58 +00:00
committed by GitHub
parent edb63922e0
commit de9a52d046
55 changed files with 1179 additions and 355 deletions

View File

@@ -93,11 +93,7 @@ function TabLabel<T extends string>({ tab, isActive, showToolip, onClick }: ITab
let tabIcon: JSX.Element | undefined;
if (tab.icon) {
if (typeof tab.icon === "object") {
tabIcon = tab.icon;
} else if (typeof tab.icon === "string") {
tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />;
}
tabIcon = tab.icon;
}
const id = domIDForTabID(tab.id);

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { Glass } from "@vector-im/compound-web";
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler";
import { Phase, SetupEncryptionStore } from "../../../stores/SetupEncryptionStore";
@@ -92,7 +93,9 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
onClick={this.onSkipClick}
className="mx_CompleteSecurity_skip"
aria-label={_t("encryption|verification|after_new_login|skip_verification")}
/>
>
<CloseIcon />
</AccessibleButton>
);
}

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type ReactNode } from "react";
import { Tooltip } from "@vector-im/compound-web";
import { RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { CloseIcon, RestartIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../../languageHandler";
import AccessibleButton from "../../../views/elements/AccessibleButton";
@@ -77,7 +77,9 @@ export const VerifyEmailModal: React.FC<Props> = ({
onClick={onCloseClick}
className="mx_Dialog_cancelButton"
aria-label={_t("dialog_close_label")}
/>
>
<CloseIcon />
</AccessibleButton>
</>
);
};

View File

@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
import React, { createRef } from "react";
import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import ContextMenu, { type IProps as IContextMenuProps } from "../../structures/ContextMenu";
@@ -65,7 +66,9 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
<ContextMenu {...this.props}>
<div className="mx_DialPadContextMenuWrapper">
<div>
<AccessibleButton className="mx_DialPadContextMenu_cancel" onClick={this.onCancelClick} />
<AccessibleButton className="mx_DialPadContextMenu_cancel" onClick={this.onCancelClick}>
<CloseIcon />
</AccessibleButton>
</div>
<div className="mx_DialPadContextMenu_header">
<Field

View File

@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { CheckCircleIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
@@ -72,9 +73,18 @@ export const AnalyticsLearnMoreDialog: React.FC<IProps> = ({
{_t("analytics|pseudonymous_usage_data", { analyticsOwner })}
</div>
<ul className="mx_AnalyticsLearnMore_bullets">
<li>{_t("analytics|bullet_1", {}, { Bold: (sub) => <strong>{sub}</strong> })}</li>
<li>{_t("analytics|bullet_2", {}, { Bold: (sub) => <strong>{sub}</strong> })}</li>
<li>{_t("analytics|disable_prompt")}</li>
<li>
<CheckCircleIcon />
{_t("analytics|bullet_1", {}, { Bold: (sub) => <strong>{sub}</strong> })}
</li>
<li>
<CheckCircleIcon />
{_t("analytics|bullet_2", {}, { Bold: (sub) => <strong>{sub}</strong> })}
</li>
<li>
<CheckCircleIcon />
{_t("analytics|disable_prompt")}
</li>
</ul>
{privacyPolicyLink}
</div>

View File

@@ -13,6 +13,7 @@ import FocusLock from "react-focus-lock";
import classNames from "classnames";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { I18nContext } from "@element-hq/web-shared-components";
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@@ -134,7 +135,9 @@ export default class BaseDialog extends React.Component<IProps> {
title={_t("action|close")}
aria-label={_t("dialog_close_label")}
placement="bottom"
/>
>
<CloseIcon />
</AccessibleButton>
);
}

View File

@@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type FormEvent } from "react";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import FocusLock from "react-focus-lock";
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@@ -91,7 +92,9 @@ export default abstract class ScrollableBaseModal<
onClick={this.onCancel}
className="mx_CompoundDialog_cancelButton"
aria-label={_t("dialog_close_label")}
/>
>
<CloseIcon />
</AccessibleButton>
<form onSubmit={this.onSubmit} className="mx_CompoundDialog_form">
<div className="mx_CompoundDialog_content">{this.renderContent()}</div>
<div className="mx_CompoundDialog_footer">

View File

@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, type ChangeEvent } from "react";
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler";
import Field from "./Field";
@@ -74,12 +75,14 @@ export class EditableItem extends React.Component<IItemProps, IItemState> {
return (
<div className="mx_EditableItem">
<div
<AccessibleButton
onClick={this.onRemove}
className="mx_EditableItem_delete"
title={_t("action|remove")}
role="button"
/>
>
<CloseIcon />
</AccessibleButton>
<span className="mx_EditableItem_item">{this.props.value}</span>
</div>
);

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type Ref } from "react";
import React, { type JSX, type Ref } from "react";
import classnames from "classnames";
interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
@@ -16,6 +16,11 @@ interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
// If false, they'll be in a div. Putting interactive components that have labels
// themselves in labels can cause strange bugs like https://github.com/vector-im/element-web/issues/18031
childrenInLabel?: boolean;
/**
* If provided will override the default dot icon drawn for checked state
*/
icon?: JSX.Element;
}
export default class StyledRadioButton extends React.PureComponent<IProps> {
@@ -25,7 +30,7 @@ export default class StyledRadioButton extends React.PureComponent<IProps> {
};
public render(): React.ReactNode {
const { children, className, disabled, outlined, childrenInLabel, inputRef, ...otherProps } = this.props;
const { children, className, disabled, outlined, childrenInLabel, inputRef, icon, ...otherProps } = this.props;
const _className = classnames("mx_StyledRadioButton", className, {
mx_StyledRadioButton_disabled: disabled,
mx_StyledRadioButton_enabled: !disabled,
@@ -42,9 +47,9 @@ export default class StyledRadioButton extends React.PureComponent<IProps> {
disabled={disabled}
{...otherProps}
/>
{/* Used to render the radio button circle */}
<div>
<div />
{/* Empty div is used to render the radio button circle */}
<div>{icon}</div>
</div>
</React.Fragment>
);

View File

@@ -6,12 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, createRef } from "react";
import React, { createRef, type JSX } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { CallErrorCode, CallState } from "matrix-js-sdk/src/webrtc/call";
import classNames from "classnames";
import { Clock } from "@element-hq/web-shared-components";
import { VolumeOffSolidIcon, VolumeOnSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import {
EndCallIcon,
VideoCallDeclinedSolidIcon,
VideoCallMissedSolidIcon,
VideoCallSolidIcon,
VoiceCallMissedSolidIcon,
VoiceCallSolidIcon,
VolumeOffSolidIcon,
VolumeOnSolidIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler";
import MemberAvatar from "../avatars/MemberAvatar";
@@ -36,6 +45,17 @@ interface IState {
length: number;
}
export function getCallStateIcon(isVoice: boolean, state: undefined | "missed" | "declined"): JSX.Element {
let icon = isVoice ? <VoiceCallSolidIcon /> : <VideoCallSolidIcon />;
if (state === "missed") {
icon = isVoice ? <VoiceCallMissedSolidIcon /> : <VideoCallMissedSolidIcon />;
} else if (state === "declined") {
icon = isVoice ? <EndCallIcon /> : <VideoCallDeclinedSolidIcon />;
}
return <div className="mx_LegacyCallEvent_type_icon">{icon}</div>;
}
export default class LegacyCallEvent extends React.PureComponent<IProps, IState> {
private wrapperElement = createRef<HTMLDivElement>();
private resizeObserver?: ResizeObserver;
@@ -90,11 +110,12 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
private renderCallBackButton(text: string): JSX.Element {
return (
<AccessibleButton
className="mx_LegacyCallEvent_content_button mx_LegacyCallEvent_content_button_callBack"
className="mx_LegacyCallEvent_content_button"
onClick={this.props.callEventGrouper.callBack}
kind="primary"
>
<span> {text} </span>
{this.props.callEventGrouper.isVoice ? <VoiceCallSolidIcon /> : <VideoCallSolidIcon />}
{text}
</AccessibleButton>
);
}
@@ -122,18 +143,20 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
<div className="mx_LegacyCallEvent_content">
{silenceIcon}
<AccessibleButton
className="mx_LegacyCallEvent_content_button mx_LegacyCallEvent_content_button_reject"
className="mx_LegacyCallEvent_content_button"
onClick={this.props.callEventGrouper.rejectCall}
kind="danger"
>
<span> {_t("action|decline")} </span>
<EndCallIcon />
{_t("action|decline")}
</AccessibleButton>
<AccessibleButton
className="mx_LegacyCallEvent_content_button mx_LegacyCallEvent_content_button_answer"
className="mx_LegacyCallEvent_content_button"
onClick={this.props.callEventGrouper.answerCall}
kind="primary"
>
<span> {_t("action|accept")} </span>
{this.props.callEventGrouper.isVoice ? <VoiceCallSolidIcon /> : <VideoCallSolidIcon />}
{_t("action|accept")}
</AccessibleButton>
{this.props.timestamp}
</div>
@@ -265,15 +288,23 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
mx_LegacyCallEvent_voice: isVoice,
mx_LegacyCallEvent_video: !isVoice,
mx_LegacyCallEvent_narrow: this.state.narrow,
mx_LegacyCallEvent_missed: this.props.callEventGrouper.callWasMissed,
mx_LegacyCallEvent_noAnswer: callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout,
mx_LegacyCallEvent_rejected: callState === CallState.Ended && this.props.callEventGrouper.gotRejected,
});
let silenceIcon;
if (this.state.narrow && this.state.callState === CallState.Ringing) {
silenceIcon = this.renderSilenceIcon();
}
let iconState: Parameters<typeof getCallStateIcon>[1] = undefined;
if (this.props.callEventGrouper.callWasMissed) {
iconState = "missed";
} else if (
callState === CallState.Ended &&
(hangupReason === CallErrorCode.InviteTimeout || this.props.callEventGrouper.gotRejected)
) {
iconState = "declined";
}
return (
<div className="mx_LegacyCallEvent_wrapper" ref={this.wrapperElement}>
<div className={className}>
@@ -283,7 +314,7 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState>
<div className="mx_LegacyCallEvent_info_basic">
<div className="mx_LegacyCallEvent_sender">{sender}</div>
<div className="mx_LegacyCallEvent_type">
<div className="mx_LegacyCallEvent_type_icon" />
{getCallStateIcon(!!isVoice, iconState)}
{callType}
</div>
</div>

View File

@@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type ReactNode } from "react";
import classNames from "classnames";
import { type PollAnswerSubevent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
import { CheckIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { _t } from "../../../languageHandler";
import { Icon as TrophyIcon } from "../../../../res/img/element-icons/trophy.svg";
@@ -85,6 +86,7 @@ const ActivePollOption: React.FC<Omit<PollOptionProps, "totalVoteCount"> & { chi
disabled={isEnded}
aria-label={ariaLabel}
onChange={() => onOptionSelected?.(answer.id)}
icon={isChecked ? <CheckIcon /> : undefined}
>
<div aria-hidden="true">{children}</div>
</StyledRadioButton>

View File

@@ -35,7 +35,7 @@ import {
} from "matrix-js-sdk/src/crypto-api";
import { Tooltip } from "@vector-im/compound-web";
import { uniqueId } from "lodash";
import { CircleIcon, CheckCircleIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { CircleIcon, CheckCircleIcon, ThreadsIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import ReplyChain from "../elements/ReplyChain";
import { _t } from "../../../languageHandler";
@@ -498,6 +498,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
return (
<div className="mx_ThreadPanel_replies">
<ThreadsIcon />
<span className="mx_ThreadPanel_replies_amount">{this.state.thread.length}</span>
<ThreadMessagePreview thread={this.state.thread} />
</div>
@@ -515,12 +516,18 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
if (this.props.highlightLink) {
return (
<a className="mx_ThreadSummary_icon" href={this.props.highlightLink}>
<ThreadsIcon />
{_t("timeline|thread_info_basic")}
</a>
);
}
return <p className="mx_ThreadSummary_icon">{_t("timeline|thread_info_basic")}</p>;
return (
<p className="mx_ThreadSummary_icon">
<ThreadsIcon />
{_t("timeline|thread_info_basic")}
</p>
);
}
}

View File

@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { type ChangeEvent, createRef, type SyntheticEvent } from "react";
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton";
import Field from "../elements/Field";
@@ -106,7 +107,9 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
return (
<div className="mx_DialPadModal">
<div>
<AccessibleButton className="mx_DialPadModal_cancel" onClick={this.onCancelClick} />
<AccessibleButton className="mx_DialPadModal_cancel" onClick={this.onCancelClick}>
<CloseIcon />
</AccessibleButton>
</div>
<div className="mx_DialPadModal_header">
<form onSubmit={this.onFormSubmit}>{dialPadField}</form>

View File

@@ -11,14 +11,20 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { CallType, type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import classNames from "classnames";
import { VolumeOffSolidIcon, VolumeOnSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import {
EndCallIcon,
VideoCallSolidIcon,
VoiceCallSolidIcon,
VolumeOffSolidIcon,
VolumeOnSolidIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../LegacyCallHandler";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { _t } from "../languageHandler";
import RoomAvatar from "../components/views/avatars/RoomAvatar";
import AccessibleButton, { type ButtonEvent } from "../components/views/elements/AccessibleButton";
import { getCallStateIcon } from "../components/views/messages/LegacyCallEvent.tsx";
export const getIncomingLegacyCallToastKey = (callId: string): string => `call_${callId}`;
@@ -95,34 +101,31 @@ export default class IncomingLegacyCallToast extends React.Component<IProps, ISt
silenceButtonTooltip = _t("voip|silenced");
}
const contentClass = classNames("mx_IncomingLegacyCallToast_content", {
mx_IncomingLegacyCallToast_content_voice: isVoice,
mx_IncomingLegacyCallToast_content_video: !isVoice,
});
return (
<React.Fragment>
<RoomAvatar room={room ?? undefined} size="32px" />
<div className={contentClass}>
<div className="mx_IncomingLegacyCallToast_content">
<span className="mx_LegacyCallEvent_caller">{room ? room.name : _t("voip|unknown_caller")}</span>
<div className="mx_LegacyCallEvent_type">
<div className="mx_LegacyCallEvent_type_icon" />
{getCallStateIcon(isVoice, undefined)}
{isVoice ? _t("voip|voice_call") : _t("voip|video_call")}
</div>
<div className="mx_IncomingLegacyCallToast_buttons">
<AccessibleButton
className="mx_IncomingLegacyCallToast_button mx_IncomingLegacyCallToast_button_decline"
className="mx_IncomingLegacyCallToast_button"
onClick={this.onRejectClick}
kind="danger"
>
<span> {_t("action|decline")} </span>
<EndCallIcon />
{_t("action|decline")}
</AccessibleButton>
<AccessibleButton
className="mx_IncomingLegacyCallToast_button mx_IncomingLegacyCallToast_button_accept"
className="mx_IncomingLegacyCallToast_button"
onClick={this.onAnswerClick}
kind="primary"
>
<span> {_t("action|accept")} </span>
{isVoice ? <VoiceCallSolidIcon /> : <VideoCallSolidIcon />}
{_t("action|accept")}
</AccessibleButton>
</div>
</div>