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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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