Feature/fix tabulator stops (#1831)

Add tab stops to action to increase accessibility of SCM-Manager with keyboard only usage. Also add a focus trap for modals to ensure the actions inside the modal can be used without losing the focus.

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-11-16 11:35:58 +01:00
committed by GitHub
parent 0530e3864f
commit dc5f7d0f23
47 changed files with 1380 additions and 118 deletions

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, useState, MouseEvent } from "react";
import React, { FC, useState, MouseEvent, KeyboardEvent } from "react";
import styled from "styled-components";
import Level from "../layout/Level";
import AddButton from "../buttons/AddButton";
@@ -56,7 +56,7 @@ const AddEntryToTableField: FC<Props> = ({
setEntryToAdd(entryName);
};
const addButtonClicked = (event: MouseEvent) => {
const addButtonClicked = (event: MouseEvent | KeyboardEvent) => {
event.preventDefault();
appendEntry();
};

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, useState, MouseEvent } from "react";
import React, { FC, useState, MouseEvent, KeyboardEvent } from "react";
import styled from "styled-components";
import { SelectValue } from "@scm-manager/ui-types";
import Level from "../layout/Level";
@@ -61,7 +61,7 @@ const AutocompleteAddEntryToTableField: FC<Props> = ({
setSelectedValue(selection);
};
const addButtonClicked = (event: MouseEvent) => {
const addButtonClicked = (event: MouseEvent | KeyboardEvent) => {
event.preventDefault();
appendEntry();
};

View File

@@ -27,6 +27,7 @@ import LabelWithHelpIcon from "./LabelWithHelpIcon";
import useInnerRef from "./useInnerRef";
import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
import classNames from "classnames";
import { createA11yId } from "../createA11yId";
export interface CheckboxElement extends HTMLElement {
value: boolean;
@@ -83,17 +84,20 @@ const InnerCheckbox: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
}
};
const id = createA11yId("checkbox");
const helpId = createA11yId("checkbox");
const renderHelp = () => {
const { title, helpText } = props;
if (helpText && !title) {
return <Help message={helpText} />;
return <Help message={helpText} id={helpId} />;
}
};
const renderLabelWithHelp = () => {
const { title, helpText } = props;
if (title) {
return <LabelWithHelpIcon label={title} helpText={helpText} />;
return <LabelWithHelpIcon label={title} helpText={helpText} id={id} helpId={helpId} />;
}
};
return (
@@ -116,6 +120,8 @@ const InnerCheckbox: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
checked={props.checked}
disabled={disabled}
readOnly={readOnly}
aria-labelledby={id}
aria-describedby={helpId}
{...createAttributesForTesting(testId)}
/>{" "}
{label}

View File

@@ -26,6 +26,7 @@ import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { createAttributesForTesting } from "../devBuild";
import LabelWithHelpIcon from "./LabelWithHelpIcon";
import { createA11yId } from "../createA11yId";
type Props = {
name?: string;
@@ -73,9 +74,11 @@ const FileInput: FC<Props> = ({
}
};
const id = createA11yId("file-input");
return (
<div className={classNames("field", className)}>
<LabelWithHelpIcon label={label} helpText={helpText} />
<LabelWithHelpIcon label={label} helpText={helpText} id={id} />
<div className="file is-info has-name is-fullwidth">
<label className="file-label">
<input
@@ -87,6 +90,7 @@ const FileInput: FC<Props> = ({
disabled={disabled}
onChange={handleChange}
onBlur={handleBlur}
aria-describedby={id}
{...createAttributesForTesting(testId)}
/>
<span className="file-cta">

View File

@@ -34,13 +34,14 @@ type Props = {
placeholder?: string;
autoFocus?: boolean;
className?: string;
id?: string;
};
const FixedHeightInput = styled.input`
height: 2.5rem;
`;
const FilterInput: FC<Props> = ({ filter, value, testId, placeholder, autoFocus, className }) => {
const FilterInput: FC<Props> = ({ filter, value, testId, placeholder, autoFocus, className, id }) => {
const [stateValue, setStateValue] = useState(value || "");
const [timeoutId, setTimeoutId] = useState<ReturnType<typeof setTimeout>>();
const [t] = useTranslation("commons");
@@ -79,6 +80,7 @@ const FilterInput: FC<Props> = ({ filter, value, testId, placeholder, autoFocus,
value={stateValue}
onChange={(event) => setStateValue(event.target.value)}
autoFocus={autoFocus || false}
aria-describedby={id}
/>
<span className="icon is-small is-left">
<i className="fas fa-filter" />

View File

@@ -27,6 +27,7 @@ import LabelWithHelpIcon from "./LabelWithHelpIcon";
import { createAttributesForTesting } from "../devBuild";
import useAutofocus from "./useAutofocus";
import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
import { createA11yId } from "../createA11yId";
type BaseProps = {
label?: string;
@@ -102,11 +103,16 @@ export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>
} else if (informationMessage) {
helper = <p className="help is-info">{informationMessage}</p>;
}
const id = createA11yId("input");
const helpId = createA11yId("input");
return (
<fieldset className={classNames("field", className)} disabled={readOnly}>
<LabelWithHelpIcon label={label} helpText={helpText} />
<LabelWithHelpIcon label={label} helpText={helpText} id={id} helpId={helpId} />
<div className="control">
<input
aria-labelledby={id}
aria-describedby={helpId}
ref={field}
name={name}
className={classNames("input", errorView)}

View File

@@ -27,24 +27,26 @@ import Help from "../Help";
type Props = {
label?: string;
helpText?: string;
id?: string;
helpId?: string;
};
class LabelWithHelpIcon extends React.Component<Props> {
renderHelp() {
const { helpText } = this.props;
const { helpText, helpId } = this.props;
if (helpText) {
return <Help message={helpText} />;
return <Help message={helpText} id={helpId} />;
}
}
render() {
const { label } = this.props;
const { label, id } = this.props;
if (label) {
const help = this.renderHelp();
return (
<label className="label">
{label} {help}
<span id={id}>{label}</span> {help}
</label>
);
}

View File

@@ -25,6 +25,7 @@ import React, { ChangeEvent, FC, FocusEvent } from "react";
import classNames from "classnames";
import { Help } from "../index";
import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
import { createA11yId } from "../createA11yId";
type BaseProps = {
label?: string;
@@ -36,18 +37,23 @@ type BaseProps = {
defaultChecked?: boolean;
className?: string;
readOnly?: boolean;
ariaLabelledby?: string;
};
const InnerRadio: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
name,
defaultChecked,
readOnly,
ariaLabelledby,
...props
}) => {
const id = ariaLabelledby || createA11yId("radio");
const helpId = createA11yId("radio");
const renderHelp = () => {
const helpText = props.helpText;
if (helpText) {
return <Help message={helpText} />;
return <Help message={helpText} id={helpId} />;
}
};
@@ -71,6 +77,8 @@ const InnerRadio: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
}
};
const labelElement = props.label ? (<span id={id}>{props.label}</span>) : null;
return (
<fieldset className="is-inline-block" disabled={readOnly}>
{/*
@@ -89,8 +97,10 @@ const InnerRadio: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
disabled={props.disabled}
ref={props.innerRef}
defaultChecked={defaultChecked}
aria-labelledby={id}
aria-describedby={helpId}
/>{" "}
{props.label}
{labelElement}
{renderHelp()}
</label>
</fieldset>

View File

@@ -27,6 +27,7 @@ import LabelWithHelpIcon from "./LabelWithHelpIcon";
import { createAttributesForTesting } from "../devBuild";
import useInnerRef from "./useInnerRef";
import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
import { createA11yId } from "../createA11yId";
export type SelectItem = {
value: string;
@@ -46,6 +47,7 @@ type BaseProps = {
readOnly?: boolean;
className?: string;
addValueToOptions?: boolean;
ariaLabelledby?: string;
};
const InnerSelect: FC<FieldProps<BaseProps, HTMLSelectElement, string>> = ({
@@ -61,6 +63,7 @@ const InnerSelect: FC<FieldProps<BaseProps, HTMLSelectElement, string>> = ({
className,
options,
addValueToOptions,
ariaLabelledby,
...props
}) => {
const field = useInnerRef(props.innerRef);
@@ -106,10 +109,12 @@ const InnerSelect: FC<FieldProps<BaseProps, HTMLSelectElement, string>> = ({
}, [field, value, name]);
const loadingClass = loading ? "is-loading" : "";
const a11yId = ariaLabelledby || createA11yId("select");
const helpId = createA11yId("select");
return (
<fieldset className="field" disabled={readOnly}>
<LabelWithHelpIcon label={label} helpText={helpText} />
<LabelWithHelpIcon label={label} helpText={helpText} id={a11yId} helpId={helpId} />
<div className={classNames("control select", loadingClass, className)}>
<select
name={name}
@@ -119,6 +124,8 @@ const InnerSelect: FC<FieldProps<BaseProps, HTMLSelectElement, string>> = ({
onChange={handleInput}
onBlur={handleBlur}
disabled={disabled}
aria-labelledby={a11yId}
aria-describedby={helpId}
{...createAttributesForTesting(testId)}
>
{opts.map((opt) => {

View File

@@ -26,6 +26,7 @@ import LabelWithHelpIcon from "./LabelWithHelpIcon";
import useAutofocus from "./useAutofocus";
import classNames from "classnames";
import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
import { createA11yId } from "../createA11yId";
type BaseProps = {
name?: string;
@@ -102,9 +103,12 @@ const InnerTextarea: FC<FieldProps<BaseProps, HTMLTextAreaElement, string>> = ({
helper = <p className="help is-info">{informationMessage}</p>;
}
const id = createA11yId("textarea");
const helpId = createA11yId("textarea");
return (
<fieldset className="field" disabled={readOnly}>
<LabelWithHelpIcon label={label} helpText={helpText} />
<LabelWithHelpIcon label={label} helpText={helpText} id={id} helpId={helpId} />
<div className="control">
<textarea
className={classNames("textarea", errorView)}
@@ -117,6 +121,8 @@ const InnerTextarea: FC<FieldProps<BaseProps, HTMLTextAreaElement, string>> = ({
disabled={disabled}
onKeyDown={onKeyDown}
defaultValue={defaultValue}
aria-labelledby={id}
aria-describedby={helpId}
/>
</div>
{helper}