diff --git a/src/formio/utils.js b/src/formio/utils.js index a7a80219f..772ec68ad 100644 --- a/src/formio/utils.js +++ b/src/formio/utils.js @@ -19,26 +19,61 @@ const escapeHtml = source => { const getAriaDescriptions = element => (element.getAttribute('aria-describedby') || '').split(' ').filter(description => !!description); +/** + * Set or remove description IDs from the element's `aria-describedby` attribute. + * @param {HTMLElement} element The element to update. + * @param {string[]} descriptionIds Element IDs to set or remove. + * @param {'present' | 'absent'} state Desired state + * @return {void} + */ +const updateAriaDescriptions = (element, descriptionIds, state) => { + let ariaDescriptions = getAriaDescriptions(element); + + switch (state) { + case 'absent': { + ariaDescriptions = ariaDescriptions.filter( + description => !descriptionIds.includes(description) + ); + break; + } + case 'present': { + const idsToAdd = descriptionIds.filter( + description => !ariaDescriptions.includes(description) + ); + ariaDescriptions.push(...idsToAdd); + break; + } + default: { + throw new Error(`Unknown state: ${state}`); + } + } + + if (ariaDescriptions.length > 0) { + element.setAttribute('aria-describedby', ariaDescriptions.join(' ')); + } else { + element.removeAttribute('aria-describedby'); + } +}; + +/** + * Update the accessible error attributes for the input elements + * @param {HTMLElement[]} elements + * @param {Boolean} hasErrors + * @param {Boolean} hasMessages + * @param {string} messageContainerId + * @return {void} + */ const setErrorAttributes = (elements, hasErrors, hasMessages, messageContainerId) => { // Update the attributes 'aria-invalid' and 'aria-describedby' using hasErrors elements.forEach(element => { - let ariaDescriptions = getAriaDescriptions(element); + const desiredState = + hasErrors & hasMessages + ? // The input has an error, but the error message isn't yet part of the ariaDescriptions + 'present' + : // The input doesn't have an error, but the error message is still a part of the ariaDescriptions + 'absent'; - if (hasErrors && hasMessages && !ariaDescriptions.includes(messageContainerId)) { - // The input has an error, but the error message isn't yet part of the ariaDescriptions - ariaDescriptions.push(messageContainerId); - } - - if (!hasErrors && ariaDescriptions.includes(messageContainerId)) { - // The input doesn't have an error, but the error message is still a part of the ariaDescriptions - ariaDescriptions = ariaDescriptions.filter(description => description !== messageContainerId); - } - - if (ariaDescriptions.length > 0) { - element.setAttribute('aria-describedby', ariaDescriptions.join(' ')); - } else { - element.removeAttribute('aria-describedby'); - } + updateAriaDescriptions(element, [messageContainerId], desiredState); if (hasErrors) { element.setAttribute('aria-invalid', 'true'); @@ -64,22 +99,7 @@ const linkToSoftRequiredDisplay = (elements, component) => { // Update the attribute 'aria-describedby' based on whether the component is empty elements.forEach(element => { - let ariaDescriptions = getAriaDescriptions(element); - - softRequiredIds.forEach(id => { - if (isEmpty && !ariaDescriptions.includes(id)) { - ariaDescriptions.push(id); - } - if (!isEmpty && ariaDescriptions.includes(id)) { - ariaDescriptions = ariaDescriptions.filter(description => description !== id); - } - }); - - if (ariaDescriptions.length > 0) { - element.setAttribute('aria-describedby', ariaDescriptions.join(' ')); - } else { - element.removeAttribute('aria-describedby'); - } + updateAriaDescriptions(element, softRequiredIds, isEmpty ? 'present' : 'absent'); }); };