Skip to content

Commit

Permalink
♿ [open-formulieren/open-forms#4716] WIP Added aria attributes to inv…
Browse files Browse the repository at this point in the history
…alid fields
  • Loading branch information
robinmolen committed Oct 1, 2024
1 parent 09f3f60 commit 487ed65
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 10 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;
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/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
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, '"').replace(/'/g, ''').replace(/&/g, '&');
};

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 487ed65

Please sign in to comment.