mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 16:35:45 +01:00
Improve accessibility (#1956)
Fixes several accessibility issues: - Provide a style for empty table column headers - Add aria references and aria-labels - Remove aria references if the referenced element is not rendered
This commit is contained in:
committed by
GitHub
parent
9fa0396167
commit
1fe7b0a01e
2
gradle/changelog/improve_a11y.yaml
Normal file
2
gradle/changelog/improve_a11y.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: fixed
|
||||
description: Improve accessibility ([#1956](https://github.com/scm-manager/scm-manager/pull/1956))
|
||||
@@ -37,6 +37,7 @@ type Props = {
|
||||
label?: string;
|
||||
testId?: string;
|
||||
searchPlaceholder?: string;
|
||||
groupAriaLabelledby?: string;
|
||||
};
|
||||
|
||||
const createAbsoluteLink = (url: string) => {
|
||||
@@ -52,7 +53,8 @@ const OverviewPageActions: FC<Props> = ({
|
||||
groupSelected,
|
||||
label,
|
||||
testId,
|
||||
searchPlaceholder
|
||||
searchPlaceholder,
|
||||
groupAriaLabelledby
|
||||
}) => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
@@ -62,6 +64,7 @@ const OverviewPageActions: FC<Props> = ({
|
||||
const groupSelector = groups && (
|
||||
<div className="column is-flex">
|
||||
<Select
|
||||
ariaLabelledby={groupAriaLabelledby}
|
||||
className="is-fullwidth"
|
||||
options={groups.map(g => ({ value: g, label: g }))}
|
||||
value={currentGroup}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import React, { FC } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Color, Size } from "./styleConstants";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@@ -51,8 +52,10 @@ const Tag: FC<Props> = ({
|
||||
title,
|
||||
onClick,
|
||||
onRemove,
|
||||
children,
|
||||
children
|
||||
}) => {
|
||||
const [t] = useTranslation("commons");
|
||||
|
||||
let showIcon = null;
|
||||
if (icon) {
|
||||
showIcon = (
|
||||
@@ -64,7 +67,7 @@ const Tag: FC<Props> = ({
|
||||
}
|
||||
let showDelete = null;
|
||||
if (onRemove) {
|
||||
showDelete = <button className="tag is-delete" onClick={onRemove} />;
|
||||
showDelete = <button className="tag is-delete" onClick={onRemove} aria-label={t("tag.delete")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -78,7 +81,7 @@ const Tag: FC<Props> = ({
|
||||
{
|
||||
"is-outlined": outlined,
|
||||
"is-rounded": rounded,
|
||||
"is-clickable": onClick,
|
||||
"is-clickable": onClick
|
||||
},
|
||||
size === "small" && smallClassNames
|
||||
)}
|
||||
|
||||
@@ -34,6 +34,7 @@ exports[`Storyshots BranchSelector Default 1`] = `
|
||||
className="control select is-fullwidth"
|
||||
>
|
||||
<select
|
||||
aria-labelledby="branch-select_0"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
@@ -3048,7 +3049,6 @@ exports[`Storyshots Forms/Radio Default 1`] = `
|
||||
className="radio mr-2"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radio_56"
|
||||
aria-labelledby="radio_55"
|
||||
checked={false}
|
||||
onBlur={[Function]}
|
||||
@@ -3070,7 +3070,6 @@ exports[`Storyshots Forms/Radio Default 1`] = `
|
||||
className="radio mr-2"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radio_58"
|
||||
aria-labelledby="radio_57"
|
||||
checked={true}
|
||||
onBlur={[Function]}
|
||||
@@ -3100,7 +3099,6 @@ exports[`Storyshots Forms/Radio Disabled 1`] = `
|
||||
disabled={true}
|
||||
>
|
||||
<input
|
||||
aria-describedby="radio_60"
|
||||
aria-labelledby="radio_59"
|
||||
checked={true}
|
||||
disabled={true}
|
||||
@@ -3128,7 +3126,6 @@ Array [
|
||||
className="radio mr-2"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radio_68"
|
||||
aria-labelledby="radio_67"
|
||||
checked={false}
|
||||
onBlur={[Function]}
|
||||
@@ -3160,7 +3157,6 @@ exports[`Storyshots Forms/Radio ReactHookForm 1`] = `
|
||||
className="radio mr-2"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radio_70"
|
||||
aria-labelledby="radio_69"
|
||||
defaultChecked={true}
|
||||
name="rememberMe"
|
||||
@@ -3184,7 +3180,6 @@ exports[`Storyshots Forms/Radio ReactHookForm 1`] = `
|
||||
className="radio mr-2"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radio_72"
|
||||
aria-labelledby="radio_71"
|
||||
name="rememberMe"
|
||||
onBlur={[Function]}
|
||||
@@ -3208,7 +3203,6 @@ exports[`Storyshots Forms/Radio ReactHookForm 1`] = `
|
||||
className="radio mr-2 ml-2"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radio_74"
|
||||
aria-labelledby="radio_73"
|
||||
name="scramblePassword"
|
||||
onBlur={[Function]}
|
||||
@@ -3232,7 +3226,6 @@ exports[`Storyshots Forms/Radio ReactHookForm 1`] = `
|
||||
disabled={true}
|
||||
>
|
||||
<input
|
||||
aria-describedby="radio_76"
|
||||
aria-labelledby="radio_75"
|
||||
disabled={true}
|
||||
name="disabled"
|
||||
@@ -3256,7 +3249,6 @@ exports[`Storyshots Forms/Radio ReactHookForm 1`] = `
|
||||
className="radio mr-2"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radio_78"
|
||||
aria-labelledby="radio_77"
|
||||
name="readonly"
|
||||
onBlur={[Function]}
|
||||
@@ -3298,7 +3290,6 @@ Array [
|
||||
className="radio mr-2"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radio_66"
|
||||
aria-labelledby="radio_65"
|
||||
checked={false}
|
||||
onBlur={[Function]}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { ChangeEvent, FC, FocusEvent } from "react";
|
||||
import React, { ChangeEvent, FC, FocusEvent, useMemo } from "react";
|
||||
import classNames from "classnames";
|
||||
import { Help } from "../index";
|
||||
import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
|
||||
@@ -47,8 +47,8 @@ const InnerRadio: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
|
||||
ariaLabelledby,
|
||||
...props
|
||||
}) => {
|
||||
const id = ariaLabelledby || createA11yId("radio");
|
||||
const helpId = createA11yId("radio");
|
||||
const id = useMemo(() => ariaLabelledby || createA11yId("radio"), [ariaLabelledby]);
|
||||
const helpId = useMemo(() => createA11yId("radio"), []);
|
||||
|
||||
const renderHelp = () => {
|
||||
const helpText = props.helpText;
|
||||
@@ -77,7 +77,7 @@ const InnerRadio: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const labelElement = props.label ? (<span id={id}>{props.label}</span>) : null;
|
||||
const labelElement = props.label ? <span id={id}>{props.label}</span> : null;
|
||||
|
||||
return (
|
||||
<fieldset className="is-inline-block" disabled={readOnly}>
|
||||
@@ -98,7 +98,7 @@ const InnerRadio: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
|
||||
ref={props.innerRef}
|
||||
defaultChecked={defaultChecked}
|
||||
aria-labelledby={id}
|
||||
aria-describedby={helpId}
|
||||
aria-describedby={props.helpText ? helpId : undefined}
|
||||
/>{" "}
|
||||
{labelElement}
|
||||
{renderHelp()}
|
||||
|
||||
@@ -124,7 +124,7 @@ const InnerSelect: FC<FieldProps<BaseProps, HTMLSelectElement, string>> = ({
|
||||
onChange={handleInput}
|
||||
onBlur={handleBlur}
|
||||
disabled={disabled}
|
||||
aria-labelledby={label ? a11yId : undefined}
|
||||
aria-labelledby={ariaLabelledby || (label ? a11yId : undefined)}
|
||||
aria-describedby={helpText ? helpId : undefined}
|
||||
{...createAttributesForTesting(testId)}
|
||||
>
|
||||
|
||||
@@ -511,6 +511,14 @@ ul.is-separated {
|
||||
&.is-darker {
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
// Explicitly "remove" styles from td element to use it as an empty table column header, which is necessary for
|
||||
// a11y because an empty th element is not allowed.
|
||||
&.has-no-style {
|
||||
background-color: transparent;
|
||||
&.is-darker {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: $blue;
|
||||
@@ -528,6 +536,13 @@ ul.is-separated {
|
||||
&.is-darker {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
// Explicitly "remove" styles from td element to use it as an empty table column header, which is necessary for
|
||||
// a11y because an empty th element is not allowed.
|
||||
&.has-no-style {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
&.is-hoverable tbody tr:not(.is-selected):hover {
|
||||
background-color: whitesmoke;
|
||||
|
||||
@@ -246,5 +246,8 @@
|
||||
},
|
||||
"pdfViewer": {
|
||||
"error": "Das Dokument konnte nicht angezeigt werden. Es kann <1>hier</1> heruntergeladen werden."
|
||||
},
|
||||
"tag": {
|
||||
"delete": "Löschen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"invalidNamespace": "Keine Repositories gefunden. Möglicherweise existiert der ausgewählte Namespace nicht.",
|
||||
"createButton": "Repository hinzufügen",
|
||||
"filterRepositories": "Repositories filtern",
|
||||
"filterByNamespace": "Nach Namespace filtern",
|
||||
"allNamespaces": "Alle Namespaces",
|
||||
"clone": "Clone/Checkout",
|
||||
"contact": "E-Mail senden an {{contact}}",
|
||||
|
||||
@@ -247,5 +247,8 @@
|
||||
},
|
||||
"pdfViewer": {
|
||||
"error": "Failed to display the document. Please download it from <1>here</1>."
|
||||
},
|
||||
"tag": {
|
||||
"delete": "Delete"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"invalidNamespace": "No repositories found. It's likely that the selected namespace does not exist.",
|
||||
"createButton": "Add Repository",
|
||||
"filterRepositories": "Filter repositories",
|
||||
"filterByNamespace": "Filter by namespace",
|
||||
"allNamespaces": "All namespaces",
|
||||
"clone": "Clone/Checkout",
|
||||
"contact": "Send mail to {{contact}}",
|
||||
|
||||
@@ -191,6 +191,10 @@ const Overview: FC = () => {
|
||||
</div>
|
||||
<PageActions>
|
||||
{showActions ? (
|
||||
<>
|
||||
<label id="select-namespace" hidden>
|
||||
{t("overview.filterByNamespace")}
|
||||
</label>
|
||||
<OverviewPageActions
|
||||
showCreateButton={showCreateButton}
|
||||
currentGroup={
|
||||
@@ -198,12 +202,14 @@ const Overview: FC = () => {
|
||||
}
|
||||
groups={namespacesToRender}
|
||||
groupSelected={namespaceSelected}
|
||||
groupAriaLabelledby="select-namespace"
|
||||
link={namespace ? `repos/${namespace}` : "repos"}
|
||||
createLink="/repos/create/"
|
||||
label={t("overview.createButton")}
|
||||
testId="repository-overview"
|
||||
searchPlaceholder={t("overview.filterRepositories")}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</PageActions>
|
||||
</Page>
|
||||
|
||||
Reference in New Issue
Block a user