From 03fa34d0b1e31febdf3d3a32d885f4417f4ecf7d Mon Sep 17 00:00:00 2001 From: Thomas Zerr Date: Mon, 2 Dec 2024 06:59:13 +0100 Subject: [PATCH] Fix accessibility issues when creating a repository Reviewed-by: Florian Scholdei --- .../changelog/fix-add-repo-accessibility.yaml | 6 +++++ .../src/__snapshots__/storyshots.test.ts.snap | 25 ------------------- scm-ui/ui-components/src/forms/FileUpload.tsx | 4 ++- scm-ui/ui-components/src/forms/InputField.tsx | 5 +++- .../src/forms/LabelWithHelpIcon.tsx | 8 ++++-- .../src/base/forms/combobox/Combobox.tsx | 6 +++-- .../src/base/forms/combobox/ComboboxField.tsx | 5 +++- scm-ui/ui-core/src/base/forms/index.ts | 1 + .../src/base/forms/misc/RequiredMarker.tsx | 23 +++++++++++++++++ scm-ui/ui-core/src/base/index.ts | 19 +++++++------- .../repos/components/ImportFromBundleForm.tsx | 9 ++++--- .../repos/components/ImportFromUrlForm.tsx | 8 +++--- .../components/ImportFullRepositoryForm.tsx | 7 +++++- .../components/NamespaceAndNameFields.tsx | 1 + .../src/repos/components/NamespaceInput.tsx | 3 +++ 15 files changed, 80 insertions(+), 50 deletions(-) create mode 100644 gradle/changelog/fix-add-repo-accessibility.yaml create mode 100644 scm-ui/ui-core/src/base/forms/misc/RequiredMarker.tsx diff --git a/gradle/changelog/fix-add-repo-accessibility.yaml b/gradle/changelog/fix-add-repo-accessibility.yaml new file mode 100644 index 0000000000..3e76b5f785 --- /dev/null +++ b/gradle/changelog/fix-add-repo-accessibility.yaml @@ -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 diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index af4f21a2f9..4e77c71329 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -2321,7 +2321,6 @@ exports[`Storyshots Forms/AddKeyValueEntryToTableField Default 1`] = ` > Key -
Value -
Key -
Value -
Upload File -
Field with AutoFocus -
Field with Default Value -
Readonly -
Disabled -
First Name -
Last Name -
Remember Me -
Scramble Password -
Disabled wont be submitted -
Readonly will be submitted -
Ref Radio Button -
Field with AutoFocus -
Field with Default Value -
Message -
Footer -
Readonly -
Disabled -
Text -
Input -
Textarea -
) => void; filenamePlaceholder?: string; disabled?: boolean; + required?: boolean; }; /** * @deprecated */ -const FileUpload: FC = ({ handleFile, filenamePlaceholder = "", disabled = false }) => { +const FileUpload: FC = ({ handleFile, filenamePlaceholder = "", disabled = false, required }) => { const [t] = useTranslation("commons"); const [file, setFile] = useState(null); @@ -45,6 +46,7 @@ const FileUpload: FC = ({ handleFile, filenamePlaceholder = "", disabled // @ts-ignore the uploaded file doesn't match our types handleFile(uploadedFile, event); }} + aria-required={required} /> diff --git a/scm-ui/ui-components/src/forms/InputField.tsx b/scm-ui/ui-components/src/forms/InputField.tsx index 598932a10e..e20d98da1c 100644 --- a/scm-ui/ui-components/src/forms/InputField.tsx +++ b/scm-ui/ui-components/src/forms/InputField.tsx @@ -39,6 +39,7 @@ type BaseProps = { testId?: string; defaultValue?: string | number; readOnly?: boolean; + required?: boolean; }; export const InnerInputField: FC> = ({ @@ -58,6 +59,7 @@ export const InnerInputField: FC autofocus, defaultValue, readOnly, + required, ...props }) => { const field = useAutofocus(autofocus, props.innerRef); @@ -101,7 +103,7 @@ export const InnerInputField: FC const helpId = createA11yId("input"); return (
- +
onKeyPress={handleKeyPress} onBlur={handleBlur} defaultValue={defaultValue} + aria-required={required} {...createAttributesForTesting(testId)} />
diff --git a/scm-ui/ui-components/src/forms/LabelWithHelpIcon.tsx b/scm-ui/ui-components/src/forms/LabelWithHelpIcon.tsx index ed3951e606..c0537896ca 100644 --- a/scm-ui/ui-components/src/forms/LabelWithHelpIcon.tsx +++ b/scm-ui/ui-components/src/forms/LabelWithHelpIcon.tsx @@ -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 { } render() { - const { label, id } = this.props; + const { label, id, required } = this.props; if (label) { const help = this.renderHelp(); return ( ); } diff --git a/scm-ui/ui-core/src/base/forms/combobox/Combobox.tsx b/scm-ui/ui-core/src/base/forms/combobox/Combobox.tsx index cf6b5279a2..0920916ffd 100644 --- a/scm-ui/ui-core/src/base/forms/combobox/Combobox.tsx +++ b/scm-ui/ui-core/src/base/forms/combobox/Combobox.tsx @@ -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 = { "aria-describedby"?: string; "aria-labelledby"?: string; "aria-label"?: string; + "aria-required"?: boolean; testId?: string; ref?: Ref; form?: string; @@ -149,13 +150,14 @@ function ComboboxComponent(props: ComboboxProps, ref: ForwardedRef { props.onKeyDown && props.onKeyDown(e); - }} + }} {...createAttributesForTesting(props.testId)} /> {options} diff --git a/scm-ui/ui-core/src/base/forms/combobox/ComboboxField.tsx b/scm-ui/ui-core/src/base/forms/combobox/ComboboxField.tsx index 4260b165da..7f66a59a8d 100644 --- a/scm-ui/ui-core/src/base/forms/combobox/ComboboxField.tsx +++ b/scm-ui/ui-core/src/base/forms/combobox/ComboboxField.tsx @@ -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( error, className, isLoading, + required, ...props - }: ComboboxProps & { label: string; helpText?: string; error?: string; isLoading?: boolean }, + }: ComboboxProps & { label: string; helpText?: string; error?: string; isLoading?: boolean; required?: boolean }, ref: React.ForwardedRef ) { const labelId = useAriaId(); @@ -43,6 +45,7 @@ const ComboboxField = function ComboboxField(
diff --git a/scm-ui/ui-core/src/base/forms/index.ts b/scm-ui/ui-core/src/base/forms/index.ts index 0d5022cb3d..b15ad7905a 100644 --- a/scm-ui/ui-core/src/base/forms/index.ts +++ b/scm-ui/ui-core/src/base/forms/index.ts @@ -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, diff --git a/scm-ui/ui-core/src/base/forms/misc/RequiredMarker.tsx b/scm-ui/ui-core/src/base/forms/misc/RequiredMarker.tsx new file mode 100644 index 0000000000..d3d8fa746e --- /dev/null +++ b/scm-ui/ui-core/src/base/forms/misc/RequiredMarker.tsx @@ -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 *; +}; + +export default RequiredMarker; diff --git a/scm-ui/ui-core/src/base/index.ts b/scm-ui/ui-core/src/base/index.ts index 6c6f470d82..db167c946a 100644 --- a/scm-ui/ui-core/src/base/index.ts +++ b/scm-ui/ui-core/src/base/index.ts @@ -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"; diff --git a/scm-ui/ui-webapp/src/repos/components/ImportFromBundleForm.tsx b/scm-ui/ui-webapp/src/repos/components/ImportFromBundleForm.tsx index 22bb0686e5..3445f329a1 100644 --- a/scm-ui/ui-webapp/src/repos/components/ImportFromBundleForm.tsx +++ b/scm-ui/ui-webapp/src/repos/components/ImportFromBundleForm.tsx @@ -35,7 +35,7 @@ const ImportFromBundleForm: FC = ({ setCompressed, password, setPassword, - disabled + disabled, }) => { const [t] = useTranslation("repos"); @@ -43,12 +43,13 @@ const ImportFromBundleForm: FC = ({ <>
- + { + handleFile={(file) => { setFile(file); setValid(!!file); }} + required={true} />
@@ -66,7 +67,7 @@ const ImportFromBundleForm: FC = ({
setPassword(value)} + onChange={(value) => setPassword(value)} type="password" label={t("import.bundle.password.title")} helpText={t("import.bundle.password.helpText")} diff --git a/scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx b/scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx index b6dbdcc13e..93a97394af 100644 --- a/scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx +++ b/scm-ui/ui-webapp/src/repos/components/ImportFromUrlForm.tsx @@ -59,12 +59,14 @@ const ImportFromUrlForm: FC = ({ repository, onChange, setValid, disabled errorMessage={t("validation.url-invalid")} disabled={disabled} onBlur={handleImportUrlBlur} + required={true} + aria-required={true} />
onChange({ ...repository, username })} + onChange={(username) => onChange({ ...repository, username })} value={repository.username} helpText={t("help.usernameHelpText")} disabled={disabled} @@ -73,7 +75,7 @@ const ImportFromUrlForm: FC = ({ repository, onChange, setValid, disabled
onChange({ ...repository, password })} + onChange={(password) => onChange({ ...repository, password })} value={repository.password} type="password" helpText={t("help.passwordHelpText")} @@ -83,7 +85,7 @@ const ImportFromUrlForm: FC = ({ repository, onChange, setValid, disabled
onChange({ ...repository, skipLfs })} + onChange={(skipLfs) => onChange({ ...repository, skipLfs })} checked={repository.skipLfs} helpText={t("help.skipLfsHelpText")} disabled={disabled} diff --git a/scm-ui/ui-webapp/src/repos/components/ImportFullRepositoryForm.tsx b/scm-ui/ui-webapp/src/repos/components/ImportFullRepositoryForm.tsx index c62645707e..d407df6348 100644 --- a/scm-ui/ui-webapp/src/repos/components/ImportFullRepositoryForm.tsx +++ b/scm-ui/ui-webapp/src/repos/components/ImportFullRepositoryForm.tsx @@ -31,12 +31,17 @@ const ImportFullRepositoryForm: FC = ({ setFile, setValid, password, setP return (
- + { setFile(file); setValid(!!file); }} + required={true} />
diff --git a/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx b/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx index f7382121d7..5391fbe24f 100644 --- a/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx +++ b/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx @@ -96,6 +96,7 @@ const NamespaceAndNameFields: FC = ({ repository, onChange, setValid, dis errorMessage={t("validation.name-invalid")} helpText={t("help.nameHelpText")} disabled={disabled} + required={true} /> ); diff --git a/scm-ui/ui-webapp/src/repos/components/NamespaceInput.tsx b/scm-ui/ui-webapp/src/repos/components/NamespaceInput.tsx index f1d7c9dbf8..722584e636 100644 --- a/scm-ui/ui-webapp/src/repos/components/NamespaceInput.tsx +++ b/scm-ui/ui-webapp/src/repos/components/NamespaceInput.tsx @@ -61,6 +61,7 @@ const NamespaceInput: FC = ({ return ( // @ts-ignore = ({ onQueryChange={setQuery} options={data} isLoading={isLoading} + required={true} + aria-required={true} nullable /> );