mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-03 03:55:51 +01:00
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:
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user