feat(a11y): add injection token configure default politeness and duration (#15126)

Adds an injection token that allows consumers to configure the default `politeness` and `duration` for the live announcer.

Fixes #15121.
This commit is contained in:
Kristiyan Kostadinov
2019-02-14 18:56:46 +01:00
committed by Jeremy Elbourn
parent 0db904418d
commit e2c9873196
6 changed files with 118 additions and 36 deletions
@@ -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<HTMLElement | null>('liveAnnouncerElement', {
providedIn: 'root',
factory: LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY,
});
/** @docs-private */
export function LIVE_ANNOUNCER_ELEMENT_TOKEN_FACTORY(): null {
return null;
}
@@ -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<HTMLElement | null>('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<LiveAnnouncerDefaultOptions>('LIVE_ANNOUNCER_DEFAULT_OPTIONS');
@@ -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', () => {
+22 -8
View File
@@ -21,12 +21,14 @@ 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 {
private _liveElement: HTMLElement;
@@ -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<void>;
announce(message: string, ...args: any[]): Promise<void> {
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.
+1 -1
View File
@@ -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';
+10 -3
View File
@@ -150,6 +150,8 @@ export interface ListKeyManagerOption {
getLabel?(): string;
}
export declare const LIVE_ANNOUNCER_DEFAULT_OPTIONS: InjectionToken<LiveAnnouncerDefaultOptions>;
export declare const LIVE_ANNOUNCER_ELEMENT_TOKEN: InjectionToken<HTMLElement | null>;
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<void>;
constructor(elementToken: any, _ngZone: NgZone, _document: any, _defaultOptions?: LiveAnnouncerDefaultOptions | undefined);
announce(message: string, politeness?: AriaLivePoliteness): Promise<void>;
announce(message: string, duration?: number): Promise<void>;
announce(message: string, politeness?: AriaLivePoliteness, duration?: number): Promise<void>;
announce(message: string, politeness?: AriaLivePoliteness): Promise<void>;
announce(message: string): Promise<void>;
clear(): void;
ngOnDestroy(): void;
}
export interface LiveAnnouncerDefaultOptions {
duration?: number;
politeness?: AriaLivePoliteness;
}
export declare const MESSAGES_CONTAINER_ID = "cdk-describedby-message-container";
export interface RegisteredMessage {