diff --git a/packages/scripts/dist/a11y.d.ts b/packages/scripts/dist/a11y.d.ts index 0880a7f4..7cf28478 100644 --- a/packages/scripts/dist/a11y.d.ts +++ b/packages/scripts/dist/a11y.d.ts @@ -1,9 +1,11 @@ export declare class A11y { private logger; private observer; + private liveRegionUpdateTimeout; + private shouldFocusFirstInvalidField; constructor(); /** - * Apply the field-level accessibility tagging (error alert live regions, + * Apply the field-level accessibility tagging (error containers, * aria-required, fallback aria-labels, radio group roles) to every field * within `root`. Defaults to the whole document on initial load, but can be * pointed at a freshly injected fragment (e.g. a Supporter Hub overlay) so @@ -11,22 +13,23 @@ export declare class A11y { * idempotent, so re-scanning already tagged fields is safe. */ static scanFields(root?: ParentNode): void; - private static addErrorAlertArea; + private static addGlobalErrorLiveRegion; private static addGroupRole; private static addRequired; private static addLabel; private updateFrequencyLabel; private setAutoGeneratedAltTags; /** - * Observe #engrid for .en__field__error additions and removals, mirroring - * text into the per-field .en__field__error__alert live region and toggling - * aria-invalid / aria-describedby on the corresponding input. Runs for the - * lifetime of the page so async validators (NeverBounce, VGS, server - * re-renders) are caught without timing assumptions. + * Observe #engrid for .en__field__error additions and removals, relocating + * each error after its field's input so DOM order matches visual order. Also + * toggles aria-invalid / aria-describedby on the corresponding input (or radio + * group when the field has role="group") and updates a single, DOM-ordered + * global live region. Runs for the lifetime of the page so async validators + * (NeverBounce, VGS, server re-renders) are caught without timing assumptions. */ private observeErrorMessages; - private moveErrorMessage; - private clearErrorMessage; + private tagFieldError; + private clearFieldError; /** * Make everything on the page inert except the supplied overlay element and * its ancestors. This hides background content from assistive technology and @@ -40,5 +43,10 @@ export declare class A11y { * `inert` is true; ignored when `inert` is false. */ static inertPage(inert: boolean, overlay?: HTMLElement): void; - private manageErrorListAlertRole; + private scheduleLiveRegionUpdate; + private updateGlobalErrorLiveRegion; + private formatErrorMessage; + private focusFirstInvalidField; + private getFieldLabel; + private normalizeLabel; } diff --git a/packages/scripts/dist/a11y.js b/packages/scripts/dist/a11y.js index f16e723b..9a982de6 100644 --- a/packages/scripts/dist/a11y.js +++ b/packages/scripts/dist/a11y.js @@ -1,19 +1,32 @@ import { EngridLogger } from "./logger"; +import { ENGrid } from "./engrid"; +import { EnForm } from "./events/en-form"; // a11y means accessibility // This Component is supposed to be used as a helper for Aria Attributes & Other Accessibility Features export class A11y { constructor() { + var _a; this.logger = new EngridLogger("A11y", "#FFFFFF", "#811212", "👁️‍🗨️"); this.observer = null; + this.liveRegionUpdateTimeout = null; + this.shouldFocusFirstInvalidField = false; A11y.scanFields(); this.updateFrequencyLabel(); const ecardImages = document.querySelectorAll('.en__ecarditems__list img'); this.setAutoGeneratedAltTags(ecardImages); - this.manageErrorListAlertRole(); this.observeErrorMessages(); + (_a = ENGrid.enForm) === null || _a === void 0 ? void 0 : _a.addEventListener('submit', () => { + this.shouldFocusFirstInvalidField = true; + }); + // onSubmit only fires once validation has passed, so disarm the focus + // flag: a successful submit must not leave it set, or the next unrelated + // async field error (e.g. NeverBounce on blur) would steal focus. + EnForm.getInstance().onSubmit.subscribe(() => { + this.shouldFocusFirstInvalidField = false; + }); } /** - * Apply the field-level accessibility tagging (error alert live regions, + * Apply the field-level accessibility tagging (error containers, * aria-required, fallback aria-labels, radio group roles) to every field * within `root`. Defaults to the whole document on initial load, but can be * pointed at a freshly injected fragment (e.g. a Supporter Hub overlay) so @@ -21,24 +34,29 @@ export class A11y { * idempotent, so re-scanning already tagged fields is safe. */ static scanFields(root = document) { - A11y.addErrorAlertArea(root); + A11y.addGlobalErrorLiveRegion(root); A11y.addRequired(root); A11y.addLabel(root); A11y.addGroupRole(root); } - static addErrorAlertArea(root = document) { - const fieldElements = root.querySelectorAll('.en__field .en__field__element'); - fieldElements.forEach((fieldElement) => { - const fieldWrapper = fieldElement.closest('.en__field'); - if (fieldWrapper === null || fieldWrapper === void 0 ? void 0 : fieldWrapper.querySelector('.en__field__error__alert')) - return; - const errorAlert = document.createElement('div'); - errorAlert.setAttribute('aria-live', 'polite'); - errorAlert.setAttribute('aria-atomic', 'true'); - errorAlert.classList.add('en__field__error__alert'); - errorAlert.id = `en__field__error__alert--${Math.random().toString(36).slice(2, 7)}`; - fieldElement.insertAdjacentElement('afterend', errorAlert); - }); + static addGlobalErrorLiveRegion(root = document) { + var _a; + const liveRegionId = 'engrid-a11y-error-summary'; + if (document.getElementById(liveRegionId)) + return; + const form = (_a = ENGrid.enForm) !== null && _a !== void 0 ? _a : root.querySelector('form.en__component'); + if (!form) + return; + const region = document.createElement('div'); + region.id = liveRegionId; + region.className = 'engrid__sr-only'; + region.setAttribute('aria-live', 'polite'); + region.setAttribute('aria-atomic', 'true'); + form.insertBefore(region, form.firstChild); + const errorList = document.querySelector('ul.en__errorList'); + if (errorList === null || errorList === void 0 ? void 0 : errorList.hasAttribute('role')) { + errorList.removeAttribute('role'); + } } static addGroupRole(root = document) { // Add role="group" to all EN Radio fields @@ -52,6 +70,13 @@ export class A11y { const label = field.querySelector("label"); if (label) { label.setAttribute("id", `en__field__label--${Math.random().toString(36).slice(2, 7)}`); + // EN renders the group's question label as a