Skip to content

Commit c064bfd

Browse files
committed
feat(a11y): add injection token configure default politeness and duration
Adds an injection token that allows consumers to configure the default `politeness` and `duration` for the live announcer. Fixes #15121.
1 parent 57aadc2 commit c064bfd

File tree

6 files changed

+118
-36
lines changed

6 files changed

+118
-36
lines changed

src/cdk/a11y/live-announcer/live-announcer-token.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {InjectionToken} from '@angular/core';
10+
11+
// The tokens for the live announcer are defined in a separate file from LiveAnnouncer
12+
// as a workaround for https://github.com/angular/angular/issues/22559
13+
14+
/** Possible politeness levels. */
15+
export type AriaLivePoliteness = 'off' | 'polite' | 'assertive';
16+
17+
export const LIVE_ANNOUNCER_ELEMENT_TOKEN =
18+
new InjectionToken<HTMLElement | null>('liveAnnouncerElement', {
19+
providedIn: 'root',
20+
factory: LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY,
21+
});
22+
23+
/** @docs-private */
24+
export function LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY(): null {
25+
return null;
26+
}
27+
28+
/** Object that can be used to configure the default options for the LiveAnnouncer. */
29+
export interface LiveAnnouncerDefaultOptions {
30+
/** Default politeness for the announcements. */
31+
politeness?: AriaLivePoliteness;
32+
33+
/** Default duration for the announcement messages. */
34+
duration?: number;
35+
}
36+
37+
/** Injection token that can be used to configure the default options for the LiveAnnouncer. */
38+
export const LIVE_ANNOUNCER_DEFAULT_OPTIONS =
39+
new InjectionToken<LiveAnnouncerDefaultOptions>('LIVE_ANNOUNCER_DEFAULT_OPTIONS');

src/cdk/a11y/live-announcer/live-announcer.spec.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import {ComponentFixture, fakeAsync, flush, inject, TestBed, tick} from '@angula
44
import {By} from '@angular/platform-browser';
55
import {A11yModule} from '../index';
66
import {LiveAnnouncer} from './live-announcer';
7-
import {LIVE_ANNOUNCER_ELEMENT_TOKEN} from './live-announcer-token';
7+
import {
8+
LIVE_ANNOUNCER_ELEMENT_TOKEN,
9+
LIVE_ANNOUNCER_DEFAULT_OPTIONS,
10+
LiveAnnouncerDefaultOptions,
11+
} from './live-announcer-tokens';
812

913

1014
describe('LiveAnnouncer', () => {
@@ -189,6 +193,47 @@ describe('LiveAnnouncer', () => {
189193
expect(customLiveElement.textContent).toBe('Custom Element');
190194
}));
191195
});
196+
197+
describe('with a default options', () => {
198+
beforeEach(() => {
199+
return TestBed.configureTestingModule({
200+
imports: [A11yModule],
201+
declarations: [TestApp],
202+
providers: [{
203+
provide: LIVE_ANNOUNCER_DEFAULT_OPTIONS,
204+
useValue: {
205+
politeness: 'assertive',
206+
duration: 1337
207+
} as LiveAnnouncerDefaultOptions
208+
}],
209+
});
210+
});
211+
212+
beforeEach(inject([LiveAnnouncer], (la: LiveAnnouncer) => {
213+
announcer = la;
214+
ariaLiveElement = getLiveElement();
215+
}));
216+
217+
it('should pick up the default politeness from the injection token', fakeAsync(() => {
218+
announcer.announce('Hello');
219+
220+
tick(2000);
221+
222+
expect(ariaLiveElement.getAttribute('aria-live')).toBe('assertive');
223+
}));
224+
225+
it('should pick up the default politeness from the injection token', fakeAsync(() => {
226+
announcer.announce('Hello');
227+
228+
tick(100);
229+
expect(ariaLiveElement.textContent).toBe('Hello');
230+
231+
tick(1337);
232+
expect(ariaLiveElement.textContent).toBeFalsy();
233+
}));
234+
235+
});
236+
192237
});
193238

