Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions src/cdk/a11y/live-announcer/live-announcer-token.ts

This file was deleted.

39 changes: 39 additions & 0 deletions src/cdk/a11y/live-announcer/live-announcer-tokens.ts
Original file line number Diff line number Diff line change
@@ -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');
47 changes: 46 additions & 1 deletion src/cdk/a11y/live-announcer/live-announcer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
30 changes: 22 additions & 8 deletions src/cdk/a11y/live-announcer/live-announcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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];
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/cdk/a11y/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
13 changes: 10 additions & 3 deletions tools/public_api_guard/cdk/a11y.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down