Spike AXE A11Y testing in Cypress (#9111)
* Spike AXE A11Y testing in Cypress * Fix NewRoomIntro breaking html/aria list rules * Fix HeaderButtons breaking aria role semantics rules * missing type * Switch left panel from aside to nav and include space panel * Give the page a main heading of the room name when viewing a room * Use header landmark on RoomHeader * Improve aria attributes on composer when autocomplete is closed * Fix aria-owns on RoomHeader * Give Spinner an aria role * Give server picker help button an aria label * Improve auth aria attributes and semantics * Improve heading semantics in use case selection screen * Fix autocomplete attribute to be valid * Fix heading semantics on login page * Improve Cypress axe testing * Add axe tests * Stop synapse after the timeline tests * Await spinners to fade before percy snapshotting timeline tests * Improve naming of plugin * Update snapshots * Fix accidental heading change * Fix double synapse stoppage * Fix Cypress timeline avatar assertions to be DPI agnostic * Fix aria attributes on date separators * delint * Update snapshots * Revert style change * Skip redundant call
This commit is contained in:
parent
05cc5f62dd
commit
d5db131eef
40 changed files with 244 additions and 83 deletions
|
@ -22,7 +22,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export default function AuthBody({ flex, children }: PropsWithChildren<Props>) {
|
||||
return <div className={classNames("mx_AuthBody", { "mx_AuthBody_flex": flex })}>
|
||||
return <main className={classNames("mx_AuthBody", { "mx_AuthBody_flex": flex })}>
|
||||
{ children }
|
||||
</div>;
|
||||
</main>;
|
||||
}
|
||||
|
|
|
@ -23,9 +23,9 @@ import { _t } from '../../../languageHandler';
|
|||
export default class AuthFooter extends React.Component {
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_AuthFooter">
|
||||
<footer className="mx_AuthFooter" role="contentinfo">
|
||||
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@ import React from 'react';
|
|||
|
||||
export default class AuthHeaderLogo extends React.PureComponent {
|
||||
public render(): React.ReactNode {
|
||||
return <div className="mx_AuthHeaderLogo">
|
||||
return <aside className="mx_AuthHeaderLogo">
|
||||
Matrix
|
||||
</div>;
|
||||
</aside>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -422,7 +422,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
|||
<Field
|
||||
id="mx_LoginForm_password"
|
||||
className={pwFieldClass}
|
||||
autoComplete="password"
|
||||
autoComplete="current-password"
|
||||
type="password"
|
||||
name="password"
|
||||
label={_t('Password')}
|
||||
|
|
|
@ -206,6 +206,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
checked={!this.state.defaultChosen}
|
||||
onChange={this.onOtherChosen}
|
||||
childrenInLabel={false}
|
||||
aria-label={_t("Other homeserver")}
|
||||
>
|
||||
<Field
|
||||
type="text"
|
||||
|
@ -230,7 +231,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
{ _t("Continue") }
|
||||
</AccessibleButton>
|
||||
|
||||
<h4>{ _t("Learn more") }</h4>
|
||||
<h2>{ _t("Learn more") }</h2>
|
||||
<a href="https://matrix.org/faq/#what-is-a-homeserver%3F" target="_blank" rel="noreferrer noopener">
|
||||
{ _t("About homeservers") }
|
||||
</a>
|
||||
|
|
|
@ -85,8 +85,13 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
|
|||
}
|
||||
|
||||
return <div className="mx_ServerPicker">
|
||||
<h3>{ title || _t("Homeserver") }</h3>
|
||||
{ !disableCustomUrls ? <AccessibleButton className="mx_ServerPicker_help" onClick={onHelpClick} /> : null }
|
||||
<h2>{ title || _t("Homeserver") }</h2>
|
||||
{ !disableCustomUrls ? (
|
||||
<AccessibleButton
|
||||
className="mx_ServerPicker_help"
|
||||
onClick={onHelpClick}
|
||||
aria-label={_t("Help")}
|
||||
/>): null }
|
||||
<span className="mx_ServerPicker_server" title={typeof serverName === "string" ? serverName : undefined}>
|
||||
{ serverName }
|
||||
</span>
|
||||
|
|
|
@ -39,6 +39,7 @@ export default class Spinner extends React.PureComponent<IProps> {
|
|||
className="mx_Spinner_icon"
|
||||
style={{ width: w, height: h }}
|
||||
aria-label={_t("Loading...")}
|
||||
role="progressbar"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -57,7 +57,7 @@ export function UseCaseSelection({ onFinished }: Props) {
|
|||
</div>
|
||||
<div className="mx_UseCaseSelection_info mx_UseCaseSelection_slideInDelayed">
|
||||
<h2>{ _t("Who will you chat to the most?") }</h2>
|
||||
<h4>{ _t("We'll help you get connected.") }</h4>
|
||||
<h3>{ _t("We'll help you get connected.") }</h3>
|
||||
</div>
|
||||
<div className="mx_UseCaseSelection_options mx_UseCaseSelection_slideInDelayed">
|
||||
<UseCaseSelectionButton
|
||||
|
|
|
@ -223,7 +223,7 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
|||
isExpanded={!!this.state.contextMenuPosition}
|
||||
title={_t("Jump to date")}
|
||||
>
|
||||
<div aria-hidden="true">{ this.getLabel() }</div>
|
||||
<h2 aria-hidden="true">{ this.getLabel() }</h2>
|
||||
<div className="mx_DateSeparator_chevron" />
|
||||
{ contextMenu }
|
||||
</ContextMenuTooltipButton>
|
||||
|
@ -237,15 +237,15 @@ export default class DateSeparator extends React.Component<IProps, IState> {
|
|||
if (this.state.jumpToDateEnabled) {
|
||||
dateHeaderContent = this.renderJumpToDateMenu();
|
||||
} else {
|
||||
dateHeaderContent = <div aria-hidden="true">{ label }</div>;
|
||||
dateHeaderContent = <h2 aria-hidden="true">{ label }</h2>;
|
||||
}
|
||||
|
||||
// ARIA treats <hr/>s as separators, here we abuse them slightly so manually treat this entire thing as one
|
||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||
return <h2 className="mx_DateSeparator" role="separator" tabIndex={-1} aria-label={label}>
|
||||
return <div className="mx_DateSeparator" role="separator" tabIndex={-1} aria-label={label}>
|
||||
<hr role="none" />
|
||||
{ dateHeaderContent }
|
||||
<hr role="none" />
|
||||
</h2>;
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@ export default abstract class HeaderButtons<P = {}> extends React.Component<IPro
|
|||
public abstract renderButtons(): JSX.Element;
|
||||
|
||||
public render() {
|
||||
return <div className="mx_HeaderButtons">
|
||||
return <div className="mx_HeaderButtons" role="tablist">
|
||||
{ this.renderButtons() }
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -760,7 +760,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
const { completionIndex } = this.state;
|
||||
const hasAutocomplete = Boolean(this.state.autoComplete);
|
||||
let activeDescendant;
|
||||
let activeDescendant: string;
|
||||
if (hasAutocomplete && completionIndex >= 0) {
|
||||
activeDescendant = generateCompletionDomId(completionIndex);
|
||||
}
|
||||
|
@ -784,8 +784,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
aria-multiline="true"
|
||||
aria-autocomplete="list"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={hasAutocomplete}
|
||||
aria-owns="mx_Autocomplete"
|
||||
aria-expanded={hasAutocomplete ? true : undefined}
|
||||
aria-owns={hasAutocomplete ? "mx_Autocomplete" : undefined}
|
||||
aria-activedescendant={activeDescendant}
|
||||
dir="auto"
|
||||
aria-disabled={this.props.disabled}
|
||||
|
|
|
@ -219,8 +219,7 @@ const NewRoomIntro = () => {
|
|||
<span> { subText } { subButton } </span>
|
||||
);
|
||||
|
||||
return <div className="mx_NewRoomIntro">
|
||||
|
||||
return <li className="mx_NewRoomIntro">
|
||||
{ !hasExpectedEncryptionSettings(cli, room) && (
|
||||
<EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
|
||||
|
@ -230,7 +229,7 @@ const NewRoomIntro = () => {
|
|||
) }
|
||||
|
||||
{ body }
|
||||
</div>;
|
||||
</li>;
|
||||
};
|
||||
|
||||
export default NewRoomIntro;
|
||||
|
|
|
@ -45,6 +45,8 @@ import { NotificationStateEvents } from '../../../stores/notifications/Notificat
|
|||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import RoomLiveShareWarning from '../beacon/RoomLiveShareWarning';
|
||||
import { BetaPill } from "../beta/BetaCard";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
||||
export interface ISearchInfo {
|
||||
searchTerm: string;
|
||||
|
@ -71,6 +73,7 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
contextMenuPosition?: DOMRect;
|
||||
rightPanelOpen: boolean;
|
||||
}
|
||||
|
||||
export default class RoomHeader extends React.Component<IProps, IState> {
|
||||
|
@ -89,23 +92,29 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
super(props, context);
|
||||
const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room);
|
||||
notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
this.state = {};
|
||||
this.state = {
|
||||
rightPanelOpen: RightPanelStore.instance.isOpen,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
}
|
||||
cli?.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
}
|
||||
|
||||
private onRightPanelStoreUpdate = () => {
|
||||
this.setState({ rightPanelOpen: RightPanelStore.instance.isOpen });
|
||||
};
|
||||
|
||||
private onRoomStateEvents = (event: MatrixEvent) => {
|
||||
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
||||
return;
|
||||
|
@ -230,7 +239,9 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
const roomName = <RoomName room={this.props.room}>
|
||||
{ (name) => {
|
||||
const roomName = name || oobName;
|
||||
return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
|
||||
return <div dir="auto" className={textClasses} title={roomName} role="heading" aria-level={1}>
|
||||
{ roomName }
|
||||
</div>;
|
||||
} }
|
||||
</RoomName>;
|
||||
|
||||
|
@ -311,8 +322,11 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
) : null;
|
||||
|
||||
return (
|
||||
<div className="mx_RoomHeader light-panel">
|
||||
<div className="mx_RoomHeader_wrapper" aria-owns="mx_RightPanel">
|
||||
<header className="mx_RoomHeader light-panel">
|
||||
<div
|
||||
className="mx_RoomHeader_wrapper"
|
||||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||
>
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
|
||||
{ name }
|
||||
|
@ -322,7 +336,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
{ buttons }
|
||||
</div>
|
||||
<RoomLiveShareWarning roomId={this.props.room.roomId} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue