fix(form-core): aria-required for compatible roles
This commit is contained in:
parent
fa9cdffc8f
commit
aa47817465
3 changed files with 112 additions and 1 deletions
5
.changeset/lazy-bulldogs-hear.md
Normal file
5
.changeset/lazy-bulldogs-hear.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/form-core': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: prevent a11y violations when applying aria-required
|
||||||
|
|
@ -9,6 +9,33 @@ export class Required extends Validator {
|
||||||
return 'Required';
|
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'.
|
* We don't have an execute function, since the Required validator is 'special'.
|
||||||
* The outcome depends on the modelValue of the FormControl and
|
* 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
|
// eslint-disable-next-line class-methods-use-this
|
||||||
onFormControlConnect(formControl) {
|
onFormControlConnect(formControl) {
|
||||||
if (formControl._inputNode) {
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
74
packages/form-core/test/validate/Required.test.js
Normal file
74
packages/form-core/test/validate/Required.test.js
Normal file
|
|
@ -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}></${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}></${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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue