Skip to content

Commit

Permalink
Merge pull request #717 from open-formulieren/feature/4716-accessibil…
Browse files Browse the repository at this point in the history
…ity-improvements-form-fields

Accessibility improvements - form fields
  • Loading branch information
sergei-maertens authored Oct 24, 2024
2 parents 3619d68 + 5d34ffa commit 65b168a
Show file tree
Hide file tree
Showing 44 changed files with 1,885 additions and 45 deletions.
1 change: 1 addition & 0 deletions src/components/ProgressIndicator/ProgressIndicatorItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const ProgressIndicatorItem = ({
placeholder={!canNavigateTo}
modifiers={canNavigateTo ? getLinkModifiers(isActive) : []}
aria-label={label}
aria-current={isActive ? 'step' : undefined}
>
<FormattedMessage
description="Step label in progress indicator"
Expand Down
4 changes: 3 additions & 1 deletion src/components/forms/Checkbox/Checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const Checkbox = ({

const {error, touched} = getFieldMeta(name);
const invalid = touched && !!error;
const errorMessageId = invalid ? `${id}-error-message` : undefined;

return (
<FormField type="checkbox" invalid={invalid} className="utrecht-form-field--openforms">
Expand All @@ -40,10 +41,11 @@ const Checkbox = ({
invalid={invalid}
required={isRequired}
appearance="custom"
aria-describedby={errorMessageId}
{...inputProps}
/>
<HelpText>{description}</HelpText>
{touched && <ValidationErrors error={error} />}
{touched && <ValidationErrors error={error} id={errorMessageId} />}
</FormField>
);
};
Expand Down
4 changes: 3 additions & 1 deletion src/components/forms/DateField/DateField.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const DateField = ({
break;
}
}
const errorMessageId = invalid ? `${id}-error-message` : undefined;

return (
<FormField type="text" invalid={invalid} className="utrecht-form-field--openforms">
Expand All @@ -76,11 +77,12 @@ const DateField = ({
disabled={disabled}
invalid={invalid}
extraOnChange={onChange}
aria-describedby={errorMessageId}
{...fieldProps}
{...props}
/>
<HelpText>{description}</HelpText>
{touched && <ValidationErrors error={error} />}
{touched && <ValidationErrors error={error} id={errorMessageId} />}
</FormField>
);
};
Expand Down
1 change: 1 addition & 0 deletions src/components/forms/EmailField/EmailField.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ EmailField.propTypes = {
description: PropTypes.node,
id: PropTypes.string,
disabled: PropTypes.bool,
autocomplete: PropTypes.oneOf(['email']),
};

export default EmailField;
4 changes: 3 additions & 1 deletion src/components/forms/NumberField/NumberField.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const NumberField = ({
thousandSeparator,
decimalScale: isInteger ? undefined : 2,
};
const errorMessageId = invalid ? `${id}-error-message` : undefined;

return (
<UtrechtFormField type="text" invalid={invalid} className="utrecht-form-field--openforms">
Expand All @@ -66,11 +67,12 @@ const NumberField = ({
type={useNumberType ? 'number' : 'text'}
customInput={Textbox}
readOnly={readOnly}
aria-describedby={errorMessageId}
{...separatorProps}
/>
</Paragraph>
<HelpText>{description}</HelpText>
{touched && <ValidationErrors error={error} />}
{touched && <ValidationErrors error={error} id={errorMessageId} />}
</UtrechtFormField>
);
};
Expand Down
4 changes: 3 additions & 1 deletion src/components/forms/RadioField/RadioField.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const RadioField = ({
const {error, touched} = getFieldMeta(name);
const invalid = touched && !!error;
const descriptionid = `${id}-description`;
const errorMessageId = invalid ? `${id}-error-message` : undefined;
return (
<Fieldset
className="utrecht-form-fieldset--openforms"
Expand All @@ -58,6 +59,7 @@ export const RadioField = ({
id={`${id}-opt-${index}`}
name={name}
value={optionValue}
aria-describedby={errorMessageId}
{...inputProps}
/>
<Paragraph className="utrecht-form-field__label utrecht-form-field__label--radio">
Expand All @@ -74,7 +76,7 @@ export const RadioField = ({
))}

<HelpText id={descriptionid}>{description}</HelpText>
{touched && <ValidationErrors error={error} />}
{touched && <ValidationErrors error={error} id={errorMessageId} />}
</Fieldset>
);
};
Expand Down
4 changes: 3 additions & 1 deletion src/components/forms/SelectField/SelectField.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const SelectField = ({
const {setValue, setTouched} = getFieldHelpers(name);

const invalid = touched && !!error;
const errorMessageId = invalid ? `${id}-error-message` : undefined;

// map the formik value back to the value object for react-select
let value = undefined;
Expand Down Expand Up @@ -135,9 +136,10 @@ const SelectField = ({
}}
value={value}
onBlur={() => setTouched(true)}
aria-describedby={errorMessageId}
/>
<HelpText>{description}</HelpText>
{touched && <ValidationErrors error={error} />}
{touched && <ValidationErrors error={error} id={errorMessageId} />}
</FormField>
);
};
Expand Down
4 changes: 3 additions & 1 deletion src/components/forms/TextField/TextField.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const TextField = ({

const {error, touched} = getFieldMeta(name);
const invalid = touched && !!error;
const errorMessageId = invalid ? `${id}-error-message` : undefined;
return (
<UtrechtFormField type="text" invalid={invalid} className="utrecht-form-field--openforms">
<Label id={id} isRequired={isRequired} disabled={disabled}>
Expand All @@ -33,11 +34,12 @@ export const TextField = ({
className="utrecht-textbox--openforms"
disabled={disabled}
invalid={invalid}
aria-describedby={errorMessageId}
{...inputProps}
/>
</Paragraph>
<HelpText>{description}</HelpText>
{touched && <ValidationErrors error={error} />}
{touched && <ValidationErrors error={error} id={errorMessageId} />}
</UtrechtFormField>
);
};
Expand Down
9 changes: 7 additions & 2 deletions src/components/forms/ValidationErrors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@ import {FormFieldDescription} from '@utrecht/component-library-react';
import PropTypes from 'prop-types';
import React from 'react';

const ValidationErrors = ({error = ''}) => {
const ValidationErrors = ({error = '', id}) => {
if (!error) return null;
return (
<FormFieldDescription invalid className="utrecht-form-field-description--openforms-errors">
<FormFieldDescription
id={id}
invalid
className="utrecht-form-field-description--openforms-errors"
>
{error}
</FormFieldDescription>
);
};

ValidationErrors.propTypes = {
error: PropTypes.string,
id: PropTypes.string,
};

export default ValidationErrors;
1 change: 1 addition & 0 deletions src/components/modals/FormStepSaveModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ const FormStepSaveModal = ({
defaultMessage="The email address where you will receive the resume link."
/>
}
autocomplete="email"
/>

<Toolbar modifiers={['bottom', 'reverse']}>
Expand Down
6 changes: 6 additions & 0 deletions src/formio/components/Checkbox.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Formio} from 'react-formio';

import {setErrorAttributes} from '../utils';
import './Checkbox.scss';

/**
Expand All @@ -17,6 +18,11 @@ class Checkbox extends Formio.Components.components.checkbox {
].join(' ');
return info;
}

setErrorClasses(elements, dirty, hasErrors, hasMessages) {
setErrorAttributes(elements, hasErrors, hasMessages, this.refs.messageContainer.id);
return super.setErrorClasses(elements, dirty, hasErrors, hasMessages);
}
}

export default Checkbox;
75 changes: 75 additions & 0 deletions src/formio/components/Checkbox.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {screen} from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import _ from 'lodash';
import {Formio} from 'react-formio';

import OpenFormsModule from 'formio/module';

// Use our custom components
Formio.use(OpenFormsModule);

const selectboxesForm = {
type: 'form',
components: [
{
key: 'checkbox',
type: 'checkbox',
label: 'Checkbox',
validate: {
required: true,
},
},
],
};

const renderForm = async () => {
let formJSON = _.cloneDeep(selectboxesForm);
const container = document.createElement('div');
document.body.appendChild(container);
const form = await Formio.createForm(container, formJSON);
return {form, container};
};

describe('The checkbox component', () => {
afterEach(() => {
document.body.innerHTML = '';
});

test('Checkbox component required and checked', async () => {
const user = userEvent.setup({delay: 50});
const {form} = await renderForm();

const checkbox = screen.getByLabelText('Checkbox');

expect(checkbox).toBeVisible();

await user.click(checkbox);

expect(form.isValid()).toBeTruthy();
});

test('Checkbox component required without being checked', async () => {
const user = userEvent.setup({delay: 50});
const {form} = await renderForm();

const checkbox = screen.getByLabelText('Checkbox');

// Check and uncheck the checkbox to trigger the validation
await user.click(checkbox);
await user.click(checkbox);

// All selectboxes are marked as invalid and have aria-describedby and aria-invalid
expect(checkbox).toHaveClass('is-invalid');
expect(checkbox).toHaveAttribute('aria-describedby');
expect(checkbox).toHaveAttribute('aria-invalid', 'true');
expect(form.isValid()).toBeFalsy();

await user.click(checkbox);

// All checkboxes are again marked as valid and without aria-describedby and aria-invalid
expect(checkbox).not.toHaveClass('is-invalid');
expect(checkbox).not.toHaveAttribute('aria-describedby');
expect(checkbox).not.toHaveAttribute('aria-invalid');
expect(form.isValid()).toBeTruthy();
});
});
7 changes: 7 additions & 0 deletions src/formio/components/Currency.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import FormioUtils from 'formiojs/utils';
import _, {set} from 'lodash';
import {Formio} from 'react-formio';

import {setErrorAttributes} from '../utils';

/**
* Extend the default text field to modify it to our needs.
*/
Expand Down Expand Up @@ -79,6 +81,11 @@ class Currency extends Formio.Components.components.currency {
return info;
}

setErrorClasses(elements, dirty, hasErrors, hasMessages) {
setErrorAttributes(elements, hasErrors, hasMessages, this.refs.messageContainer.id);
return super.setErrorClasses(elements, dirty, hasErrors, hasMessages);
}

// Issue OF#1351
// Taken from Formio https://github.com/formio/formio.js/blob/v4.13.13/src/components/currency/Currency.js#L65
// Modified for the case where negative currencies are allowed.
Expand Down
77 changes: 77 additions & 0 deletions src/formio/components/Currency.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {screen} from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import _ from 'lodash';
import {Formio} from 'react-formio';

import OpenFormsModule from 'formio/module';

// Use our custom components
Formio.use(OpenFormsModule);

const currencyForm = {
type: 'form',
components: [
{
key: 'currency',
type: 'currency',
label: 'Currency',
validate: {
required: true,
},
},
],
};

const renderForm = async () => {
let formJSON = _.cloneDeep(currencyForm);
const container = document.createElement('div');
document.body.appendChild(container);
const form = await Formio.createForm(container, formJSON);
return {form, container};
};

describe('The currency component', () => {
afterEach(() => {
document.body.innerHTML = '';
});

test('Single currency component with valid input', async () => {
const user = userEvent.setup({delay: 50});
const {form} = await renderForm();

const input = screen.getByLabelText('Currency');

expect(input).toBeVisible();

await user.type(input, '6');

expect(form.isValid()).toBeTruthy();
});

test('Single currency component with invalid input', async () => {
const user = userEvent.setup({delay: 50});
const {form} = await renderForm();

const input = screen.getByLabelText('Currency');

// Trigger validation
await user.type(input, '6');
await user.clear(input);
await user.tab({shift: true});

// Input is invalid and should have aria-describedby and aria-invalid
expect(input).toHaveClass('is-invalid');
expect(input).toHaveAttribute('aria-describedby');
expect(input).toHaveAttribute('aria-invalid', 'true');
expect(form.isValid()).toBeFalsy();

await user.type(input, '6');
await user.tab({shift: true});

// Input is again valid and without aria-describedby and aria-invalid
expect(input).not.toHaveClass('is-invalid');
expect(input).not.toHaveAttribute('aria-describedby');
expect(input).not.toHaveAttribute('aria-invalid');
expect(form.isValid()).toBeTruthy();
});
});
Loading

0 comments on commit 65b168a

Please sign in to comment.