Skip to content

Commit f03e8ce

Browse files
Merge pull request #610 from centrica-engineering/feature/inputter-validation-accessibility
feat: validation message to input element mapping
2 parents 6b99c15 + bbf36a4 commit f03e8ce

File tree

5 files changed

+55
-21
lines changed

5 files changed

+55
-21
lines changed

packages/muon/components/inputter/src/inputter-component.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,29 @@ export class Inputter extends ScopedElementsMixin(ValidationMixin(MaskMixin(Muon
9898
this._helperId = `${this._randomId}-helper`;
9999
}
100100

101+
willUpdate(changedProperties) {
102+
super.willUpdate(changedProperties);
103+
104+
let validationEle = this.querySelector(`#${this._id}-validation`);
105+
if (!validationEle) {
106+
validationEle = document.createElement('div');
107+
validationEle.setAttribute('class', 'visually-hidden');
108+
validationEle.setAttribute('id', `${this._id}-validation`);
109+
this.appendChild(validationEle);
110+
}
111+
const slottedInput = this._slottedInputs[0];
112+
if (this._shouldShowValidation) {
113+
validationEle.setAttribute('aria-live', 'polite');
114+
slottedInput?.setAttribute('aria-errormessage', `${this._id}-validation`);
115+
slottedInput?.setAttribute('aria-invalid', 'true');
116+
validationEle.textContent = `${this._isMultiple ? this.heading : this._slottedLabel?.textContent} ${this.validationMessage}`;
117+
} else {
118+
slottedInput?.removeAttribute('aria-errormessage');
119+
slottedInput?.removeAttribute('aria-invalid');
120+
validationEle.textContent = '';
121+
}
122+
}
123+
101124
_onChange(changeEvent) {
102125
this._pristine = false;
103126
changeEvent.stopPropagation();

packages/muon/components/inputter/src/inputter-styles.slotted.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
@import "./inputter-extends.css";
2+
@import "../../../css/accessibility.css";
23

34
light-dom {
5+
@extend %global-accessibility;
6+
47
/* NOTE: targeting Safari only */
58
@media not all and (min-resolution: 0.001dpcm) { /* stylelint-disable-line media-feature-range-notation */
69
/*

packages/muon/mixins/validation-mixin.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ export const ValidationMixin = dedupeMixin((superClass) =>
221221
}).join(' ');
222222
}
223223

224+
get _shouldShowValidation() {
225+
return this.showMessage && this.isDirty && !!this.validationMessage;
226+
}
227+
224228
/**
225229
* A method to get validation message template.
226230
*
@@ -229,12 +233,11 @@ export const ValidationMixin = dedupeMixin((superClass) =>
229233
* @override
230234
*/
231235
get _addValidationMessage() {
232-
if (this.showMessage && this.isDirty && this.validationMessage) {
236+
if (this._shouldShowValidation) {
233237
return html`
234238
<div class="validation">
235239
${this._addValidationIcon}
236-
<div class="message" role="alert" aria-live="assertive">
237-
<div class="visually-hidden">${this._isMultiple ? this.heading : this._slottedLabel?.textContent}</div>
240+
<div class="message">
238241
${this.validationMessage}
239242
</div>
240243
</div>`;

packages/muon/tests/components/inputter/inputter.test.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,13 @@ describe('Inputter', () => {
177177

178178
const validationMessage = shadowRoot.querySelector('.validation .message');
179179
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
180-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label Length must be between 8 and 20 characters.', 'validation message has correct value');
180+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Length must be between 8 and 20 characters.', 'validation message has correct value');
181181

182+
const validationId = `${inputter._id}-validation`;
183+
const validationLightDOM = inputter.querySelector(`#${validationId}`);
184+
// eslint-disable-next-line no-unused-expressions
185+
expect(validationLightDOM).to.be.ok;
186+
expect(inputElement.getAttribute('aria-errormessage')).to.be.equal(validationId);
182187
const validationIcon = shadowRoot.querySelector('.validation .icon');
183188
expect(validationIcon).to.not.be.null; // eslint-disable-line no-unused-expressions
184189
expect(validationIcon.name).to.equal('exclamation-circle', 'validation icon has correct value');
@@ -210,7 +215,7 @@ describe('Inputter', () => {
210215

211216
const validationMessage = shadowRoot.querySelector('.validation .message');
212217
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
213-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label Length must be between 8 and 20 characters.', 'validation message has correct value');
218+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Length must be between 8 and 20 characters.', 'validation message has correct value');
214219

215220
const validationIcon = shadowRoot.querySelector('.validation .icon');
216221
expect(validationIcon).to.not.be.null; // eslint-disable-line no-unused-expressions
@@ -251,7 +256,7 @@ describe('Inputter', () => {
251256

252257
const validationMessage = shadowRoot.querySelector('.validation .message');
253258
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
254-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value');
259+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value');
255260

256261
const validationIcon = shadowRoot.querySelector('.validation .icon');
257262
expect(validationIcon).to.not.be.null; // eslint-disable-line no-unused-expressions
@@ -309,7 +314,7 @@ describe('Inputter', () => {
309314

310315
const validationMessage = shadowRoot.querySelector('.validation .message');
311316
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
312-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value');
317+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value');
313318

314319
const validationIcon = shadowRoot.querySelector('.validation .icon');
315320
expect(validationIcon).to.not.be.null; // eslint-disable-line no-unused-expressions
@@ -319,7 +324,7 @@ describe('Inputter', () => {
319324
await inputter.updateComplete;
320325
expect(changeEventSpy.callCount).to.equal(3, '`change` event fired');
321326
expect(changeEventSpy.lastCall.args[0].detail.value).to.equal('12-3', '`change` event has value `12-3`');
322-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label Length must be at least 4 characters.', 'validation message has correct value');
327+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Length must be at least 4 characters.', 'validation message has correct value');
323328

324329
inputMask = shadowRoot.querySelector('.input-mask');
325330
expect(inputMask.textContent).to.be.equal(' 0', '`input-mask` has correct value');
@@ -415,7 +420,7 @@ describe('Inputter', () => {
415420
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
416421

417422

418-
expect(validationMessage.textContent?.trim().replace(/\s\s+/g, ' ')).to.equal('What is your heating source? This field is required.', 'validation message has correct value');
423+
expect(validationMessage.textContent?.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value');
419424

420425
const validationIcon = shadowRoot.querySelector('.validation .icon');
421426
expect(validationIcon).to.not.be.null; // eslint-disable-line no-unused-expressions

packages/muon/tests/mixins/validation.test.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ describe('form-element-validation', () => {
103103
await formElement.updateComplete;
104104
let validationMessage = shadowRoot.querySelector('.validation');
105105
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
106-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value');
106+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value');
107107

108108
await fillIn(inputElement, 'hello world');
109109
expect(formElement.value).to.equal('hello world', '`value` property has value `hello world`');
@@ -113,7 +113,7 @@ describe('form-element-validation', () => {
113113
await formElement.updateComplete;
114114
validationMessage = shadowRoot.querySelector('.validation');
115115
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
116-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label Length must be between 5 and 10 characters.', 'validation message has correct value');
116+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Length must be between 5 and 10 characters.', 'validation message has correct value');
117117
});
118118

119119
it('text validation on input', async () => {
@@ -147,7 +147,7 @@ describe('form-element-validation', () => {
147147
await formElement.updateComplete;
148148
let validationMessage = shadowRoot.querySelector('.validation');
149149
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
150-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value');
150+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value');
151151

152152
await fillIn(inputElement, 'hello world', 'input');
153153
expect(formElement.value).to.equal('hello world', '`value` property has value `hello world`');
@@ -157,7 +157,7 @@ describe('form-element-validation', () => {
157157
await formElement.updateComplete;
158158
validationMessage = shadowRoot.querySelector('.validation');
159159
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
160-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label Length must be between 5 and 10 characters.', 'validation message has correct value');
160+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Length must be between 5 and 10 characters.', 'validation message has correct value');
161161
});
162162

163163
it('text native validation', async () => {
@@ -191,7 +191,7 @@ describe('form-element-validation', () => {
191191
await formElement.updateComplete;
192192
let validationMessage = shadowRoot.querySelector('.validation');
193193
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
194-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ').toLowerCase()).contains('input label this field is required', 'validation message has correct value');
194+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ').toLowerCase()).contains('this field is required', 'validation message has correct value');
195195

196196
await fillIn(inputElement, 'test validation');
197197
expect(formElement.value).to.equal('test validation', '`value` property has value `test validation`');
@@ -234,7 +234,7 @@ describe('form-element-validation', () => {
234234
await formElement.updateComplete;
235235
let validationMessage = shadowRoot.querySelector('.validation');
236236
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
237-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value');
237+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value');
238238

239239
await fillIn(inputElement, '56');
240240
expect(formElement.value).to.equal('56', '`value` property has value `56`');
@@ -244,7 +244,7 @@ describe('form-element-validation', () => {
244244
await formElement.updateComplete;
245245
validationMessage = shadowRoot.querySelector('.validation');
246246
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
247-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label match the pattern.', 'validation message has correct value');
247+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('match the pattern.', 'validation message has correct value');
248248

249249
});
250250

@@ -309,7 +309,7 @@ describe('form-element-validation', () => {
309309
await formElement.updateComplete;
310310
const validationMessage = shadowRoot.querySelector('.validation');
311311
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
312-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('What is your heating source? This field is required.', 'validation message has correct value');
312+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value');
313313
});
314314

315315
it('checkbox validation', async () => {
@@ -341,7 +341,7 @@ describe('form-element-validation', () => {
341341
expect(changeEventSpy.callCount).to.equal(1, '`change` event fired');
342342
const validationMessage = shadowRoot.querySelector('.validation');
343343
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
344-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('What is your heating source? This field is required.', 'validation message has correct value');
344+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value');
345345
});
346346

347347
it('select validation', async () => {
@@ -373,7 +373,7 @@ describe('form-element-validation', () => {
373373
expect(changeEventSpy.callCount).to.equal(1, '`change` event fired');
374374
const validationMessage = shadowRoot.querySelector('.validation');
375375
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
376-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('What is your heating source? This field is required.', 'validation message has correct value');
376+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value');
377377
});
378378

379379
it('date validation', async () => {
@@ -408,7 +408,7 @@ describe('form-element-validation', () => {
408408

409409
let validationMessage = shadowRoot.querySelector('.validation');
410410
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
411-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value');
411+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value');
412412

413413
await fillIn(inputElement, '10/11/2021');
414414
await formElement.updateComplete;
@@ -418,6 +418,6 @@ describe('form-element-validation', () => {
418418

419419
validationMessage = shadowRoot.querySelector('.validation');
420420
expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions
421-
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label Date must be on or after 11/11/2021.', 'validation message has correct value');
421+
expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Date must be on or after 11/11/2021.', 'validation message has correct value');
422422
});
423423
});

0 commit comments

Comments
 (0)