Fix accessibility issues when creating a repository

Reviewed-by: Florian Scholdei <florian.scholdei@cloudogu.com>
This commit is contained in:
Thomas Zerr
2024-12-02 06:59:13 +01:00
parent 5f539ebc3d
commit 03fa34d0b1
15 changed files with 80 additions and 50 deletions

View File

@@ -0,0 +1,6 @@
- type: fixed
description: Screen reader reads the label of NamespaceInput correctly
- type: added
description: Required symbol for corresponding inputs of create repository form
- type: added
description: Screen reader reads whether a create repository form input is required

View File

@@ -2321,7 +2321,6 @@ exports[`Storyshots Forms/AddKeyValueEntryToTableField Default 1`] = `
>
Key
</span>
</label>
<div
className="control"
@@ -2348,7 +2347,6 @@ exports[`Storyshots Forms/AddKeyValueEntryToTableField Default 1`] = `
>
Value
</span>
</label>
<div
className="control"
@@ -2402,7 +2400,6 @@ exports[`Storyshots Forms/AddKeyValueEntryToTableField Disabled 1`] = `
>
Key
</span>
</label>
<div
className="control"
@@ -2430,7 +2427,6 @@ exports[`Storyshots Forms/AddKeyValueEntryToTableField Disabled 1`] = `
>
Value
</span>
</label>
<div
className="control"
@@ -2965,7 +2961,6 @@ exports[`Storyshots Forms/FileInput Default 1`] = `
>
Upload File
</span>
<div
className="Tooltip__TooltipWrapper-sc-fmp853-0 fJWuAF"
onClick={[Function]}
@@ -3059,7 +3054,6 @@ exports[`Storyshots Forms/InputField AutoFocus 1`] = `
>
Field with AutoFocus
</span>
</label>
<div
className="control"
@@ -3146,7 +3140,6 @@ exports[`Storyshots Forms/InputField Default Value 1`] = `
>
Field with Default Value
</span>
</label>
<div
className="control"
@@ -3215,7 +3208,6 @@ exports[`Storyshots Forms/InputField React Hook Form 1`] = `
>
Readonly
</span>
</label>
<div
className="control"
@@ -3243,7 +3235,6 @@ exports[`Storyshots Forms/InputField React Hook Form 1`] = `
>
Disabled
</span>
</label>
<div
className="control"
@@ -3272,7 +3263,6 @@ exports[`Storyshots Forms/InputField React Hook Form 1`] = `
>
First Name
</span>
</label>
<div
className="control"
@@ -3299,7 +3289,6 @@ exports[`Storyshots Forms/InputField React Hook Form 1`] = `
>
Last Name
</span>
</label>
<div
className="control"
@@ -3855,7 +3844,6 @@ exports[`Storyshots Forms/Select ReactHookForm 1`] = `
>
Remember Me
</span>
</label>
<div
className="control select"
@@ -3890,7 +3878,6 @@ exports[`Storyshots Forms/Select ReactHookForm 1`] = `
>
Scramble Password
</span>
</label>
<div
className="control select"
@@ -3925,7 +3912,6 @@ exports[`Storyshots Forms/Select ReactHookForm 1`] = `
>
Disabled wont be submitted
</span>
</label>
<div
className="control select"
@@ -3963,7 +3949,6 @@ exports[`Storyshots Forms/Select ReactHookForm 1`] = `
>
Readonly will be submitted
</span>
</label>
<div
className="control select"
@@ -4020,7 +4005,6 @@ Array [
>
Ref Radio Button
</span>
</label>
<div
className="control select"
@@ -4071,7 +4055,6 @@ exports[`Storyshots Forms/Textarea AutoFocus 1`] = `
>
Field with AutoFocus
</span>
</label>
<div
className="control"
@@ -4152,7 +4135,6 @@ exports[`Storyshots Forms/Textarea Default Value 1`] = `
>
Field with Default Value
</span>
</label>
<div
className="control"
@@ -4297,7 +4279,6 @@ exports[`Storyshots Forms/Textarea ReactHookForm 1`] = `
>
Message
</span>
</label>
<div
className="control"
@@ -4324,7 +4305,6 @@ exports[`Storyshots Forms/Textarea ReactHookForm 1`] = `
>
Footer
</span>
</label>
<div
className="control"
@@ -4352,7 +4332,6 @@ exports[`Storyshots Forms/Textarea ReactHookForm 1`] = `
>
Readonly
</span>
</label>
<div
className="control"
@@ -4380,7 +4359,6 @@ exports[`Storyshots Forms/Textarea ReactHookForm 1`] = `
>
Disabled
</span>
</label>
<div
className="control"
@@ -17941,7 +17919,6 @@ Array [
>
Text
</span>
</label>
<div
className="control"
@@ -18505,7 +18482,6 @@ Array [
>
Input
</span>
<div
className="Tooltip__TooltipWrapper-sc-fmp853-0 fJWuAF"
onClick={[Function]}
@@ -18548,7 +18524,6 @@ Array [
>
Textarea
</span>
<div
className="Tooltip__TooltipWrapper-sc-fmp853-0 fJWuAF"
onClick={[Function]}

View File

@@ -21,12 +21,13 @@ type Props = {
handleFile: (file: File, event?: ChangeEvent<HTMLInputElement>) => void;
filenamePlaceholder?: string;
disabled?: boolean;
required?: boolean;
};
/**
* @deprecated
*/
const FileUpload: FC<Props> = ({ handleFile, filenamePlaceholder = "", disabled = false }) => {
const FileUpload: FC<Props> = ({ handleFile, filenamePlaceholder = "", disabled = false, required }) => {
const [t] = useTranslation("commons");
const [file, setFile] = useState<File | null>(null);
@@ -45,6 +46,7 @@ const FileUpload: FC<Props> = ({ handleFile, filenamePlaceholder = "", disabled
// @ts-ignore the uploaded file doesn't match our types
handleFile(uploadedFile, event);
}}
aria-required={required}
/>
<span className="file-cta">
<span className="file-icon">

View File

@@ -39,6 +39,7 @@ type BaseProps = {
testId?: string;
defaultValue?: string | number;
readOnly?: boolean;
required?: boolean;
};
export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>> = ({
@@ -58,6 +59,7 @@ export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>
autofocus,
defaultValue,
readOnly,
required,
...props
}) => {
const field = useAutofocus<HTMLInputElement>(autofocus, props.innerRef);
@@ -101,7 +103,7 @@ export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>
const helpId = createA11yId("input");
return (
<fieldset className={classNames("field", className)} disabled={readOnly}>
<LabelWithHelpIcon label={label} helpText={helpText} id={id} helpId={helpId} />
<LabelWithHelpIcon label={label} helpText={helpText} id={id} helpId={helpId} required={required} />
<div className="control">
<input
aria-labelledby={id}
@@ -117,6 +119,7 @@ export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>
onKeyPress={handleKeyPress}
onBlur={handleBlur}
defaultValue={defaultValue}
aria-required={required}
{...createAttributesForTesting(testId)}
/>
</div>

View File

@@ -16,12 +16,14 @@
import React from "react";
import Help from "../Help";
import { RequiredMarker } from "@scm-manager/ui-core";
type Props = {
label?: string;
helpText?: string;
id?: string;
helpId?: string;
required?: boolean;
};
/**
@@ -36,13 +38,15 @@ class LabelWithHelpIcon extends React.Component<Props> {
}
render() {
const { label, id } = this.props;
const { label, id, required } = this.props;
if (label) {
const help = this.renderHelp();
return (
<label className="label">
<span id={id}>{label}</span> {help}
<span id={id}>{label}</span>
{required ? <RequiredMarker /> : null}
{help}
</label>
);
}

View File

@@ -40,7 +40,7 @@ const OptionsWrapper = styled(HeadlessCombobox.Options).attrs({
background-color: var(--scm-secondary-background);
max-width: 35ch;
width: 35ch;
&:empty {
border: 0;
clip: rect(0 0 0 0);
@@ -86,6 +86,7 @@ type BaseProps<T> = {
"aria-describedby"?: string;
"aria-labelledby"?: string;
"aria-label"?: string;
"aria-required"?: boolean;
testId?: string;
ref?: Ref<HTMLInputElement>;
form?: string;
@@ -149,13 +150,14 @@ function ComboboxComponent<T>(props: ComboboxProps<T>, ref: ForwardedRef<HTMLInp
aria-describedby={props["aria-describedby"]}
aria-labelledby={props["aria-labelledby"]}
aria-label={props["aria-label"]}
aria-required={props["aria-required"]}
id={props.id}
placeholder={props.placeholder}
onBlur={props.onBlur}
autoComplete="off"
onKeyDown={(e) => {
props.onKeyDown && props.onKeyDown(e);
}}
}}
{...createAttributesForTesting(props.testId)}
/>
<OptionsWrapper className="is-absolute">{options}</OptionsWrapper>

View File

@@ -22,6 +22,7 @@ import { useAriaId } from "../../helpers";
import { withForwardRef } from "../helpers";
import Combobox, { ComboboxProps } from "./Combobox";
import classNames from "classnames";
import RequiredMarker from "../misc/RequiredMarker";
/**
* @beta
@@ -34,8 +35,9 @@ const ComboboxField = function ComboboxField<T>(
error,
className,
isLoading,
required,
...props
}: ComboboxProps<T> & { label: string; helpText?: string; error?: string; isLoading?: boolean },
}: ComboboxProps<T> & { label: string; helpText?: string; error?: string; isLoading?: boolean; required?: boolean },
ref: React.ForwardedRef<HTMLInputElement>
) {
const labelId = useAriaId();
@@ -43,6 +45,7 @@ const ComboboxField = function ComboboxField<T>(
<Field className={className}>
<Label id={labelId}>
{label}
{required ? <RequiredMarker /> : null}
{helpText ? <Help className="ml-1" text={helpText} /> : null}
</Label>
<div className={classNames("control", { "is-loading": isLoading })}>

View File

@@ -46,6 +46,7 @@ export { default as Textarea } from "./input/Textarea";
export { default as Select } from "./select/Select";
export * from "./resourceHooks";
export { default as Label } from "./base/label/Label";
export { default as RequiredMarker } from "./misc/RequiredMarker";
const RadioGroupExport = {
Option: RadioButton,

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
import React from "react";
const RequiredMarker = () => {
return <span aria-hidden={true}>*</span>;
};
export default RequiredMarker;

View File

@@ -14,13 +14,12 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
export * from "./buttons"
export * from "./forms"
export * from "./helpers"
export * from "./misc"
export * from "./layout"
export * from "./notifications"
export * from "./overlays"
export * from "./shortcuts"
export * from "./text"
export * from "./buttons";
export * from "./forms";
export * from "./helpers";
export * from "./misc";
export * from "./layout";
export * from "./notifications";
export * from "./overlays";
export * from "./shortcuts";
export * from "./text";

View File

@@ -35,7 +35,7 @@ const ImportFromBundleForm: FC<Props> = ({
setCompressed,
password,
setPassword,
disabled
disabled,
}) => {
const [t] = useTranslation("repos");
@@ -43,12 +43,13 @@ const ImportFromBundleForm: FC<Props> = ({
<>
<div className="columns">
<div className="column is-half is-vcentered">
<LabelWithHelpIcon label={t("import.bundle.title")} helpText={t("import.bundle.helpText")} />
<LabelWithHelpIcon label={t("import.bundle.title")} helpText={t("import.bundle.helpText")} required={true} />
<FileUpload
handleFile={file => {
handleFile={(file) => {
setFile(file);
setValid(!!file);
}}
required={true}
/>
</div>
<div className="column is-half is-vcentered">
@@ -66,7 +67,7 @@ const ImportFromBundleForm: FC<Props> = ({
<div className="column is-half is-vcentered">
<InputField
value={password}
onChange={value => setPassword(value)}
onChange={(value) => setPassword(value)}
type="password"
label={t("import.bundle.password.title")}
helpText={t("import.bundle.password.helpText")}

View File

@@ -59,12 +59,14 @@ const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid, disabled
errorMessage={t("validation.url-invalid")}
disabled={disabled}
onBlur={handleImportUrlBlur}
required={true}
aria-required={true}
/>
</div>
<div className="column is-half px-3">
<InputField
label={t("import.username")}
onChange={username => onChange({ ...repository, username })}
onChange={(username) => onChange({ ...repository, username })}
value={repository.username}
helpText={t("help.usernameHelpText")}
disabled={disabled}
@@ -73,7 +75,7 @@ const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid, disabled
<div className="column is-half px-3">
<InputField
label={t("import.password")}
onChange={password => onChange({ ...repository, password })}
onChange={(password) => onChange({ ...repository, password })}
value={repository.password}
type="password"
helpText={t("help.passwordHelpText")}
@@ -83,7 +85,7 @@ const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid, disabled
<div className="column is-full px-3">
<Checkbox
label={t("import.skipLfs")}
onChange={skipLfs => onChange({ ...repository, skipLfs })}
onChange={(skipLfs) => onChange({ ...repository, skipLfs })}
checked={repository.skipLfs}
helpText={t("help.skipLfsHelpText")}
disabled={disabled}

View File

@@ -31,12 +31,17 @@ const ImportFullRepositoryForm: FC<Props> = ({ setFile, setValid, password, setP
return (
<div className="columns">
<div className="column is-half is-vcentered">
<LabelWithHelpIcon label={t("import.fullImport.title")} helpText={t("import.fullImport.helpText")} />
<LabelWithHelpIcon
label={t("import.fullImport.title")}
helpText={t("import.fullImport.helpText")}
required={true}
/>
<FileUpload
handleFile={(file: File) => {
setFile(file);
setValid(!!file);
}}
required={true}
/>
</div>
<div className="column is-half is-vcentered">

View File

@@ -96,6 +96,7 @@ const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, setValid, dis
errorMessage={t("validation.name-invalid")}
helpText={t("help.nameHelpText")}
disabled={disabled}
required={true}
/>
</>
);

View File

@@ -61,6 +61,7 @@ const NamespaceInput: FC<Props> = ({
return (
// @ts-ignore
<ComboboxField
aria-label={props.label}
label={props.label}
helpText={props.helpText}
value={repositorySelectValue}
@@ -68,6 +69,8 @@ const NamespaceInput: FC<Props> = ({
onQueryChange={setQuery}
options={data}
isLoading={isLoading}
required={true}
aria-required={true}
nullable
/>
);