diff --git a/src/cdk/a11y/live-announcer/live-announcer-token.ts b/src/cdk/a11y/live-announcer/live-announcer-token.ts deleted file mode 100644 index ce03b95033e7..000000000000 --- a/src/cdk/a11y/live-announcer/live-announcer-token.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {InjectionToken} from '@angular/core'; - -// The token for the live announcer element is defined in a separate file from LiveAnnouncer -// as a workaround for https://github.com/angular/angular/issues/22559 - -export const LIVE_ANNOUNCER_ELEMENT_TOKEN = - new InjectionToken('liveAnnouncerElement', { - providedIn: 'root', - factory: LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY, - }); - -/** @docs-private */ -export function LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY(): null { - return null; -} diff --git a/src/cdk/a11y/live-announcer/live-announcer-tokens.ts b/src/cdk/a11y/live-announcer/live-announcer-tokens.ts new file mode 100644 index 000000000000..1f9fd83233b6 --- /dev/null +++ b/src/cdk/a11y/live-announcer/live-announcer-tokens.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {InjectionToken} from '@angular/core'; + +// The tokens for the live announcer are defined in a separate file from LiveAnnouncer +// as a workaround for https://github.com/angular/angular/issues/22559 + +/** Possible politeness levels. */ +export type AriaLivePoliteness = 'off' | 'polite' | 'assertive'; + +export const LIVE_ANNOUNCER_ELEMENT_TOKEN = + new InjectionToken('liveAnnouncerElement', { + providedIn: 'root', + factory: LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY, + }); + +/** @docs-private */ +export function LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY(): null { + return null; +} + +/** Object that can be used to configure the default options for the LiveAnnouncer. */ +export interface LiveAnnouncerDefaultOptions { + /** Default politeness for the announcements. */ + politeness?: AriaLivePoliteness; + + /** Default duration for the announcement messages. */ + duration?: number; +} + +/** Injection token that can be used to configure the default options for the LiveAnnouncer. */ +export const LIVE_ANNOUNCER_DEFAULT_OPTIONS = + new InjectionToken('LIVE_ANNOUNCER_DEFAULT_OPTIONS'); diff --git a/src/cdk/a11y/live-announcer/live-announcer.spec.ts b/src/cdk/a11y/live-announcer/live-announcer.spec.ts index b95707b071a5..b12c3a148fd5 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.spec.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.spec.ts @@ -4,7 +4,11 @@ import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angula import {By} from '@angular/platform-browser'; import {A11yModule} from '../index'; import {LiveAnnouncer} from './live-announcer'; -import {LIVE_ANNOUNCER_ELEMENT_TOKEN} from './live-announcer-token'; +import { + LIVE_ANNOUNCER_ELEMENT_TOKEN, + LIVE_ANNOUNCER_DEFAULT_OPTIONS, + LiveAnnouncerDefaultOptions, +} from './live-announcer-tokens'; describe('LiveAnnouncer', () => { @@ -189,6 +193,47 @@ describe('LiveAnnouncer', () => { expect(customLiveElement.textContent).toBe('Custom Element'); })); }); + + describe('with a default options', () => { + beforeEach(() => { + return TestBed.configureTestingModule({ + imports: [A11yModule], + declarations: [TestApp], + providers: [{ + provide: LIVE_ANNOUNCER_DEFAULT_OPTIONS, + useValue: { + politeness: 'assertive', + duration: 1337 + } as LiveAnnouncerDefaultOptions + }], + }); + }); + + beforeEach(inject([LiveAnnouncer], (la: LiveAnnouncer) => { + announcer = la; + ariaLiveElement = getLiveElement(); + })); + + it('should pick up the default politeness from the injection token', fakeAsync(() => { + announcer.announce('Hello'); + + tick(2000); + + expect(ariaLiveElement.getAttribute('aria-live')).toBe('assertive'); + })); + + it('should pick up the default politeness from the injection token', fakeAsync(() => { + announcer.announce('Hello'); + + tick(100); + expect(ariaLiveElement.textContent).toBe('Hello'); + + tick(1337); + expect(ariaLiveElement.textContent).toBeFalsy(); + })); + + }); + }); describe('CdkAriaLive', () => { diff --git a/src/cdk/a11y/live-announcer/live-announcer.ts b/src/cdk/a11y/live-announcer/live-announcer.ts index 2668425a8083..5488f91a708e 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.ts @@ -21,11 +21,13 @@ import { SkipSelf, } from '@angular/core'; import {Subscription} from 'rxjs'; -import {LIVE_ANNOUNCER_ELEMENT_TOKEN} from './live-announcer-token'; - +import { + AriaLivePoliteness, + LiveAnnouncerDefaultOptions, + LIVE_ANNOUNCER_ELEMENT_TOKEN, + LIVE_ANNOUNCER_DEFAULT_OPTIONS, +} from './live-announcer-tokens'; -/** Possible politeness levels. */ -export type AriaLivePoliteness = 'off' | 'polite' | 'assertive'; @Injectable({providedIn: 'root'}) export class LiveAnnouncer implements OnDestroy { @@ -36,7 +38,9 @@ export class LiveAnnouncer implements OnDestroy { constructor( @Optional() @Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN) elementToken: any, private _ngZone: NgZone, - @Inject(DOCUMENT) _document: any) { + @Inject(DOCUMENT) _document: any, + @Optional() @Inject(LIVE_ANNOUNCER_DEFAULT_OPTIONS) + private _defaultOptions?: LiveAnnouncerDefaultOptions) { // We inject the live element and document as `any` because the constructor signature cannot // reference browser globals (HTMLElement, Document) on non-browser environments, since having @@ -82,8 +86,9 @@ export class LiveAnnouncer implements OnDestroy { announce(message: string, politeness?: AriaLivePoliteness, duration?: number): Promise; announce(message: string, ...args: any[]): Promise { - let politeness: AriaLivePoliteness; - let duration: number; + const defaultOptions = this._defaultOptions; + let politeness: AriaLivePoliteness | undefined; + let duration: number | undefined; if (args.length === 1 && typeof args[0] === 'number') { duration = args[0]; @@ -94,8 +99,17 @@ export class LiveAnnouncer implements OnDestroy { this.clear(); clearTimeout(this._previousTimeout); + if (!politeness) { + politeness = + (defaultOptions && defaultOptions.politeness) ? defaultOptions.politeness : 'polite'; + } + + if (duration == null && defaultOptions) { + duration = defaultOptions.duration; + } + // TODO: ensure changing the politeness works on all environments we support. - this._liveElement.setAttribute('aria-live', politeness! || 'polite'); + this._liveElement.setAttribute('aria-live', politeness); // This 100ms timeout is necessary for some browser + screen-reader combinations: // - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout. diff --git a/src/cdk/a11y/public-api.ts b/src/cdk/a11y/public-api.ts index 367db5804dc5..3416529b9c6d 100644 --- a/src/cdk/a11y/public-api.ts +++ b/src/cdk/a11y/public-api.ts @@ -12,7 +12,7 @@ export * from './key-manager/list-key-manager'; export * from './focus-trap/focus-trap'; export * from './interactivity-checker/interactivity-checker'; export * from './live-announcer/live-announcer'; -export * from './live-announcer/live-announcer-token'; +export * from './live-announcer/live-announcer-tokens'; export * from './focus-monitor/focus-monitor'; export * from './fake-mousedown'; export * from './a11y-module'; diff --git a/tools/public_api_guard/cdk/a11y.d.ts b/tools/public_api_guard/cdk/a11y.d.ts index fc31c025de80..8376cd054875 100644 --- a/tools/public_api_guard/cdk/a11y.d.ts +++ b/tools/public_api_guard/cdk/a11y.d.ts @@ -150,6 +150,8 @@ export interface ListKeyManagerOption { getLabel?(): string; } +export declare const LIVE_ANNOUNCER_DEFAULT_OPTIONS: InjectionToken; + export declare const LIVE_ANNOUNCER_ELEMENT_TOKEN: InjectionToken; export declare function LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY(): null; @@ -159,15 +161,20 @@ export declare const LIVE_ANNOUNCER_PROVIDER: Provider; export declare function LIVE_ANNOUNCER_PROVIDER_FACTORY(parentAnnouncer: LiveAnnouncer, liveElement: any, _document: any, ngZone: NgZone): LiveAnnouncer; export declare class LiveAnnouncer implements OnDestroy { - constructor(elementToken: any, _ngZone: NgZone, _document: any); - announce(message: string): Promise; + constructor(elementToken: any, _ngZone: NgZone, _document: any, _defaultOptions?: LiveAnnouncerDefaultOptions | undefined); + announce(message: string, politeness?: AriaLivePoliteness): Promise; announce(message: string, duration?: number): Promise; announce(message: string, politeness?: AriaLivePoliteness, duration?: number): Promise; - announce(message: string, politeness?: AriaLivePoliteness): Promise; + announce(message: string): Promise; clear(): void; ngOnDestroy(): void; } +export interface LiveAnnouncerDefaultOptions { + duration?: number; + politeness?: AriaLivePoliteness; +} + export declare const MESSAGES_CONTAINER_ID = "cdk-describedby-message-container"; export interface RegisteredMessage {