Skip to content

Commit f010b17

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 a518cb3 commit f010b17

File tree

2 files changed

+44
-7
lines changed

2 files changed

+44
-7
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: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,17 @@ export type AriaLivePoliteness = 'off' | 'polite' | 'assertive';
2929

3030
@Injectable({providedIn: 'root'})
3131
export class LiveAnnouncer implements OnDestroy {
32-
private readonly _liveElement: Element;
32+
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

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

@@ -72,9 +74,17 @@ export class LiveAnnouncer implements OnDestroy {
7274
}
7375
}
7476

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

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);
7888
liveEl.classList.add('cdk-visually-hidden');
7989
liveEl.setAttribute('aria-atomic', 'true');
8090
liveEl.setAttribute('aria-live', 'polite');

0 commit comments

Comments
 (0)