194239
describe('CdkAriaLive', () => {

src/cdk/a11y/live-announcer/live-announcer.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import {
2121
SkipSelf,
2222
} from '@angular/core';
2323
import {Subscription} from 'rxjs';
24-
import {LIVE_ANNOUNCER_ELEMENT_TOKEN} from './live-announcer-token';
25-
24+
import {
25+
AriaLivePoliteness,
26+
LiveAnnouncerDefaultOptions,
27+
LIVE_ANNOUNCER_ELEMENT_TOKEN,
28+
LIVE_ANNOUNCER_DEFAULT_OPTIONS,
29+
} from './live-announcer-tokens';
2630

27-
/** Possible politeness levels. */
28-
export type AriaLivePoliteness = 'off' | 'polite' | 'assertive';
2931

3032
@Injectable({providedIn: 'root'})
3133
export class LiveAnnouncer implements OnDestroy {
@@ -36,7 +38,9 @@ export class LiveAnnouncer implements OnDestroy {
3638
constructor(
3739
@Optional() @Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN) elementToken: any,
3840
private _ngZone: NgZone,
39-
@Inject(DOCUMENT) _document: any) {
41+
@Inject(DOCUMENT) _document: any,
42+
@Optional() @Inject(LIVE_ANNOUNCER_DEFAULT_OPTIONS)
43+
private _defaultOptions?: LiveAnnouncerDefaultOptions) {
4044

4145
// We inject the live element and document as `any` because the constructor signature cannot
4246
// reference browser globals (HTMLElement, Document) on non-browser environments, since having
@@ -82,8 +86,9 @@ export class LiveAnnouncer implements OnDestroy {
8286
announce(message: string, politeness?: AriaLivePoliteness, duration?: number): Promise<void>;
8387

8488
announce(message: string, ...args: any[]): Promise<void> {
85-
let politeness: AriaLivePoliteness;
86-
let duration: number;
89+
const defaultOptions = this._defaultOptions;
90+
let politeness: AriaLivePoliteness | undefined;
91+
let duration: number | undefined;
8792

8893
if (args.length === 1 && typeof args[0] === 'number') {
8994
duration = args[0];
@@ -94,8 +99,17 @@ export class LiveAnnouncer implements OnDestroy {
9499
this.clear();
95100
clearTimeout(this._previousTimeout);
96101

102+
if (!politeness) {
103+
politeness =
104+
(defaultOptions && defaultOptions.politeness) ? defaultOptions.politeness : 'polite';
105+
}
106+
107+
if (duration == null && defaultOptions) {
108+
duration = defaultOptions.duration;
109+
}
110+
97111
// TODO: ensure changing the politeness works on all environments we support.
98-
this._liveElement.setAttribute('aria-live', politeness! || 'polite');
112+
this._liveElement.setAttribute('aria-live', politeness);
99113

100114
// This 100ms timeout is necessary for some browser + screen-reader combinations:
101115
// - Both JAWS and NVDA over IE11 will not announce anything without a non-zero timeout.

src/cdk/a11y/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export * from './key-manager/list-key-manager';
1212
export * from './focus-trap/focus-trap';
1313
export * from './interactivity-checker/interactivity-checker';
1414
export * from './live-announcer/live-announcer';
15-
export * from './live-announcer/live-announcer-token';
15+
export * from './live-announcer/live-announcer-tokens';
1616
export * from './focus-monitor/focus-monitor';
1717
export * from './fake-mousedown';
1818
export * from './a11y-module';

tools/public_api_guard/cdk/a11y.d.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ export interface ListKeyManagerOption {
150150
getLabel?(): string;
151151
}
152152

153+
export declare const LIVE_ANNOUNCER_DEFAULT_OPTIONS: InjectionToken<LiveAnnouncerDefaultOptions>;
154+
153155
export declare const LIVE_ANNOUNCER_ELEMENT_TOKEN: InjectionToken<HTMLElement | null>;
154156

155157
export declare function LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY(): null;
@@ -159,15 +161,20 @@ export declare const LIVE_ANNOUNCER_PROVIDER: Provider;
159161
export declare function LIVE_ANNOUNCER_PROVIDER_FACTORY(parentAnnouncer: LiveAnnouncer, liveElement: any, _document: any, ngZone: NgZone): LiveAnnouncer;
160162

161163
export declare class LiveAnnouncer implements OnDestroy {
162-
constructor(elementToken: any, _ngZone: NgZone, _document: any);
163-
announce(message: string): Promise<void>;
164+
constructor(elementToken: any, _ngZone: NgZone, _document: any, _defaultOptions?: LiveAnnouncerDefaultOptions | undefined);
165+
announce(message: string, politeness?: AriaLivePoliteness): Promise<void>;
164166
announce(message: string, duration?: number): Promise<void>;
165167
announce(message: string, politeness?: AriaLivePoliteness, duration?: number): Promise<void>;
166-
announce(message: string, politeness?: AriaLivePoliteness): Promise<void>;
168+
announce(message: string): Promise<void>;
167169
clear(): void;
168170
ngOnDestroy(): void;
169171
}
170172

173+
export interface LiveAnnouncerDefaultOptions {
174+
duration?: number;
175+
politeness?: AriaLivePoliteness;
176+
}
177+
171178
export declare const MESSAGES_CONTAINER_ID = "cdk-describedby-message-container";
172179

173180
export interface RegisteredMessage {

0 commit comments

Comments
 (0)