diff --git a/src/cdk/a11y/live-announcer/live-announcer.spec.ts b/src/cdk/a11y/live-announcer/live-announcer.spec.ts index 1d6285cc3ec5..7a37b21d4340 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.spec.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.spec.ts @@ -81,6 +81,33 @@ describe('LiveAnnouncer', () => { tick(100); expect(spy).toHaveBeenCalled(); })); + + it('should ensure that there is only one live element at a time', fakeAsync(() => { + announcer.ngOnDestroy(); + fixture.destroy(); + + TestBed.resetTestingModule().configureTestingModule({ + imports: [A11yModule], + declarations: [TestApp], + }); + + const extraElement = document.createElement('div'); + extraElement.classList.add('cdk-live-announcer-element'); + document.body.appendChild(extraElement); + + inject([LiveAnnouncer], (la: LiveAnnouncer) => { + announcer = la; + ariaLiveElement = getLiveElement(); + fixture = TestBed.createComponent(TestApp); + })(); + + announcer.announce('Hey Google'); + tick(100); + + expect(document.body.querySelectorAll('.cdk-live-announcer-element').length) + .toBe(1, 'Expected only one live announcer element in the DOM.'); + })); + }); describe('with a custom element', () => { diff --git a/src/cdk/a11y/live-announcer/live-announcer.ts b/src/cdk/a11y/live-announcer/live-announcer.ts index f06eb5da9037..6e1fa148fe78 100644 --- a/src/cdk/a11y/live-announcer/live-announcer.ts +++ b/src/cdk/a11y/live-announcer/live-announcer.ts @@ -30,14 +30,16 @@ export type AriaLivePoliteness = 'off' | 'polite' | 'assertive'; @Injectable({providedIn: 'root'}) export class LiveAnnouncer implements OnDestroy { private readonly _liveElement: HTMLElement; + private _document: Document; constructor( @Optional() @Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN) elementToken: any, - @Inject(DOCUMENT) private _document: any) { + @Inject(DOCUMENT) _document: any) { // 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 // a class decorator causes TypeScript to preserve the constructor signature types. + this._document = _document; this._liveElement = elementToken || this._createLiveElement(); } @@ -73,9 +75,16 @@ export class LiveAnnouncer implements OnDestroy { } private _createLiveElement(): HTMLElement { - let liveEl = this._document.createElement('div'); + const elementClass = 'cdk-live-announcer-element'; + const previousElements = this._document.getElementsByClassName(elementClass); - liveEl.classList.add('cdk-live-announcer-element'); + // Remove any old containers. This can happen when coming in from a server-side-rendered page. + for (let i = 0; i < previousElements.length; i++) { + previousElements[i].parentNode!.removeChild(previousElements[i]); + } + + const liveEl = this._document.createElement('div'); + liveEl.classList.add(elementClass); liveEl.classList.add('cdk-visually-hidden'); liveEl.setAttribute('aria-atomic', 'true');