mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-26 09:19:12 +01:00
Fix accessibility issues when creating a repository
Reviewed-by: Florian Scholdei <florian.scholdei@cloudogu.com>
This commit is contained in:
6
gradle/changelog/fix-add-repo-accessibility.yaml
Normal file
6
gradle/changelog/fix-add-repo-accessibility.yaml
Normal 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
|
||||
@@ -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]}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
23
scm-ui/ui-core/src/base/forms/misc/RequiredMarker.tsx
Normal file
23
scm-ui/ui-core/src/base/forms/misc/RequiredMarker.tsx
Normal 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;
|
||||
@@ -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";
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user