Skip to content

Commit

Permalink
♿ [open-formulieren/open-forms#4716] Added aria attributes to invalid…
Browse files Browse the repository at this point in the history
… form fields
  • Loading branch information
robinmolen committed Oct 1, 2024
1 parent 09f3f60 commit be8731c
Show file tree
Hide file tree
Showing 17 changed files with 139 additions and 11 deletions.
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.element);
return super.setErrorClasses(elements, dirty, hasErrors, hasMessages);
}
}

export default Checkbox;
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.element);
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
8 changes: 8 additions & 0 deletions src/formio/components/DateField.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {Formio} from 'react-formio';

import {MinMaxDateValidator} from 'formio/validators/minMaxDateAndDatetimeValidator';

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

const DateTimeField = Formio.Components.components.datetime;

const extractDate = value => {
Expand Down Expand Up @@ -40,6 +42,12 @@ class DateField extends DateTimeField {
return info;
}

setErrorClasses(elements, dirty, hasErrors, hasMessages) {
const targetElements = [this.element.querySelector('input:not([type="hidden"])')];
setErrorAttributes(targetElements, hasErrors, hasMessages, this.element);
return super.setErrorClasses(targetElements, dirty, hasErrors, hasMessages);
}

beforeSubmit() {
// The field itself should prevent any invalid dates from being passed in
// so we are not checking that here
Expand Down
8 changes: 8 additions & 0 deletions src/formio/components/DateTimeField.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {Formio} from 'react-formio';

import {MinMaxDatetimeValidator} from 'formio/validators/minMaxDateAndDatetimeValidator';

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

const DateTimeFormio = Formio.Components.components.datetime;

class DateTimeField extends DateTimeFormio {
Expand All @@ -27,6 +29,12 @@ class DateTimeField extends DateTimeFormio {
return info;
}

setErrorClasses(elements, dirty, hasErrors, hasMessages) {
const targetElements = [this.element.querySelector('input:not([type="hidden"])')];
setErrorAttributes(targetElements, hasErrors, hasMessages, this.element);
return super.setErrorClasses(targetElements, dirty, hasErrors, hasMessages);
}

get suffix() {
// Don't show an icon
return null;
Expand Down
7 changes: 7 additions & 0 deletions src/formio/components/Email.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {Formio} from 'react-formio';

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

/**
* Extend the default email field to modify it to our needs.
*/
Expand All @@ -16,6 +18,11 @@ class Email extends Formio.Components.components.email {
return info;
}

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

restoreCaretPosition() {
if (!this.root?.currentSelection || !this.refs.input?.length) return;

Expand Down
7 changes: 7 additions & 0 deletions src/formio/components/IBANField.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {electronicFormatIBAN, isValidIBAN} from 'ibantools';
import {Formio} from 'react-formio';

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

const TextField = Formio.Components.components.textfield;

const IbanValidator = {
Expand Down Expand Up @@ -49,4 +51,9 @@ export default class IBANField extends TextField {
].join(' ');
return info;
}

setErrorClasses(elements, dirty, hasErrors, hasMessages) {
setErrorAttributes(elements, hasErrors, hasMessages, this.element);
return super.setErrorClasses(elements, dirty, hasErrors, hasMessages);
}
}
6 changes: 6 additions & 0 deletions src/formio/components/Number.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {maskInput} from '@formio/vanilla-text-mask';
import {set} from 'lodash';
import {Formio} from 'react-formio';

import {setErrorAttributes} from '../utils';
import enableValidationPlugins from '../validators/plugins';

/**
Expand Down Expand Up @@ -32,6 +33,11 @@ class Number extends Formio.Components.components.number {
return super.checkComponentValidity(data, dirty, row, updatedOptions);
}

setErrorClasses(elements, dirty, hasErrors, hasMessages) {
setErrorAttributes(elements, hasErrors, hasMessages, this.element);
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/number/Number.js#L112
// Modified for the case where negative numbers are allowed.
Expand Down
7 changes: 7 additions & 0 deletions src/formio/components/Password.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {Formio} from 'react-formio';

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

/**
* Extend the default password field to modify it to our needs.
*
Expand All @@ -16,6 +18,11 @@ class Password extends Formio.Components.components.password {
].join(' ');
return info;
}

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

export default Password;
6 changes: 6 additions & 0 deletions src/formio/components/PhoneNumberField.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Formio} from 'formiojs';

import {setErrorAttributes} from '../utils';
import enableValidationPlugins from '../validators/plugins';

const PhoneNumber = Formio.Components.components.phoneNumber;
Expand Down Expand Up @@ -44,6 +45,11 @@ class PhoneNumberField extends PhoneNumber {
return info;
}

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

checkComponentValidity(data, dirty, row, options = {}) {
let updatedOptions = {...options};
if (this.component.validate.plugins && this.component.validate.plugins.length) {
Expand Down
7 changes: 7 additions & 0 deletions src/formio/components/Radio.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {Formio} from 'react-formio';

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

/**
* Extend the default radio field to modify it to our needs.
*/
Expand All @@ -14,6 +16,11 @@ class Radio extends Formio.Components.components.radio {
].join(' ');
return info;
}

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

export default Radio;
10 changes: 9 additions & 1 deletion src/formio/components/Select.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import isObject from 'lodash/isObject';
import {Formio} from 'react-formio';

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

/**
* Extend the default select field to modify it to our needs.
Expand All @@ -16,6 +16,14 @@ class Select extends Formio.Components.components.select {
return info;
}

setErrorClasses(elements, dirty, hasErrors, hasMessages) {
const targetElements = [
this.element.querySelector('.form-control input.choices__input.choices__input--cloned'),
];
setErrorAttributes(targetElements, hasErrors, hasMessages, this.element);
return super.setErrorClasses(targetElements, dirty, hasErrors, hasMessages);
}

setValue(value, flags = {}) {
// check if it's an appointment config field
if (this.component?.appointments != null) {
Expand Down
6 changes: 6 additions & 0 deletions src/formio/components/Selectboxes.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 Selectboxes extends Formio.Components.components.selectboxes {
].join(' ');
return info;
}

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

export default Selectboxes;
7 changes: 6 additions & 1 deletion src/formio/components/TextArea.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Formio} from 'react-formio';

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

/**
* Extend the default text field to modify it to our needs.
Expand All @@ -17,6 +17,11 @@ class TextArea extends Formio.Components.components.textarea {
return info;
}

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

renderElement(value, index) {
// security issue #19 - self XSS if the contents are not escaped and formio ends
// up rendering the unsanitized content. As a workaround, we apply the escaping
Expand Down
13 changes: 6 additions & 7 deletions src/formio/components/TextField.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import debounce from 'lodash/debounce';
import {Formio} from 'react-formio';

import {get} from '../../api';
import {setErrorAttributes} from '../utils';
import enableValidationPlugins from '../validators/plugins';

const POSTCODE_REGEX = /^[0-9]{4}\s?[a-zA-Z]{2}$/;
Expand Down Expand Up @@ -29,13 +30,6 @@ class TextField extends Formio.Components.components.textfield {
return info;
}

checkValidity(data, dirty, row, silentCheck) {
const validity = super.checkValidity(data, dirty, row, silentCheck);
console.log({data, dirty, row, validity});
// info.attr['aria-describedby'] = '';
return validity;
}

checkComponentValidity(data, dirty, row, options = {}) {
let updatedOptions = {...options};
if (this.component.validate.plugins && this.component.validate.plugins.length) {
Expand All @@ -44,6 +38,11 @@ class TextField extends Formio.Components.components.textfield {
return super.checkComponentValidity(data, dirty, row, updatedOptions);
}

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

/**
* Return a debounced method to look up and autocomplete the location data.
*/
Expand Down
7 changes: 7 additions & 0 deletions src/formio/components/TimeField.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {Formio} from 'react-formio';

import MinMaxTimeValidator from 'formio/validators/MinMaxTimeValidator';

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

const Time = Formio.Components.components.time;

class TimeField extends Time {
Expand Down Expand Up @@ -30,6 +32,11 @@ class TimeField extends Time {
return info;
}

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

getStringAsValue(value) {
const result = super.getStringAsValue(value);
if (result === 'Invalid date') return value;
Expand Down
2 changes: 1 addition & 1 deletion src/formio/templates/map.ejs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<div ref="mapContainer" id="map-{{ctx.instance.id}}" class="{{ctx.ofPrefix}}leaflet-map"></div>
<div ref="mapContainer" id="{{ctx.instance.id}}-map" class="{{ctx.ofPrefix}}leaflet-map"></div>
36 changes: 35 additions & 1 deletion src/formio/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,38 @@ const escapeHtml = source => {
return pre.innerHTML.replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/&/g, '&amp;');
};

export {applyPrefix, escapeHtml};
const setErrorAttributes = (elements, hasErrors, hasMessages, parentElement) => {
// Update the attributes 'aria-invalid' and 'aria-describedby' using on hasErrors
elements.forEach(element => {
const errorMessageElementId = parentElement?.querySelector('[ref="messageContainer"]')?.id;
let ariaDescriptions = (element.getAttribute('aria-describedby') || '')
.split(' ')
.filter(description => description !== '');

if (hasErrors && hasMessages && !ariaDescriptions.includes(errorMessageElementId)) {
// The input has an error and the error message isn't part of the ariaDescriptions
ariaDescriptions.push(errorMessageElementId);
}

if (!hasErrors && ariaDescriptions.includes(errorMessageElementId)) {
// The input doesn't have an error, but the error message is part of the ariaDescriptions
ariaDescriptions = ariaDescriptions.filter(
description => description !== errorMessageElementId
);
}

if (ariaDescriptions.length > 0) {
element.setAttribute('aria-describedby', ariaDescriptions.join(' '));
} else {
element.removeAttribute('aria-describedby');
}

if (hasErrors) {
element.setAttribute('aria-invalid', 'true');
} else {
element.removeAttribute('aria-invalid');
}
});
};

export {applyPrefix, escapeHtml, setErrorAttributes};

0 comments on commit be8731c

Please sign in to comment.