From aa478174652110ed315de0018fdcbaa22d5b7885 Mon Sep 17 00:00:00 2001 From: Konstantinos Norgias Date: Mon, 15 Mar 2021 12:16:06 +0100 Subject: [PATCH] fix(form-core): aria-required for compatible roles --- .changeset/lazy-bulldogs-hear.md | 5 ++ .../src/validate/validators/Required.js | 34 ++++++++- .../form-core/test/validate/Required.test.js | 74 +++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 .changeset/lazy-bulldogs-hear.md create mode 100644 packages/form-core/test/validate/Required.test.js diff --git a/.changeset/lazy-bulldogs-hear.md b/.changeset/lazy-bulldogs-hear.md new file mode 100644 index 000000000..18b83af71 --- /dev/null +++ b/.changeset/lazy-bulldogs-hear.md @@ -0,0 +1,5 @@ +--- +'@lion/form-core': patch +--- + +fix: prevent a11y violations when applying aria-required diff --git a/packages/form-core/src/validate/validators/Required.js b/packages/form-core/src/validate/validators/Required.js index 03f4ff94e..3eeac9da3 100644 --- a/packages/form-core/src/validate/validators/Required.js +++ b/packages/form-core/src/validate/validators/Required.js @@ -9,6 +9,33 @@ export class Required extends Validator { return 'Required'; } + /** + * In order to prevent accessibility violations, the aria-required attribute will + * be combined with compatible aria roles: https://www.w3.org/TR/wai-aria/#aria-required + */ + static get _compatibleRoles() { + return [ + 'combobox', + 'gridcell', + 'input', + 'listbox', + 'radiogroup', + 'select', + 'spinbutton', + 'textarea', + 'textbox', + 'tree', + ]; + } + + /** + * In order to prevent accessibility violations, the aria-required attribute will + * be combined with compatible platform input elements + */ + static get _compatibleTags() { + return ['input', 'select', 'textarea']; + } + /** * We don't have an execute function, since the Required validator is 'special'. * The outcome depends on the modelValue of the FormControl and @@ -21,7 +48,12 @@ export class Required extends Validator { // eslint-disable-next-line class-methods-use-this onFormControlConnect(formControl) { if (formControl._inputNode) { - formControl._inputNode.setAttribute('aria-required', 'true'); + const role = formControl._inputNode.getAttribute('role') || ''; + const elementTagName = formControl._inputNode.tagName.toLowerCase(); + const ctor = /** @type {typeof Required} */ (this.constructor); + if (ctor._compatibleRoles.includes(role) || ctor._compatibleTags.includes(elementTagName)) { + formControl._inputNode.setAttribute('aria-required', 'true'); + } } } diff --git a/packages/form-core/test/validate/Required.test.js b/packages/form-core/test/validate/Required.test.js new file mode 100644 index 000000000..d7eb5ca93 --- /dev/null +++ b/packages/form-core/test/validate/Required.test.js @@ -0,0 +1,74 @@ +import { expect, fixture, html, unsafeStatic, defineCE } from '@open-wc/testing'; +import { LionField } from '@lion/form-core'; +import { Required } from '../../src/validate/validators/Required.js'; + +/** + * @typedef {import('../../types/FormControlMixinTypes.js').FormControlHost} FormControlHost + * @typedef {import('../../types/FormControlMixinTypes.js').HTMLElementWithValue} HTMLElementWithValue + */ + +/** @type {HTMLElementWithValue} */ +let inputNodeTag; +class RequiredElement extends LionField { + connectedCallback() { + const inputNode = document.createElement('input'); + inputNode.slot = 'input'; + this.appendChild(inputNode); + super.connectedCallback(); + } + + get _inputNode() { + return inputNodeTag || super._inputNode; + } +} + +const tagString = defineCE(RequiredElement); +const tag = unsafeStatic(tagString); + +describe('Required validation', async () => { + const validator = new Required(); + + it('get aria-required attribute if element is part of the right tag names', async () => { + const el = /** @type {FormControlHost & HTMLElement} */ (await fixture( + html`<${tag}>`, + )); + + Required._compatibleTags.forEach(tagName => { + inputNodeTag = /** @type {HTMLElementWithValue} */ (document.createElement(tagName)); + + validator.onFormControlConnect(el); + expect(el._inputNode).to.have.attribute('aria-required', 'true'); + }); + + // When incompatible tags are used, aria-required will not be added + + // @ts-ignore + inputNodeTag = /** @type {HTMLDivElementWithValue} */ (document.createElement('div')); + + validator.onFormControlConnect(el); + expect(el._inputNode).to.not.have.attribute('aria-required'); + }); + it('get aria-required attribute if element is part of the right roles', async () => { + const el = /** @type {FormControlHost & HTMLElement} */ (await fixture( + html`<${tag}>`, + )); + + Required._compatibleRoles.forEach(role => { + // @ts-ignore + inputNodeTag = /** @type {HTMLElementWithValue} */ (document.createElement('div')); + inputNodeTag.setAttribute('role', role); + + validator.onFormControlConnect(el); + expect(el._inputNode).to.have.attribute('aria-required', 'true'); + }); + + // When incompatible roles are used, aria-required will not be added + + // @ts-ignore + inputNodeTag = /** @type {HTMLElementWithValue} */ (document.createElement('div')); + inputNodeTag.setAttribute('role', 'group'); + + validator.onFormControlConnect(el); + expect(el._inputNode).to.not.have.attribute('aria-required'); + }); +});