Skip to content

Commit bf9bc0d

Browse files
crisbetojelbourn
authored andcommitted
fix(live-announcer): duplicate live element when coming in from the server (#12378)
Fixes the case where the user might get multiple live announcer elements, if they're coming in from a server-side-rendered page. Along the same lines as #11940.
1 parent 58361f1 commit bf9bc0d

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,33 @@ describe('LiveAnnouncer', () => {
8181
tick(100);
8282
expect(spy).toHaveBeenCalled();
8383
}));
84+
85+
it('should ensure that there is only one live element at a time', fakeAsync(() => {
86+
announcer.ngOnDestroy();
87+
fixture.destroy();
88+
89+
TestBed.resetTestingModule().configureTestingModule({
90+
imports: [A11yModule],
91+
declarations: [TestApp],
92+
});
93+
94+
const extraElement = document.createElement('div');
95+
extraElement.classList.add('cdk-live-announcer-element');
96+
document.body.appendChild(extraElement);
97+
98+
inject([LiveAnnouncer], (la: LiveAnnouncer) => {
99+
announcer = la;
100+
ariaLiveElement = getLiveElement();
101+
fixture = TestBed.createComponent(TestApp);
102+
})();
103+
104+
announcer.announce('Hey Google');
105+
tick(100);
106+
107+
expect(document.body.querySelectorAll('.cdk-live-announcer-element').length)
108+
.toBe(1, 'Expected only one live announcer element in the DOM.');
109+
}));
110+
84111
});
85112

86113
describe('with a custom element', () => {

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ export type AriaLivePoliteness = 'off' | 'polite' | 'assertive';
3030
@Injectable({providedIn: 'root'})
3131
export class LiveAnnouncer implements OnDestroy {
3232
private readonly _liveElement: HTMLElement;
33+
private _document: Document;
3334

3435
constructor(
3536
@Optional() @Inject(LIVE_ANNOUNCER_ELEMENT_TOKEN) elementToken: any,
36-
@Inject(DOCUMENT) private _document: any) {
37+
@Inject(DOCUMENT) _document: any) {
3738

3839
// We inject the live element and document as `any` because the constructor signature cannot
3940
// reference browser globals (HTMLElement, Document) on non-browser environments, since having
4041
// a class decorator causes TypeScript to preserve the constructor signature types.
42+
this._document = _document;
4143
this._liveElement = elementToken || this._createLiveElement();
4244
}
4345

@@ -73,9 +75,16 @@ export class LiveAnnouncer implements OnDestroy {
7375
}
7476

7577
private _createLiveElement(): HTMLElement {
76-
let liveEl = this._document.createElement('div');
78+
const elementClass = 'cdk-live-announcer-element';
79+
const previousElements = this._document.getElementsByClassName(elementClass);
7780

78-
liveEl.classList.add('cdk-live-announcer-element');
81+
// Remove any old containers. This can happen when coming in from a server-side-rendered page.
82+
for (let i = 0; i < previousElements.length; i++) {
83+
previousElements[i].parentNode!.removeChild(previousElements[i]);
84+
}
85+
86+
const liveEl = this._document.createElement('div');
87+
liveEl.classList.add(elementClass);
7988
liveEl.classList.add('cdk-visually-hidden');
8089

8190
liveEl.setAttribute('aria-atomic', 'true');

0 commit comments

Comments
 (0)