Read all errors with screen reader (#1839)

Make error notifications accessible for screen readers.
This commit is contained in:
Eduard Heimbuch
2021-11-03 08:14:54 +01:00
committed by GitHub
parent f44ef0be48
commit b78742ed0b
10 changed files with 79 additions and 90 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Read all errors with screen readers ([#1839](https://github.com/scm-manager/scm-manager/pull/1839))

View File

@@ -21,37 +21,19 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import React, { FC } from "react";
import { BackendError } from "@scm-manager/ui-api";
import Notification from "./Notification";
import { useTranslation } from "react-i18next";
import { WithTranslation, withTranslation } from "react-i18next";
type Props = WithTranslation & {
type Props = {
error: BackendError;
};
class BackendErrorNotification extends React.Component<Props> {
constructor(props: Props) {
super(props);
}
const BackendErrorNotification: FC<Props> = ({ error }) => {
const [t] = useTranslation("plugins");
render() {
return (
<Notification type="danger">
<div className="content">
<p className="subtitle">{this.renderErrorName()}</p>
<p>{this.renderErrorDescription()}</p>
{this.renderAdditionalMessages()}
<p>{this.renderViolations()}</p>
{this.renderMetadata()}
</div>
</Notification>
);
}
renderErrorName = () => {
const { error, t } = this.props;
const renderErrorName = () => {
const translation = t(`errors.${error.errorCode}.displayName`);
if (translation === error.errorCode) {
return error.message;
@@ -59,8 +41,7 @@ class BackendErrorNotification extends React.Component<Props> {
return translation;
};
renderErrorDescription = () => {
const { error, t } = this.props;
const renderErrorDescription = () => {
const translation = t(`errors.${error.errorCode}.description`);
if (translation === error.errorCode) {
return "";
@@ -68,17 +49,16 @@ class BackendErrorNotification extends React.Component<Props> {
return translation;
};
renderAdditionalMessages = () => {
const { error, t } = this.props;
const renderAdditionalMessages = () => {
if (error.additionalMessages) {
return (
<>
<hr />
{error.additionalMessages
.map(additionalMessage =>
.map((additionalMessage) =>
additionalMessage.key ? t(`errors.${additionalMessage.key}.description`) : additionalMessage.message
)
.map(message => (
.map((message) => (
<p>{message}</p>
))}
<hr />
@@ -87,8 +67,7 @@ class BackendErrorNotification extends React.Component<Props> {
}
};
renderViolations = () => {
const { error, t } = this.props;
const renderViolations = () => {
if (error.violations) {
return (
<>
@@ -110,17 +89,16 @@ class BackendErrorNotification extends React.Component<Props> {
}
};
renderMetadata = () => {
const { error, t } = this.props;
const renderMetadata = () => {
return (
<>
{this.renderContext()}
{this.renderMoreInformationLink()}
{renderContext()}
{renderMoreInformationLink()}
<div className="level is-size-7">
<div className="left">
<div className="left" aria-hidden={true}>
{t("errors.transactionId")} {error.transactionId}
</div>
<div className="right">
<div className="right" aria-hidden={true}>
{t("errors.errorCode")} {error.errorCode}
</div>
</div>
@@ -128,8 +106,7 @@ class BackendErrorNotification extends React.Component<Props> {
);
};
renderContext = () => {
const { error, t } = this.props;
const renderContext = () => {
if (error.context) {
return (
<>
@@ -150,19 +127,34 @@ class BackendErrorNotification extends React.Component<Props> {
}
};
renderMoreInformationLink = () => {
const { error, t } = this.props;
const renderMoreInformationLink = () => {
if (error.url) {
return (
<p>
{t("errors.moreInfo")}{" "}
<a href={error.url} target="_blank">
<a href={error.url} target="_blank" rel="noreferrer" aria-label={t("error.link")}>
{error.errorCode}
</a>
</p>
);
}
};
}
export default withTranslation("plugins")(BackendErrorNotification);
return (
<Notification type="danger" role="alert">
<div className="content">
<p className="subtitle">
{t("error.subtitle")}
{": "}
{renderErrorName()}
</p>
<p>{renderErrorDescription()}</p>
{renderAdditionalMessages()}
<p>{renderViolations()}</p>
{renderMetadata()}
</div>
</Notification>
);
};
export default BackendErrorNotification;

View File

@@ -40,6 +40,16 @@ const LoginLink: FC = () => {
return <a href={urls.withContextPath(`/login?from=${from}`)}>{t("errorNotification.loginLink")}</a>;
};
const BasicErrorMessage: FC = ({ children }) => {
const [t] = useTranslation("commons");
return (
<Notification type="danger" role="alert">
<strong>{t("errorNotification.prefix")}:</strong> {children}
</Notification>
);
};
const ErrorNotification: FC<Props> = ({ error }) => {
const [t] = useTranslation("commons");
if (error) {
@@ -47,22 +57,14 @@ const ErrorNotification: FC<Props> = ({ error }) => {
return <BackendErrorNotification error={error} />;
} else if (error instanceof UnauthorizedError) {
return (
<Notification type="danger">
<strong>{t("errorNotification.prefix")}:</strong> {t("errorNotification.timeout")} <LoginLink />
</Notification>
<BasicErrorMessage>
{t("errorNotification.timeout")} <LoginLink />
</BasicErrorMessage>
);
} else if (error instanceof ForbiddenError) {
return (
<Notification type="danger">
<strong>{t("errorNotification.prefix")}:</strong> {t("errorNotification.forbidden")}
</Notification>
);
return <BasicErrorMessage>{t("errorNotification.forbidden")}</BasicErrorMessage>;
} else {
return (
<Notification type="danger">
<strong>{t("errorNotification.prefix")}:</strong> {error.message}
</Notification>
);
return <BasicErrorMessage>{error.message}</BasicErrorMessage>;
}
}
return null;

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { ReactNode } from "react";
import React, { FC, ReactNode } from "react";
import classNames from "classnames";
type NotificationType = "primary" | "info" | "success" | "warning" | "danger" | "inherit";
@@ -31,33 +31,25 @@ type Props = {
onClose?: () => void;
className?: string;
children?: ReactNode;
role?: string;
};
class Notification extends React.Component<Props> {
static defaultProps = {
type: "info"
};
renderCloseButton() {
const { onClose } = this.props;
const Notification: FC<Props> = ({ type = "info", onClose, className, children, role }) => {
const renderCloseButton = () => {
if (onClose) {
return <button className="delete" onClick={onClose} />;
}
return "";
}
render() {
const { type, className, children } = this.props;
return null;
};
const color = type !== "inherit" ? "is-" + type : "";
return (
<div className={classNames("notification", color, className)}>
{this.renderCloseButton()}
<div className={classNames("notification", color, className)} role={role}>
{renderCloseButton()}
{children}
</div>
);
}
}
};
export default Notification;

View File

@@ -73471,7 +73471,6 @@ exports[`Storyshots Notification Danger 1`] = `
<div
className="notification is-danger"
>
Cleverness nuclear genuine static irresponsibility invited President Zaphod
Beeblebrox hyperspace ship. Another custard through computer-generated universe
shapes field strong disaster parties Russells ancestors infinite colour
@@ -73487,7 +73486,6 @@ exports[`Storyshots Notification Info 1`] = `
<div
className="notification is-info"
>
Cleverness nuclear genuine static irresponsibility invited President Zaphod
Beeblebrox hyperspace ship. Another custard through computer-generated universe
shapes field strong disaster parties Russells ancestors infinite colour
@@ -73503,7 +73501,6 @@ exports[`Storyshots Notification Primary 1`] = `
<div
className="notification is-primary"
>
Cleverness nuclear genuine static irresponsibility invited President Zaphod
Beeblebrox hyperspace ship. Another custard through computer-generated universe
shapes field strong disaster parties Russells ancestors infinite colour
@@ -73519,7 +73516,6 @@ exports[`Storyshots Notification Success 1`] = `
<div
className="notification is-success"
>
Cleverness nuclear genuine static irresponsibility invited President Zaphod
Beeblebrox hyperspace ship. Another custard through computer-generated universe
shapes field strong disaster parties Russells ancestors infinite colour
@@ -73535,7 +73531,6 @@ exports[`Storyshots Notification Warning 1`] = `
<div
className="notification is-warning"
>
Cleverness nuclear genuine static irresponsibility invited President Zaphod
Beeblebrox hyperspace ship. Another custard through computer-generated universe
shapes field strong disaster parties Russells ancestors infinite colour
@@ -89309,7 +89304,6 @@ exports[`Storyshots Table|Table Empty 1`] = `
<div
className="notification is-info"
>
No data found.
</div>
`;

View File

@@ -2294,7 +2294,7 @@
</div>
</div>
<div class="column is-one-third">
<div class="notification is-danger">
<div class="notification is-danger" role="alert">
<button class="delete">&nbsp</button>
<strong>Danger</strong><br>
Primar lorem ipsum dolor sit amet, consectetur

View File

@@ -1,4 +1,8 @@
{
"error": {
"subtitle": "Fehler",
"link": "Link zu weiteren Informationen"
},
"changeset": {
"contributor": {
"type": {

View File

@@ -1,4 +1,8 @@
{
"error": {
"subtitle": "Error",
"link": "Link for more information"
},
"changeset": {
"contributor": {
"type": {
@@ -471,4 +475,3 @@
}
}
}

View File

@@ -5,7 +5,7 @@
{{$content}}
<h2 class="subtitle">An error occurred during SCM-Manager startup.</h2>
<div class="notification is-danger">
<div class="notification is-danger" role="alert">
<pre>
{{ error }}
</pre>

View File

@@ -5,7 +5,7 @@
{{$content}}
<h2 class="subtitle">An error occurred during SCM-Manager startup.</h2>
<p class="notification is-danger">
<p class="notification is-danger" role="alert">
We cannot migrate your SCM-Manager 1 installation,
because the version is too old.<br />
Please migrate to version 1.60 or newer, before migration to 2.x.