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
|
@ -33,9 +33,12 @@ describe("Registration", () => {
|
|||
});
|
||||
|
||||
it("registers an account and lands on the home screen", () => {
|
||||
cy.injectAxe();
|
||||
|
||||
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
|
||||
cy.get(".mx_ServerPickerDialog_continue").should("be.visible");
|
||||
cy.percySnapshot("Server Picker");
|
||||
cy.checkA11y();
|
||||
|
||||
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
|
||||
cy.get(".mx_ServerPickerDialog_continue").click();
|
||||
|
@ -46,6 +49,7 @@ describe("Registration", () => {
|
|||
// Hide the server text as it contains the randomly allocated Synapse port
|
||||
const percyCSS = ".mx_ServerPicker_server { visibility: hidden !important; }";
|
||||
cy.percySnapshot("Registration", { percyCSS });
|
||||
cy.checkA11y();
|
||||
|
||||
cy.get("#mx_RegistrationForm_username").type("alice");
|
||||
cy.get("#mx_RegistrationForm_password").type("totally a great password");
|
||||
|
@ -55,16 +59,21 @@ describe("Registration", () => {
|
|||
|
||||
cy.get(".mx_RegistrationEmailPromptDialog").should("be.visible");
|
||||
cy.percySnapshot("Registration email prompt", { percyCSS });
|
||||
cy.checkA11y();
|
||||
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
|
||||
|
||||
cy.stopMeasuring("create-account");
|
||||
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy").should("be.visible");
|
||||
cy.percySnapshot("Registration terms prompt", { percyCSS });
|
||||
cy.checkA11y();
|
||||
|
||||
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
|
||||
cy.startMeasuring("from-submit-to-home");
|
||||
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
|
||||
|
||||
cy.get(".mx_UseCaseSelection_skip").should("exist");
|
||||
cy.percySnapshot("Use-case selection screen");
|
||||
cy.checkA11y();
|
||||
cy.get(".mx_UseCaseSelection_skip .mx_AccessibleButton").click();
|
||||
|
||||
cy.url().should('contain', '/#/home');
|
||||
|
|
|
@ -20,7 +20,6 @@ import { MessageEvent } from "matrix-events-sdk";
|
|||
|
||||
import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
|
||||
import type { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
|
@ -46,10 +45,14 @@ const expectDisplayName = (e: JQuery<HTMLElement>, displayName: string): void =>
|
|||
};
|
||||
|
||||
const expectAvatar = (e: JQuery<HTMLElement>, avatarUrl: string): void => {
|
||||
cy.getClient().then((cli: MatrixClient) => {
|
||||
cy.all([
|
||||
cy.window({ log: false }),
|
||||
cy.getClient(),
|
||||
]).then(([win, cli]) => {
|
||||
const size = AVATAR_SIZE * win.devicePixelRatio;
|
||||
expect(e.find(".mx_BaseAvatar_image").attr("src")).to.equal(
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
cli.mxcUrlToHttp(avatarUrl, AVATAR_SIZE, AVATAR_SIZE, AVATAR_RESIZE_METHOD),
|
||||
cli.mxcUrlToHttp(avatarUrl, size, size, AVATAR_RESIZE_METHOD),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -75,15 +78,17 @@ describe("Timeline", () => {
|
|||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, OLD_NAME).then(() =>
|
||||
cy.window({ log: false }).then(() => {
|
||||
cy.createRoom({ name: ROOM_NAME }).then(_room1Id => {
|
||||
roomId = _room1Id;
|
||||
});
|
||||
cy.createRoom({ name: ROOM_NAME }).then(_room1Id => {
|
||||
roomId = _room1Id;
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
describe("useOnlyCurrentProfiles", () => {
|
||||
beforeEach(() => {
|
||||
cy.uploadContent(OLD_AVATAR).then((url) => {
|
||||
|
@ -95,10 +100,6 @@ describe("Timeline", () => {
|
|||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("should show historical profiles if disabled", () => {
|
||||
cy.setSettingValue("useOnlyCurrentProfiles", null, SettingLevel.ACCOUNT, false);
|
||||
sendEvent(roomId);
|
||||
|
@ -146,11 +147,16 @@ describe("Timeline", () => {
|
|||
});
|
||||
|
||||
describe("message displaying", () => {
|
||||
beforeEach(() => {
|
||||
cy.injectAxe();
|
||||
});
|
||||
|
||||
it("should create and configure a room on IRC layout", () => {
|
||||
cy.visit("/#/room/" + roomId);
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=irc] " +
|
||||
".mx_GenericEventListSummary_summary", "created and configured the room.");
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
cy.percySnapshot("Configured room on IRC layout");
|
||||
});
|
||||
|
||||
|
@ -174,10 +180,12 @@ describe("Timeline", () => {
|
|||
.should('have.css', "margin-inline-start", "104px")
|
||||
.should('have.css', "inset-inline-start", "0px");
|
||||
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
// Exclude timestamp from snapshot
|
||||
const percyCSS = ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp "
|
||||
+ "{ visibility: hidden !important; }";
|
||||
cy.percySnapshot("Event line with inline start margin on IRC layout", { percyCSS });
|
||||
cy.checkA11y();
|
||||
});
|
||||
|
||||
it("should set inline start padding to a hidden event line", () => {
|
||||
|
|
|
@ -41,8 +41,11 @@ describe("Login", () => {
|
|||
});
|
||||
|
||||
it("logs in with an existing account and lands on the home screen", () => {
|
||||
cy.injectAxe();
|
||||
|
||||
cy.get("#mx_LoginForm_username", { timeout: 15000 }).should("be.visible");
|
||||
cy.percySnapshot("Login");
|
||||
cy.checkA11y();
|
||||
|
||||
cy.get(".mx_ServerPicker_change").click();
|
||||
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
|
||||
|
|
|
@ -22,6 +22,7 @@ import { performance } from "./performance";
|
|||
import { synapseDocker } from "./synapsedocker";
|
||||
import { webserver } from "./webserver";
|
||||
import { docker } from "./docker";
|
||||
import { log } from "./log";
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
|
@ -31,4 +32,5 @@ export default function(on: PluginEvents, config: PluginConfigOptions) {
|
|||
performance(on, config);
|
||||
synapseDocker(on, config);
|
||||
webserver(on, config);
|
||||
log(on, config);
|
||||
}
|
||||
|
|
35
cypress/plugins/log.ts
Normal file
35
cypress/plugins/log.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import PluginEvents = Cypress.PluginEvents;
|
||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||
|
||||
export function log(on: PluginEvents, config: PluginConfigOptions) {
|
||||
on("task", {
|
||||
log(message: string) {
|
||||
console.log(message);
|
||||
|
||||
return null;
|
||||
},
|
||||
table(message: string) {
|
||||
console.table(message);
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
61
cypress/support/axe.ts
Normal file
61
cypress/support/axe.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import "cypress-axe";
|
||||
import * as axe from "axe-core";
|
||||
import { Options } from "cypress-axe";
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
function terminalLog(violations: axe.Result[]): void {
|
||||
cy.task(
|
||||
'log',
|
||||
`${violations.length} accessibility violation${
|
||||
violations.length === 1 ? '' : 's'
|
||||
} ${violations.length === 1 ? 'was' : 'were'} detected`,
|
||||
);
|
||||
|
||||
// pluck specific keys to keep the table readable
|
||||
const violationData = violations.map(({ id, impact, description, nodes }) => ({
|
||||
id,
|
||||
impact,
|
||||
description,
|
||||
nodes: nodes.length,
|
||||
}));
|
||||
|
||||
cy.task('table', violationData);
|
||||
}
|
||||
|
||||
Cypress.Commands.overwrite("checkA11y", (
|
||||
originalFn: Chainable["checkA11y"],
|
||||
context?: string | Node | axe.ContextObject | undefined,
|
||||
options: Options = {},
|
||||
violationCallback?: ((violations: axe.Result[]) => void) | undefined,
|
||||
skipFailures?: boolean,
|
||||
): void => {
|
||||
return originalFn(context, {
|
||||
...options,
|
||||
rules: {
|
||||
// Disable contrast checking for now as we have too many issues with it
|
||||
'color-contrast': {
|
||||
enabled: false,
|
||||
},
|
||||
...options.rules,
|
||||
},
|
||||
}, violationCallback ?? terminalLog, skipFailures);
|
||||
});
|
|
@ -36,3 +36,4 @@ import "./iframes";
|
|||
import "./timeline";
|
||||
import "./network";
|
||||
import "./composer";
|
||||
import "./axe";
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
"dom",
|
||||
"dom.iterable"
|
||||
],
|
||||
"types": ["cypress", "@percy/cypress"],
|
||||
"types": [
|
||||
"cypress",
|
||||
"cypress-axe",
|
||||
"@percy/cypress"
|
||||
],
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
|
|
|
@ -167,10 +167,12 @@
|
|||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
||||
"allchange": "^1.0.6",
|
||||
"axe-core": "^4.4.3",
|
||||
"babel-jest": "^26.6.3",
|
||||
"blob-polyfill": "^6.0.20211015",
|
||||
"chokidar": "^3.5.1",
|
||||
"cypress": "^10.3.0",
|
||||
"cypress-axe": "^1.0.0",
|
||||
"cypress-real-events": "^1.7.1",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
|
|
|
@ -29,20 +29,20 @@ limitations under the License.
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
h2 {
|
||||
h1 {
|
||||
font-size: $font-24px;
|
||||
font-weight: 600;
|
||||
font-weight: $font-semi-bold;
|
||||
margin-top: 8px;
|
||||
color: $authpage-primary-color;
|
||||
}
|
||||
|
||||
h3 {
|
||||
h2 {
|
||||
font-size: $font-14px;
|
||||
font-weight: 600;
|
||||
font-weight: $font-semi-bold;
|
||||
color: $authpage-secondary-color;
|
||||
}
|
||||
|
||||
h3.mx_AuthBody_centered {
|
||||
h2.mx_AuthBody_centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -35,11 +35,11 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
> h4 {
|
||||
> h2 {
|
||||
font-size: $font-15px;
|
||||
font-weight: $font-semi-bold;
|
||||
color: $secondary-content;
|
||||
margin-left: 8px;
|
||||
margin: 16px 0 16px 8px;
|
||||
}
|
||||
|
||||
> a {
|
||||
|
|
|
@ -24,7 +24,7 @@ limitations under the License.
|
|||
font-size: $font-14px;
|
||||
line-height: $font-20px;
|
||||
|
||||
> h3 {
|
||||
> h2 {
|
||||
font-weight: $font-semi-bold;
|
||||
margin: 0 0 20px;
|
||||
grid-column: 1;
|
||||
|
|
|
@ -45,7 +45,7 @@ limitations under the License.
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
h4 {
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
font-size: $font-16px;
|
||||
|
|
|
@ -30,9 +30,12 @@ limitations under the License.
|
|||
border-bottom: 1px solid $menu-selected-color;
|
||||
}
|
||||
|
||||
.mx_DateSeparator > div {
|
||||
.mx_DateSeparator > h2 {
|
||||
margin: 0 25px;
|
||||
flex: 0 0 auto;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mx_DateSeparator_jumpToDateMenu {
|
||||
|
|
|
@ -382,7 +382,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
<aside className="mx_LeftPanel_roomListContainer">
|
||||
<div className="mx_LeftPanel_roomListContainer">
|
||||
{ this.renderSearchDialExplore() }
|
||||
{ this.renderBreadcrumbs() }
|
||||
{ !this.props.isMinimized && (
|
||||
|
@ -401,7 +401,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
{ roomList }
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -674,7 +674,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
<div className={bodyClasses}>
|
||||
<div className='mx_LeftPanel_outerWrapper'>
|
||||
<LeftPanelLiveShareWarning isMinimized={this.props.collapseLhs || false} />
|
||||
<div className='mx_LeftPanel_wrapper'>
|
||||
<nav className='mx_LeftPanel_wrapper'>
|
||||
<BackdropPanel
|
||||
blurMultiplier={0.5}
|
||||
backgroundImage={this.state.backgroundImage}
|
||||
|
@ -693,7 +693,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />
|
||||
<div className="mx_RoomView_wrapper">
|
||||
|
|
|
@ -100,11 +100,11 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
|
|||
return (
|
||||
<AuthPage>
|
||||
<CompleteSecurityBody>
|
||||
<h2 className="mx_CompleteSecurity_header">
|
||||
<h1 className="mx_CompleteSecurity_header">
|
||||
{ icon }
|
||||
{ title }
|
||||
{ skipButton }
|
||||
</h2>
|
||||
</h1>
|
||||
<div className="mx_CompleteSecurity_body">
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||
</div>
|
||||
|
|
|
@ -437,7 +437,7 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
|
|||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<h2> { _t('Set a new password') } </h2>
|
||||
<h1> { _t('Set a new password') } </h1>
|
||||
{ resetPasswordJsx }
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
|
|
|
@ -600,10 +600,10 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
<AuthPage>
|
||||
<AuthHeader disableLanguageSelector={this.props.isSyncing || this.state.busyLoggingIn} />
|
||||
<AuthBody>
|
||||
<h2>
|
||||
<h1>
|
||||
{ _t('Sign in') }
|
||||
{ loader }
|
||||
</h2>
|
||||
</h1>
|
||||
{ errorTextSection }
|
||||
{ serverDeadSection }
|
||||
<ServerPicker
|
||||
|
|
|
@ -507,9 +507,9 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
// when there is only a single (or 0) providers we show a wide button with `Continue with X` text
|
||||
if (providers.length > 1) {
|
||||
// i18n: ssoButtons is a placeholder to help translators understand context
|
||||
continueWithSection = <h3 className="mx_AuthBody_centered">
|
||||
continueWithSection = <h2 className="mx_AuthBody_centered">
|
||||
{ _t("Continue with %(ssoButtons)s", { ssoButtons: "" }).trim() }
|
||||
</h3>;
|
||||
</h2>;
|
||||
}
|
||||
|
||||
// i18n: ssoButtons & usernamePassword are placeholders to help translators understand context
|
||||
|
@ -521,7 +521,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
/>
|
||||
<h3 className="mx_AuthBody_centered">
|
||||
<h2 className="mx_AuthBody_centered">
|
||||
{ _t(
|
||||
"%(ssoButtons)s Or %(usernamePassword)s",
|
||||
{
|
||||
|
@ -529,7 +529,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
usernamePassword: "",
|
||||
},
|
||||
).trim() }
|
||||
</h3>
|
||||
</h2>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
|
@ -617,7 +617,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
} else {
|
||||
// regardless of whether we're the client that started the registration or not, we should
|
||||
// try our credentials anyway
|
||||
regDoneText = <h3>{ _t(
|
||||
regDoneText = <h2>{ _t(
|
||||
"<a>Log in</a> to your new account.", {},
|
||||
{
|
||||
a: (sub) => <AccessibleButton
|
||||
|
@ -630,10 +630,10 @@ export default class Registration extends React.Component<IProps, IState> {
|
|||
}}
|
||||
>{ sub }</AccessibleButton>,
|
||||
},
|
||||
) }</h3>;
|
||||
) }</h2>;
|
||||
}
|
||||
body = <div>
|
||||
<h2>{ _t("Registration Successful") }</h2>
|
||||
<h1>{ _t("Registration Successful") }</h1>
|
||||
{ regDoneText }
|
||||
</div>;
|
||||
} else {
|
||||
|
|
|
@ -298,7 +298,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
return <>
|
||||
<p>{ introText }</p>
|
||||
{ this.renderSsoForm(null) }
|
||||
<h3 className="mx_AuthBody_centered">
|
||||
<h2 className="mx_AuthBody_centered">
|
||||
{ _t(
|
||||
"%(ssoButtons)s Or %(usernamePassword)s",
|
||||
{
|
||||
|
@ -306,7 +306,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
usernamePassword: "",
|
||||
},
|
||||
).trim() }
|
||||
</h3>
|
||||
</h2>
|
||||
{ this.renderPasswordForm(null) }
|
||||
</>;
|
||||
}
|
||||
|
@ -327,16 +327,16 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthBody>
|
||||
<h2>
|
||||
<h1>
|
||||
{ _t("You're signed out") }
|
||||
</h2>
|
||||
</h1>
|
||||
|
||||
<h3>{ _t("Sign in") }</h3>
|
||||
<h2>{ _t("Sign in") }</h2>
|
||||
<div>
|
||||
{ this.renderSignInSection() }
|
||||
</div>
|
||||
|
||||
<h3>{ _t("Clear personal data") }</h3>
|
||||
<h2>{ _t("Clear personal data") }</h2>
|
||||
<p>
|
||||
{ _t(
|
||||
"Warning: Your personal data (including encryption keys) is still stored " +
|
||||
|
|
|
@ -33,7 +33,7 @@ export function AuthHeaderDisplay({ title, icon, serverPicker, children }: Props
|
|||
return (
|
||||
<Fragment>
|
||||
{ current?.icon ?? icon }
|
||||
<h2>{ current?.title ?? title }</h2>
|
||||
<h1>{ current?.title ?? title }</h1>
|
||||
{ children }
|
||||
{ current?.hideServerPicker !== true && serverPicker }
|
||||
</Fragment>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2408,6 +2408,7 @@
|
|||
"You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.",
|
||||
"Join millions for free on the largest public server": "Join millions for free on the largest public server",
|
||||
"Homeserver": "Homeserver",
|
||||
"Help": "Help",
|
||||
"Choose a locale": "Choose a locale",
|
||||
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||
|
|
|
@ -6,7 +6,7 @@ exports[`DateSeparator renders the date separator correctly 1`] = `
|
|||
roomId="!unused:example.org"
|
||||
ts={1639728540000}
|
||||
>
|
||||
<h2
|
||||
<div
|
||||
aria-label="Today"
|
||||
className="mx_DateSeparator"
|
||||
role="separator"
|
||||
|
@ -15,15 +15,15 @@ exports[`DateSeparator renders the date separator correctly 1`] = `
|
|||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<div
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
>
|
||||
Today
|
||||
</div>
|
||||
</h2>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</h2>
|
||||
</div>
|
||||
</DateSeparator>
|
||||
`;
|
||||
|
||||
|
@ -33,7 +33,7 @@ exports[`DateSeparator when feature_jump_to_date is enabled renders the date sep
|
|||
roomId="!unused:example.org"
|
||||
ts={1639728540000}
|
||||
>
|
||||
<h2
|
||||
<div
|
||||
aria-label="Fri, Dec 17 2021"
|
||||
className="mx_DateSeparator"
|
||||
role="separator"
|
||||
|
@ -88,11 +88,11 @@ exports[`DateSeparator when feature_jump_to_date is enabled renders the date sep
|
|||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
>
|
||||
Fri, Dec 17 2021
|
||||
</div>
|
||||
</h2>
|
||||
<div
|
||||
className="mx_DateSeparator_chevron"
|
||||
/>
|
||||
|
@ -103,6 +103,6 @@ exports[`DateSeparator when feature_jump_to_date is enabled renders the date sep
|
|||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</h2>
|
||||
</div>
|
||||
</DateSeparator>
|
||||
`;
|
||||
|
|
|
@ -31,6 +31,7 @@ exports[`FontScalingPanel renders the font scaling UI 1`] = `
|
|||
<div
|
||||
aria-label="Loading..."
|
||||
className="mx_Spinner_icon"
|
||||
role="progressbar"
|
||||
style={
|
||||
Object {
|
||||
"height": 32,
|
||||
|
|
|
@ -12,6 +12,7 @@ exports[`Module Components should override the factory for a ModuleSpinner 1`] =
|
|||
<div
|
||||
aria-label="Loading..."
|
||||
className="mx_Spinner_icon"
|
||||
role="progressbar"
|
||||
style={
|
||||
Object {
|
||||
"height": 32,
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -2714,6 +2714,11 @@ axe-core@^4.4.2:
|
|||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.2.tgz#dcf7fb6dea866166c3eab33d68208afe4d5f670c"
|
||||
integrity sha512-LVAaGp/wkkgYJcjmHsoKx4juT1aQvJyPcW09MLCjVTh3V2cc6PnyempiLMNH5iMdfIX/zdbjUx2KDjMLCTdPeA==
|
||||
|
||||
axe-core@^4.4.3:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f"
|
||||
integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==
|
||||
|
||||
axobject-query@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
||||
|
@ -3539,6 +3544,11 @@ csstype@^3.0.2:
|
|||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
||||
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
||||
|
||||
cypress-axe@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-1.0.0.tgz#ab4e9486eaa3bb956a90a1ae40d52df42827b4f0"
|
||||
integrity sha512-QBlNMAd5eZoyhG8RGGR/pLtpHGkvgWXm2tkP68scJ+AjYiNNOlJihxoEwH93RT+rWOLrefw4iWwEx8kpEcrvJA==
|
||||
|
||||
cypress-real-events@^1.7.1:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.1.tgz#8f430d67c29ea4f05b9c5b0311780120cbc9b935"
|
||||
|
|
Loading…
Reference in a new issue