Skip to content

Commit f6cb2c6

Browse files
crisbetojelbourn
authored andcommitted
refactor(aria-describer): better server-side rendering support (#8523)
Refactors the ARIA describer to work when running on the server. Previously it was inert because the renderer was missing some APIs.
1 parent fbf2987 commit f6cb2c6

File tree

1 file changed

+95
-88
lines changed

1 file changed

+95
-88
lines changed

src/cdk/a11y/aria-describer.ts

Lines changed: 95 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injectable, Optional, SkipSelf} from '@angular/core';
10-
import {Platform} from '@angular/cdk/platform';
9+
import {Injectable, Inject, InjectionToken, Optional, SkipSelf} from '@angular/core';
10+
import {DOCUMENT} from '@angular/common';
1111
import {addAriaReferencedId, getAriaReferenceIds, removeAriaReferencedId} from './aria-reference';
1212

1313
/**
@@ -45,151 +45,158 @@ let messagesContainer: HTMLElement | null = null;
4545
*/
4646
@Injectable()
4747
export class AriaDescriber {
48-
constructor(private _platform: Platform) { }
48+
private _document: Document;
49+
50+
constructor(@Inject(DOCUMENT) _document: any) {
51+
this._document = _document;
52+
}
4953

5054
/**
5155
* Adds to the host element an aria-describedby reference to a hidden element that contains
5256
* the message. If the same message has already been registered, then it will reuse the created
5357
* message element.
5458
*/
5559
describe(hostElement: Element, message: string) {
56-
if (!this._platform.isBrowser || !message.trim()) { return; }
60+
if (!message.trim()) {
61+
return;
62+
}
5763

5864
if (!messageRegistry.has(message)) {
59-
createMessageElement(message);
65+
this._createMessageElement(message);
6066
}
6167

62-
if (!isElementDescribedByMessage(hostElement, message)) {
63-
addMessageReference(hostElement, message);
68+
if (!this._isElementDescribedByMessage(hostElement, message)) {
69+
this._addMessageReference(hostElement, message);
6470
}
6571
}
6672

6773
/** Removes the host element's aria-describedby reference to the message element. */
6874
removeDescription(hostElement: Element, message: string) {
69-
if (!this._platform.isBrowser || !message.trim()) {
75+
if (!message.trim()) {
7076
return;
7177
}
7278

73-
if (isElementDescribedByMessage(hostElement, message)) {
74-
removeMessageReference(hostElement, message);
79+
if (this._isElementDescribedByMessage(hostElement, message)) {
80+
this._removeMessageReference(hostElement, message);
7581
}
7682

7783
const registeredMessage = messageRegistry.get(message);
7884
if (registeredMessage && registeredMessage.referenceCount === 0) {
79-
deleteMessageElement(message);
85+
this._deleteMessageElement(message);
8086
}
8187

8288
if (messagesContainer && messagesContainer.childNodes.length === 0) {
83-
deleteMessagesContainer();
89+
this._deleteMessagesContainer();
8490
}
8591
}
8692

8793
/** Unregisters all created message elements and removes the message container. */
8894
ngOnDestroy() {
89-
if (!this._platform.isBrowser) { return; }
95+
const describedElements =
96+
this._document.querySelectorAll(`[${CDK_DESCRIBEDBY_HOST_ATTRIBUTE}]`);
9097

91-
const describedElements = document.querySelectorAll(`[${CDK_DESCRIBEDBY_HOST_ATTRIBUTE}]`);
9298
for (let i = 0; i < describedElements.length; i++) {
93-
removeCdkDescribedByReferenceIds(describedElements[i]);
99+
this._removeCdkDescribedByReferenceIds(describedElements[i]);
94100
describedElements[i].removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
95101
}
96102

97103
if (messagesContainer) {
98-
deleteMessagesContainer();
104+
this._deleteMessagesContainer();
99105
}
100106

101107
messageRegistry.clear();
102108
}
103-
}
104109

105-
/**
106-
* Creates a new element in the visually hidden message container element with the message
107-
* as its content and adds it to the message registry.
108-
*/
109-
function createMessageElement(message: string) {
110-
const messageElement = document.createElement('div');
111-
messageElement.setAttribute('id', `${CDK_DESCRIBEDBY_ID_PREFIX}-${nextId++}`);
112-
messageElement.appendChild(document.createTextNode(message)!);
110+
/**
111+
* Creates a new element in the visually hidden message container element with the message
112+
* as its content and adds it to the message registry.
113+
*/
114+
private _createMessageElement(message: string) {
115+
const messageElement = this._document.createElement('div');
116+
messageElement.setAttribute('id', `${CDK_DESCRIBEDBY_ID_PREFIX}-${nextId++}`);
117+
messageElement.appendChild(this._document.createTextNode(message)!);
113118

114-
if (!messagesContainer) { createMessagesContainer(); }
115-
messagesContainer!.appendChild(messageElement);
119+
if (!messagesContainer) { this._createMessagesContainer(); }
120+
messagesContainer!.appendChild(messageElement);
116121

117-
messageRegistry.set(message, {messageElement, referenceCount: 0});
118-
}
122+
messageRegistry.set(message, {messageElement, referenceCount: 0});
123+
}
119124

120-
/** Deletes the message element from the global messages container. */
121-
function deleteMessageElement(message: string) {
122-
const registeredMessage = messageRegistry.get(message);
123-
const messageElement = registeredMessage && registeredMessage.messageElement;
124-
if (messagesContainer && messageElement) {
125-
messagesContainer.removeChild(messageElement);
125+
/** Deletes the message element from the global messages container. */
126+
private _deleteMessageElement(message: string) {
127+
const registeredMessage = messageRegistry.get(message);
128+
const messageElement = registeredMessage && registeredMessage.messageElement;
129+
if (messagesContainer && messageElement) {
130+
messagesContainer.removeChild(messageElement);
131+
}
132+
messageRegistry.delete(message);
126133
}
127-
messageRegistry.delete(message);
128-
}
129134

130-
/** Creates the global container for all aria-describedby messages. */
131-
function createMessagesContainer() {
132-
messagesContainer = document.createElement('div');
135+
/** Creates the global container for all aria-describedby messages. */
136+
private _createMessagesContainer() {
137+
messagesContainer = this._document.createElement('div');
133138

134-
messagesContainer.setAttribute('id', MESSAGES_CONTAINER_ID);
135-
messagesContainer.setAttribute('aria-hidden', 'true');
136-
messagesContainer.style.display = 'none';
137-
document.body.appendChild(messagesContainer);
138-
}
139+
messagesContainer.setAttribute('id', MESSAGES_CONTAINER_ID);
140+
messagesContainer.setAttribute('aria-hidden', 'true');
141+
messagesContainer.style.display = 'none';
142+
this._document.body.appendChild(messagesContainer);
143+
}
139144

140-
/** Deletes the global messages container. */
141-
function deleteMessagesContainer() {
142-
document.body.removeChild(messagesContainer!);
143-
messagesContainer = null;
144-
}
145+
/** Deletes the global messages container. */
146+
private _deleteMessagesContainer() {
147+
this._document.body.removeChild(messagesContainer!);
148+
messagesContainer = null;
149+
}
145150

146-
/** Removes all cdk-describedby messages that are hosted through the element. */
147-
function removeCdkDescribedByReferenceIds(element: Element) {
148-
// Remove all aria-describedby reference IDs that are prefixed by CDK_DESCRIBEDBY_ID_PREFIX
149-
const originalReferenceIds = getAriaReferenceIds(element, 'aria-describedby')
150-
.filter(id => id.indexOf(CDK_DESCRIBEDBY_ID_PREFIX) != 0);
151-
element.setAttribute('aria-describedby', originalReferenceIds.join(' '));
152-
}
151+
/** Removes all cdk-describedby messages that are hosted through the element. */
152+
private _removeCdkDescribedByReferenceIds(element: Element) {
153+
// Remove all aria-describedby reference IDs that are prefixed by CDK_DESCRIBEDBY_ID_PREFIX
154+
const originalReferenceIds = getAriaReferenceIds(element, 'aria-describedby')
155+
.filter(id => id.indexOf(CDK_DESCRIBEDBY_ID_PREFIX) != 0);
156+
element.setAttribute('aria-describedby', originalReferenceIds.join(' '));
157+
}
153158

154-
/**
155-
* Adds a message reference to the element using aria-describedby and increments the registered
156-
* message's reference count.
157-
*/
158-
function addMessageReference(element: Element, message: string) {
159-
const registeredMessage = messageRegistry.get(message)!;
159+
/**
160+
* Adds a message reference to the element using aria-describedby and increments the registered
161+
* message's reference count.
162+
*/
163+
private _addMessageReference(element: Element, message: string) {
164+
const registeredMessage = messageRegistry.get(message)!;
160165

161-
// Add the aria-describedby reference and set the describedby_host attribute to mark the element.
162-
addAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
163-
element.setAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE, '');
166+
// Add the aria-describedby reference and set the
167+
// describedby_host attribute to mark the element.
168+
addAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
169+
element.setAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE, '');
164170

165-
registeredMessage.referenceCount++;
166-
}
171+
registeredMessage.referenceCount++;
172+
}
167173

168-
/**
169-
* Removes a message reference from the element using aria-describedby and decrements the registered
170-
* message's reference count.
171-
*/
172-
function removeMessageReference(element: Element, message: string) {
173-
const registeredMessage = messageRegistry.get(message)!;
174-
registeredMessage.referenceCount--;
174+
/**
175+
* Removes a message reference from the element using aria-describedby
176+
* and decrements the registered message's reference count.
177+
*/
178+
private _removeMessageReference(element: Element, message: string) {
179+
const registeredMessage = messageRegistry.get(message)!;
180+
registeredMessage.referenceCount--;
175181

176-
removeAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
177-
element.removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
178-
}
182+
removeAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
183+
element.removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
184+
}
185+
186+
/** Returns true if the element has been described by the provided message ID. */
187+
private _isElementDescribedByMessage(element: Element, message: string): boolean {
188+
const referenceIds = getAriaReferenceIds(element, 'aria-describedby');
189+
const registeredMessage = messageRegistry.get(message);
190+
const messageId = registeredMessage && registeredMessage.messageElement.id;
179191

180-
/** Returns true if the element has been described by the provided message ID. */
181-
function isElementDescribedByMessage(element: Element, message: string): boolean {
182-
const referenceIds = getAriaReferenceIds(element, 'aria-describedby');
183-
const registeredMessage = messageRegistry.get(message);
184-
const messageId = registeredMessage && registeredMessage.messageElement.id;
192+
return !!messageId && referenceIds.indexOf(messageId) != -1;
193+
}
185194

186-
return !!messageId && referenceIds.indexOf(messageId) != -1;
187195
}
188196

189197
/** @docs-private */
190-
export function ARIA_DESCRIBER_PROVIDER_FACTORY(
191-
parentDispatcher: AriaDescriber, platform: Platform) {
192-
return parentDispatcher || new AriaDescriber(platform);
198+
export function ARIA_DESCRIBER_PROVIDER_FACTORY(parentDispatcher: AriaDescriber, _document: any) {
199+
return parentDispatcher || new AriaDescriber(_document);
193200
}
194201

195202
/** @docs-private */
@@ -198,7 +205,7 @@ export const ARIA_DESCRIBER_PROVIDER = {
198205
provide: AriaDescriber,
199206
deps: [
200207
[new Optional(), new SkipSelf(), AriaDescriber],
201-
Platform
208+
DOCUMENT as InjectionToken<any>
202209
],
203210
useFactory: ARIA_DESCRIBER_PROVIDER_FACTORY
204211
};

0 commit comments

Comments
 (0)