From 0b23261a8081a655ab6f56874c8d2f1d6e1d98b3 Mon Sep 17 00:00:00 2001 From: Chris Bubernak Date: Wed, 10 Feb 2021 14:20:37 -0800 Subject: [PATCH] feat(material/form-field): make mat-errors more polite Make the "politeness" of mat-error "polite" by default (instead of "assertive") and also make this configurable via an input binding. Fixes #21781 --- .../mdc-chips/chip-grid.spec.ts | 4 ++-- .../mdc-form-field/directives/error.ts | 12 ++++++++++-- src/material-experimental/mdc-input/input.spec.ts | 4 ++-- src/material/chips/chip-list.spec.ts | 4 ++-- src/material/form-field/error.ts | 12 ++++++++++-- src/material/input/input.spec.ts | 4 ++-- tools/public_api_guard/material/form-field.d.ts | 3 ++- 7 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/material-experimental/mdc-chips/chip-grid.spec.ts b/src/material-experimental/mdc-chips/chip-grid.spec.ts index 2f06effcd7a8..7488bb090da3 100644 --- a/src/material-experimental/mdc-chips/chip-grid.spec.ts +++ b/src/material-experimental/mdc-chips/chip-grid.spec.ts @@ -905,11 +905,11 @@ describe('MDC-based MatChipGrid', () => { }); })); - it('should set the proper role on the error messages', () => { + it('should set the proper aria-live attribute on the error messages', () => { errorTestComponent.formControl.markAsTouched(); fixture.detectChanges(); - expect(containerEl.querySelector('mat-error')!.getAttribute('role')).toBe('alert'); + expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite'); }); it('sets the aria-describedby to reference errors when in error state', () => { diff --git a/src/material-experimental/mdc-form-field/directives/error.ts b/src/material-experimental/mdc-form-field/directives/error.ts index fc65c1b9e30f..20181978b9fb 100644 --- a/src/material-experimental/mdc-form-field/directives/error.ts +++ b/src/material-experimental/mdc-form-field/directives/error.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, InjectionToken, Input} from '@angular/core'; +import {Attribute, Directive, ElementRef, InjectionToken, Input} from '@angular/core'; let nextUniqueId = 0; @@ -22,11 +22,19 @@ export const MAT_ERROR = new InjectionToken('MatError'); selector: 'mat-error', host: { 'class': 'mat-mdc-form-field-error mat-mdc-form-field-bottom-align', - 'role': 'alert', + 'aria-atomic': 'true', '[id]': 'id', }, providers: [{provide: MAT_ERROR, useExisting: MatError}], }) export class MatError { @Input() id: string = `mat-mdc-error-${nextUniqueId++}`; + + constructor(@Attribute('aria-live') ariaLive: string, elementRef: ElementRef) { + // If no aria-live value is set add 'polite' as a default. This is preferred over setting + // role='alert' so that screen readers do not interrupt the current task to read this aloud. + if (!ariaLive) { + elementRef.nativeElement.setAttribute('aria-live', 'polite'); + } + } } diff --git a/src/material-experimental/mdc-input/input.spec.ts b/src/material-experimental/mdc-input/input.spec.ts index 19749729ee05..19c18e018709 100644 --- a/src/material-experimental/mdc-input/input.spec.ts +++ b/src/material-experimental/mdc-input/input.spec.ts @@ -1037,11 +1037,11 @@ describe('MatMdcInput with forms', () => { .toBe(1, 'Expected one hint to still be shown.'); })); - it('should set the proper role on the error messages', fakeAsync(() => { + it('should set the proper aria-live attribute on the error messages', fakeAsync(() => { testComponent.formControl.markAsTouched(); fixture.detectChanges(); - expect(containerEl.querySelector('mat-error')!.getAttribute('role')).toBe('alert'); + expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite'); })); it('sets the aria-describedby to reference errors when in error state', fakeAsync(() => { diff --git a/src/material/chips/chip-list.spec.ts b/src/material/chips/chip-list.spec.ts index cfe5338706a4..47b6eb53cf1c 100644 --- a/src/material/chips/chip-list.spec.ts +++ b/src/material/chips/chip-list.spec.ts @@ -1282,11 +1282,11 @@ describe('MatChipList', () => { }); })); - it('should set the proper role on the error messages', () => { + it('should set the proper aria-live attribute on the error messages', () => { errorTestComponent.formControl.markAsTouched(); fixture.detectChanges(); - expect(containerEl.querySelector('mat-error')!.getAttribute('role')).toBe('alert'); + expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite'); }); it('sets the aria-describedby to reference errors when in error state', () => { diff --git a/src/material/form-field/error.ts b/src/material/form-field/error.ts index 0741e8c6e629..196652f3aa0b 100644 --- a/src/material/form-field/error.ts +++ b/src/material/form-field/error.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, InjectionToken, Input} from '@angular/core'; +import {Attribute, Directive, ElementRef, InjectionToken, Input} from '@angular/core'; let nextUniqueId = 0; @@ -22,11 +22,19 @@ export const MAT_ERROR = new InjectionToken('MatError'); selector: 'mat-error', host: { 'class': 'mat-error', - 'role': 'alert', '[attr.id]': 'id', + 'aria-atomic': 'true', }, providers: [{provide: MAT_ERROR, useExisting: MatError}], }) export class MatError { @Input() id: string = `mat-error-${nextUniqueId++}`; + + constructor(@Attribute('aria-live') ariaLive: string, elementRef: ElementRef) { + // If no aria-live value is set add 'polite' as a default. This is preferred over setting + // role='alert' so that screen readers do not interrupt the current task to read this aloud. + if (!ariaLive) { + elementRef.nativeElement.setAttribute('aria-live', 'polite'); + } + } } diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts index 6864c2012b34..e4135690f60e 100644 --- a/src/material/input/input.spec.ts +++ b/src/material/input/input.spec.ts @@ -1193,11 +1193,11 @@ describe('MatInput with forms', () => { .toBe(1, 'Expected one hint to still be shown.'); })); - it('should set the proper role on the error messages', fakeAsync(() => { + it('should set the proper aria-live attribute on the error messages', fakeAsync(() => { testComponent.formControl.markAsTouched(); fixture.detectChanges(); - expect(containerEl.querySelector('mat-error')!.getAttribute('role')).toBe('alert'); + expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite'); })); it('sets the aria-describedby to reference errors when in error state', fakeAsync(() => { diff --git a/tools/public_api_guard/material/form-field.d.ts b/tools/public_api_guard/material/form-field.d.ts index 28a6105e6481..95a6e6e55335 100644 --- a/tools/public_api_guard/material/form-field.d.ts +++ b/tools/public_api_guard/material/form-field.d.ts @@ -20,8 +20,9 @@ export declare const MAT_SUFFIX: InjectionToken; export declare class MatError { id: string; + constructor(ariaLive: string, elementRef: ElementRef); static ɵdir: i0.ɵɵDirectiveDefWithMeta; - static ɵfac: i0.ɵɵFactoryDef; + static ɵfac: i0.ɵɵFactoryDef; } export declare class MatFormField extends _MatFormFieldMixinBase implements AfterContentInit, AfterContentChecked, AfterViewInit, OnDestroy, CanColor {