mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jeremy Elbourn
parent
0db904418d
commit
e2c9873196
@@ -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', () => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
Vendored
+10
-3
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user