From 2c69e45ef8d4dcd904885dd671344e1b4fce1ca1 Mon Sep 17 00:00:00 2001 From: MekalaNagarajan_Centrica Date: Wed, 6 Nov 2024 11:23:50 +0000 Subject: [PATCH 1/6] feat: validation message to input element mapping --- .../inputter/src/inputter-component.js | 22 +++++++++++++++++++ .../inputter/src/inputter-styles.slotted.css | 3 +++ packages/muon/mixins/validation-mixin.js | 6 ++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/muon/components/inputter/src/inputter-component.js b/packages/muon/components/inputter/src/inputter-component.js index e45ad3b5..87da708a 100644 --- a/packages/muon/components/inputter/src/inputter-component.js +++ b/packages/muon/components/inputter/src/inputter-component.js @@ -98,6 +98,28 @@ export class Inputter extends ScopedElementsMixin(ValidationMixin(MaskMixin(Muon this._helperId = `${this._randomId}-helper`; } + willUpdate(changedProperties) { + super.willUpdate(changedProperties); + + let validationEle = this.querySelector('#validation-message'); + if (!validationEle) { + validationEle = document.createElement('div'); + validationEle.setAttribute('class', 'visually-hidden'); + validationEle.setAttribute('id', 'validation-message'); + this.appendChild(validationEle); + } + if (this._shouldShowValidation) { + this._slottedInputs[0].setAttribute('aria-errormessage', 'validation-message'); + this._slottedInputs[0].setAttribute('aria-describedby', 'validation-message'); + this._slottedInputs[0].setAttribute('aria-invalid', 'true'); + } else { + this._slottedInputs[0].removeAttribute('aria-errormessage'); + this._slottedInputs[0].removeAttribute('aria-describedby'); + this._slottedInputs[0].removeAttribute('aria-invalid'); + } + validationEle.textContent = this.validationMessage; + } + _onChange(changeEvent) { this._pristine = false; changeEvent.stopPropagation(); diff --git a/packages/muon/components/inputter/src/inputter-styles.slotted.css b/packages/muon/components/inputter/src/inputter-styles.slotted.css index c48f2eed..c870e289 100644 --- a/packages/muon/components/inputter/src/inputter-styles.slotted.css +++ b/packages/muon/components/inputter/src/inputter-styles.slotted.css @@ -1,6 +1,9 @@ @import "./inputter-extends.css"; +@import "../../../css/accessibility.css"; light-dom { + @extend %global-accessibility; + /* NOTE: targeting Safari only */ @media not all and (min-resolution: 0.001dpcm) { /* stylelint-disable-line media-feature-range-notation */ /* diff --git a/packages/muon/mixins/validation-mixin.js b/packages/muon/mixins/validation-mixin.js index dd44d9c9..2cf83ac6 100644 --- a/packages/muon/mixins/validation-mixin.js +++ b/packages/muon/mixins/validation-mixin.js @@ -221,6 +221,10 @@ export const ValidationMixin = dedupeMixin((superClass) => }).join(' '); } + get _shouldShowValidation() { + return this.showMessage && this.isDirty && !!this.validationMessage; + } + /** * A method to get validation message template. * @@ -229,7 +233,7 @@ export const ValidationMixin = dedupeMixin((superClass) => * @override */ get _addValidationMessage() { - if (this.showMessage && this.isDirty && this.validationMessage) { + if (this._shouldShowValidation) { return html`
${this._addValidationIcon} From 2b6b9d14bea5699264d3e42e4d87981f1cf19197 Mon Sep 17 00:00:00 2001 From: MekalaNagarajan_Centrica Date: Wed, 6 Nov 2024 11:43:39 +0000 Subject: [PATCH 2/6] feat: unique validation message id --- .../muon/components/inputter/src/inputter-component.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/muon/components/inputter/src/inputter-component.js b/packages/muon/components/inputter/src/inputter-component.js index 87da708a..790eb278 100644 --- a/packages/muon/components/inputter/src/inputter-component.js +++ b/packages/muon/components/inputter/src/inputter-component.js @@ -101,16 +101,16 @@ export class Inputter extends ScopedElementsMixin(ValidationMixin(MaskMixin(Muon willUpdate(changedProperties) { super.willUpdate(changedProperties); - let validationEle = this.querySelector('#validation-message'); + let validationEle = this.querySelector(`${this._id}-validation`); if (!validationEle) { validationEle = document.createElement('div'); validationEle.setAttribute('class', 'visually-hidden'); - validationEle.setAttribute('id', 'validation-message'); + validationEle.setAttribute('id', `${this._id}-validation`); this.appendChild(validationEle); } if (this._shouldShowValidation) { - this._slottedInputs[0].setAttribute('aria-errormessage', 'validation-message'); - this._slottedInputs[0].setAttribute('aria-describedby', 'validation-message'); + this._slottedInputs[0].setAttribute('aria-errormessage', `${this._id}-validation`); + this._slottedInputs[0].setAttribute('aria-describedby', `${this._id}-validation`); this._slottedInputs[0].setAttribute('aria-invalid', 'true'); } else { this._slottedInputs[0].removeAttribute('aria-errormessage'); From 7abeb99af524ed31e6478ed2e5f9b9dfe0c60454 Mon Sep 17 00:00:00 2001 From: MekalaNagarajan_Centrica Date: Wed, 6 Nov 2024 11:49:49 +0000 Subject: [PATCH 3/6] fix: validation id attribute --- packages/muon/components/inputter/src/inputter-component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/muon/components/inputter/src/inputter-component.js b/packages/muon/components/inputter/src/inputter-component.js index 790eb278..3582d567 100644 --- a/packages/muon/components/inputter/src/inputter-component.js +++ b/packages/muon/components/inputter/src/inputter-component.js @@ -101,7 +101,7 @@ export class Inputter extends ScopedElementsMixin(ValidationMixin(MaskMixin(Muon willUpdate(changedProperties) { super.willUpdate(changedProperties); - let validationEle = this.querySelector(`${this._id}-validation`); + let validationEle = this.querySelector(`#${this._id}-validation`); if (!validationEle) { validationEle = document.createElement('div'); validationEle.setAttribute('class', 'visually-hidden'); From bbd4eec1e72039c62c5e04fd40f5bf7dc3c019ed Mon Sep 17 00:00:00 2001 From: MekalaNagarajan_Centrica Date: Wed, 6 Nov 2024 14:02:55 +0000 Subject: [PATCH 4/6] feat: move validation screen reader accessibility to light dom --- .../inputter/src/inputter-component.js | 17 ++++++++++------- packages/muon/mixins/validation-mixin.js | 3 +-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/muon/components/inputter/src/inputter-component.js b/packages/muon/components/inputter/src/inputter-component.js index 3582d567..a3392b23 100644 --- a/packages/muon/components/inputter/src/inputter-component.js +++ b/packages/muon/components/inputter/src/inputter-component.js @@ -108,16 +108,19 @@ export class Inputter extends ScopedElementsMixin(ValidationMixin(MaskMixin(Muon validationEle.setAttribute('id', `${this._id}-validation`); this.appendChild(validationEle); } + const slottedInput = this._slottedInputs[0]; if (this._shouldShowValidation) { - this._slottedInputs[0].setAttribute('aria-errormessage', `${this._id}-validation`); - this._slottedInputs[0].setAttribute('aria-describedby', `${this._id}-validation`); - this._slottedInputs[0].setAttribute('aria-invalid', 'true'); + validationEle.setAttribute('aria-live', 'polite'); + slottedInput?.setAttribute('aria-errormessage', `${this._id}-validation`); + slottedInput?.setAttribute('aria-describedby', `${this._id}-validation`); + slottedInput?.setAttribute('aria-invalid', 'true'); + validationEle.textContent = `${this._isMultiple ? this.heading : this._slottedLabel?.textContent} ${this.validationMessage}`; } else { - this._slottedInputs[0].removeAttribute('aria-errormessage'); - this._slottedInputs[0].removeAttribute('aria-describedby'); - this._slottedInputs[0].removeAttribute('aria-invalid'); + slottedInput?.removeAttribute('aria-errormessage'); + slottedInput?.removeAttribute('aria-describedby'); + slottedInput?.removeAttribute('aria-invalid'); + validationEle.textContent = ''; } - validationEle.textContent = this.validationMessage; } _onChange(changeEvent) { diff --git a/packages/muon/mixins/validation-mixin.js b/packages/muon/mixins/validation-mixin.js index 2cf83ac6..3f4ae47e 100644 --- a/packages/muon/mixins/validation-mixin.js +++ b/packages/muon/mixins/validation-mixin.js @@ -237,8 +237,7 @@ export const ValidationMixin = dedupeMixin((superClass) => return html`
${this._addValidationIcon} - `; From bff1d113a8ba776a50465d3c98a08716d460c8f1 Mon Sep 17 00:00:00 2001 From: MekalaNagarajan_Centrica Date: Wed, 6 Nov 2024 15:32:47 +0000 Subject: [PATCH 5/6] feat: remove aria-describedby --- packages/muon/components/inputter/src/inputter-component.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/muon/components/inputter/src/inputter-component.js b/packages/muon/components/inputter/src/inputter-component.js index a3392b23..62365f40 100644 --- a/packages/muon/components/inputter/src/inputter-component.js +++ b/packages/muon/components/inputter/src/inputter-component.js @@ -112,12 +112,10 @@ export class Inputter extends ScopedElementsMixin(ValidationMixin(MaskMixin(Muon if (this._shouldShowValidation) { validationEle.setAttribute('aria-live', 'polite'); slottedInput?.setAttribute('aria-errormessage', `${this._id}-validation`); - slottedInput?.setAttribute('aria-describedby', `${this._id}-validation`); slottedInput?.setAttribute('aria-invalid', 'true'); validationEle.textContent = `${this._isMultiple ? this.heading : this._slottedLabel?.textContent} ${this.validationMessage}`; } else { slottedInput?.removeAttribute('aria-errormessage'); - slottedInput?.removeAttribute('aria-describedby'); slottedInput?.removeAttribute('aria-invalid'); validationEle.textContent = ''; } From af3868de0c2b8204c2d22cde3bca8e634bc4f2a2 Mon Sep 17 00:00:00 2001 From: MekalaNagarajan_Centrica Date: Wed, 6 Nov 2024 16:46:50 +0000 Subject: [PATCH 6/6] test: test fixes --- .../components/inputter/inputter.test.js | 17 ++++++++----- packages/muon/tests/mixins/validation.test.js | 24 +++++++++---------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/muon/tests/components/inputter/inputter.test.js b/packages/muon/tests/components/inputter/inputter.test.js index e8a43b01..ed489efe 100644 --- a/packages/muon/tests/components/inputter/inputter.test.js +++ b/packages/muon/tests/components/inputter/inputter.test.js @@ -177,8 +177,13 @@ describe('Inputter', () => { const validationMessage = shadowRoot.querySelector('.validation .message'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - 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'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Length must be between 8 and 20 characters.', 'validation message has correct value'); + const validationId = `${inputter._id}-validation`; + const validationLightDOM = inputter.querySelector(`#${validationId}`); + // eslint-disable-next-line no-unused-expressions + expect(validationLightDOM).to.be.ok; + expect(inputElement.getAttribute('aria-errormessage')).to.be.equal(validationId); const validationIcon = shadowRoot.querySelector('.validation .icon'); expect(validationIcon).to.not.be.null; // eslint-disable-line no-unused-expressions expect(validationIcon.name).to.equal('exclamation-circle', 'validation icon has correct value'); @@ -210,7 +215,7 @@ describe('Inputter', () => { const validationMessage = shadowRoot.querySelector('.validation .message'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - 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'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Length must be between 8 and 20 characters.', 'validation message has correct value'); const validationIcon = shadowRoot.querySelector('.validation .icon'); expect(validationIcon).to.not.be.null; // eslint-disable-line no-unused-expressions @@ -251,7 +256,7 @@ describe('Inputter', () => { const validationMessage = shadowRoot.querySelector('.validation .message'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value'); const validationIcon = shadowRoot.querySelector('.validation .icon'); expect(validationIcon).to.not.be.null; // eslint-disable-line no-unused-expressions @@ -309,7 +314,7 @@ describe('Inputter', () => { const validationMessage = shadowRoot.querySelector('.validation .message'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value'); const validationIcon = shadowRoot.querySelector('.validation .icon'); expect(validationIcon).to.not.be.null; // eslint-disable-line no-unused-expressions @@ -319,7 +324,7 @@ describe('Inputter', () => { await inputter.updateComplete; expect(changeEventSpy.callCount).to.equal(3, '`change` event fired'); expect(changeEventSpy.lastCall.args[0].detail.value).to.equal('12-3', '`change` event has value `12-3`'); - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label Length must be at least 4 characters.', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Length must be at least 4 characters.', 'validation message has correct value'); inputMask = shadowRoot.querySelector('.input-mask'); expect(inputMask.textContent).to.be.equal(' 0', '`input-mask` has correct value'); @@ -415,7 +420,7 @@ describe('Inputter', () => { expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent?.trim().replace(/\s\s+/g, ' ')).to.equal('What is your heating source? This field is required.', 'validation message has correct value'); + expect(validationMessage.textContent?.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value'); const validationIcon = shadowRoot.querySelector('.validation .icon'); expect(validationIcon).to.not.be.null; // eslint-disable-line no-unused-expressions diff --git a/packages/muon/tests/mixins/validation.test.js b/packages/muon/tests/mixins/validation.test.js index 56344bb8..f65e8a5e 100644 --- a/packages/muon/tests/mixins/validation.test.js +++ b/packages/muon/tests/mixins/validation.test.js @@ -103,7 +103,7 @@ describe('form-element-validation', () => { await formElement.updateComplete; let validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value'); await fillIn(inputElement, 'hello world'); expect(formElement.value).to.equal('hello world', '`value` property has value `hello world`'); @@ -113,7 +113,7 @@ describe('form-element-validation', () => { await formElement.updateComplete; validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - 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'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Length must be between 5 and 10 characters.', 'validation message has correct value'); }); it('text validation on input', async () => { @@ -147,7 +147,7 @@ describe('form-element-validation', () => { await formElement.updateComplete; let validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value'); await fillIn(inputElement, 'hello world', 'input'); expect(formElement.value).to.equal('hello world', '`value` property has value `hello world`'); @@ -157,7 +157,7 @@ describe('form-element-validation', () => { await formElement.updateComplete; validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - 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'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Length must be between 5 and 10 characters.', 'validation message has correct value'); }); it('text native validation', async () => { @@ -191,7 +191,7 @@ describe('form-element-validation', () => { await formElement.updateComplete; let validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ').toLowerCase()).contains('input label this field is required', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ').toLowerCase()).contains('this field is required', 'validation message has correct value'); await fillIn(inputElement, 'test validation'); expect(formElement.value).to.equal('test validation', '`value` property has value `test validation`'); @@ -234,7 +234,7 @@ describe('form-element-validation', () => { await formElement.updateComplete; let validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value'); await fillIn(inputElement, '56'); expect(formElement.value).to.equal('56', '`value` property has value `56`'); @@ -244,7 +244,7 @@ describe('form-element-validation', () => { await formElement.updateComplete; validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label match the pattern.', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('match the pattern.', 'validation message has correct value'); }); @@ -309,7 +309,7 @@ describe('form-element-validation', () => { await formElement.updateComplete; const validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('What is your heating source? This field is required.', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value'); }); it('checkbox validation', async () => { @@ -341,7 +341,7 @@ describe('form-element-validation', () => { expect(changeEventSpy.callCount).to.equal(1, '`change` event fired'); const validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('What is your heating source? This field is required.', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value'); }); it('select validation', async () => { @@ -373,7 +373,7 @@ describe('form-element-validation', () => { expect(changeEventSpy.callCount).to.equal(1, '`change` event fired'); const validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('What is your heating source? This field is required.', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value'); }); it('date validation', async () => { @@ -408,7 +408,7 @@ describe('form-element-validation', () => { let validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('input label This field is required.', 'validation message has correct value'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('This field is required.', 'validation message has correct value'); await fillIn(inputElement, '10/11/2021'); await formElement.updateComplete; @@ -418,6 +418,6 @@ describe('form-element-validation', () => { validationMessage = shadowRoot.querySelector('.validation'); expect(validationMessage).to.not.be.null; // eslint-disable-line no-unused-expressions - 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'); + expect(validationMessage.textContent.trim().replace(/\s\s+/g, ' ')).to.equal('Date must be on or after 11/11/2021.', 'validation message has correct value'); }); });