8
8
9
9
import { DOCUMENT } from '@angular/common' ;
10
10
import { Inject , Injectable , OnDestroy } from '@angular/core' ;
11
+ import { Platform } from '@angular/cdk/platform' ;
11
12
import { addAriaReferencedId , getAriaReferenceIds , removeAriaReferencedId } from './aria-reference' ;
12
13
13
14
@@ -23,24 +24,30 @@ export interface RegisteredMessage {
23
24
referenceCount : number ;
24
25
}
25
26
26
- /** ID used for the body container where all messages are appended. */
27
+ /**
28
+ * ID used for the body container where all messages are appended.
29
+ * @deprecated No longer being used. To be removed.
30
+ * @breaking -change 14.0.0
31
+ */
27
32
export const MESSAGES_CONTAINER_ID = 'cdk-describedby-message-container' ;
28
33
29
- /** ID prefix used for each created message element. */
34
+ /**
35
+ * ID prefix used for each created message element.
36
+ * @deprecated To be turned into a private variable.
37
+ * @breaking -change 14.0.0
38
+ */
30
39
export const CDK_DESCRIBEDBY_ID_PREFIX = 'cdk-describedby-message' ;
31
40
32
- /** Attribute given to each host element that is described by a message element. */
41
+ /**
42
+ * Attribute given to each host element that is described by a message element.
43
+ * @deprecated To be turned into a private variable.
44
+ * @breaking -change 14.0.0
45
+ */
33
46
export const CDK_DESCRIBEDBY_HOST_ATTRIBUTE = 'cdk-describedby-host' ;
34
47
35
48
/** Global incremental identifier for each registered message element. */
36
49
let nextId = 0 ;
37
50
38
- /** Global map of all registered message elements that have been placed into the document. */
39
- const messageRegistry = new Map < string | Element , RegisteredMessage > ( ) ;
40
-
41
- /** Container for all registered messages. */
42
- let messagesContainer : HTMLElement | null = null ;
43
-
44
51
/**
45
52
* Utility that creates visually hidden elements with a message content. Useful for elements that
46
53
* want to use aria-describedby to further describe themselves without adding additional visual
@@ -50,8 +57,22 @@ let messagesContainer: HTMLElement | null = null;
50
57
export class AriaDescriber implements OnDestroy {
51
58
private _document : Document ;
52
59
60
+ /** Map of all registered message elements that have been placed into the document. */
61
+ private _messageRegistry = new Map < string | Element , RegisteredMessage > ( ) ;
62
+
63
+ /** Container for all registered messages. */
64
+ private _messagesContainer : HTMLElement | null = null ;
65
+
66
+ /** Unique ID for the service. */
67
+ private readonly _id = `${ nextId ++ } ` ;
68
+
53
69
constructor (
54
- @Inject ( DOCUMENT ) _document : any ) {
70
+ @Inject ( DOCUMENT ) _document : any ,
71
+ /**
72
+ * @deprecated To be turned into a required parameter.
73
+ * @breaking -change 14.0.0
74
+ */
75
+ private _platform ?: Platform ) {
55
76
this . _document = _document ;
56
77
}
57
78
@@ -77,8 +98,8 @@ export class AriaDescriber implements OnDestroy {
77
98
if ( typeof message !== 'string' ) {
78
99
// We need to ensure that the element has an ID.
79
100
setMessageId ( message ) ;
80
- messageRegistry . set ( key , { messageElement : message , referenceCount : 0 } ) ;
81
- } else if ( ! messageRegistry . has ( key ) ) {
101
+ this . _messageRegistry . set ( key , { messageElement : message , referenceCount : 0 } ) ;
102
+ } else if ( ! this . _messageRegistry . has ( key ) ) {
82
103
this . _createMessageElement ( message , role ) ;
83
104
}
84
105
@@ -107,32 +128,29 @@ export class AriaDescriber implements OnDestroy {
107
128
// If the message is a string, it means that it's one that we created for the
108
129
// consumer so we can remove it safely, otherwise we should leave it in place.
109
130
if ( typeof message === 'string' ) {
110
- const registeredMessage = messageRegistry . get ( key ) ;
131
+ const registeredMessage = this . _messageRegistry . get ( key ) ;
111
132
if ( registeredMessage && registeredMessage . referenceCount === 0 ) {
112
133
this . _deleteMessageElement ( key ) ;
113
134
}
114
135
}
115
136
116
- if ( messagesContainer && messagesContainer . childNodes . length === 0 ) {
137
+ if ( this . _messagesContainer ? .childNodes . length === 0 ) {
117
138
this . _deleteMessagesContainer ( ) ;
118
139
}
119
140
}
120
141
121
142
/** Unregisters all created message elements and removes the message container. */
122
143
ngOnDestroy ( ) {
123
144
const describedElements =
124
- this . _document . querySelectorAll ( `[${ CDK_DESCRIBEDBY_HOST_ATTRIBUTE } ]` ) ;
145
+ this . _document . querySelectorAll ( `[${ CDK_DESCRIBEDBY_HOST_ATTRIBUTE } =" ${ this . _id } " ]` ) ;
125
146
126
147
for ( let i = 0 ; i < describedElements . length ; i ++ ) {
127
148
this . _removeCdkDescribedByReferenceIds ( describedElements [ i ] ) ;
128
149
describedElements [ i ] . removeAttribute ( CDK_DESCRIBEDBY_HOST_ATTRIBUTE ) ;
129
150
}
130
151
131
- if ( messagesContainer ) {
132
- this . _deleteMessagesContainer ( ) ;
133
- }
134
-
135
- messageRegistry . clear ( ) ;
152
+ this . _deleteMessagesContainer ( ) ;
153
+ this . _messageRegistry . clear ( ) ;
136
154
}
137
155
138
156
/**
@@ -149,53 +167,66 @@ export class AriaDescriber implements OnDestroy {
149
167
}
150
168
151
169
this . _createMessagesContainer ( ) ;
152
- messagesContainer ! . appendChild ( messageElement ) ;
153
- messageRegistry . set ( getKey ( message , role ) , { messageElement, referenceCount : 0 } ) ;
170
+ this . _messagesContainer ! . appendChild ( messageElement ) ;
171
+ this . _messageRegistry . set ( getKey ( message , role ) , { messageElement, referenceCount : 0 } ) ;
154
172
}
155
173
156
174
/** Deletes the message element from the global messages container. */
157
175
private _deleteMessageElement ( key : string | Element ) {
158
- const registeredMessage = messageRegistry . get ( key ) ;
176
+ const registeredMessage = this . _messageRegistry . get ( key ) ;
159
177
const messageElement = registeredMessage && registeredMessage . messageElement ;
160
- if ( messagesContainer && messageElement ) {
161
- messagesContainer . removeChild ( messageElement ) ;
178
+ if ( this . _messagesContainer && messageElement ) {
179
+ this . _messagesContainer . removeChild ( messageElement ) ;
162
180
}
163
- messageRegistry . delete ( key ) ;
181
+ this . _messageRegistry . delete ( key ) ;
164
182
}
165
183
166
184
/** Creates the global container for all aria-describedby messages. */
167
185
private _createMessagesContainer ( ) {
168
- if ( ! messagesContainer ) {
169
- const preExistingContainer = this . _document . getElementById ( MESSAGES_CONTAINER_ID ) ;
186
+ if ( this . _messagesContainer ) {
187
+ return ;
188
+ }
170
189
190
+ const containerClassName = 'cdk-describedby-message-container' ;
191
+ const serverContainers =
192
+ this . _document . querySelectorAll ( `.${ containerClassName } [platform="server"]` ) ;
193
+
194
+ for ( let i = 0 ; i < serverContainers . length ; i ++ ) {
171
195
// When going from the server to the client, we may end up in a situation where there's
172
196
// already a container on the page, but we don't have a reference to it. Clear the
173
197
// old container so we don't get duplicates. Doing this, instead of emptying the previous
174
198
// container, should be slightly faster.
175
- if ( preExistingContainer && preExistingContainer . parentNode ) {
176
- preExistingContainer . parentNode . removeChild ( preExistingContainer ) ;
199
+ const serverContainer = serverContainers [ i ] ;
200
+ if ( serverContainer . parentNode ) {
201
+ serverContainer . parentNode . removeChild ( serverContainer ) ;
177
202
}
203
+ }
204
+
205
+ const messagesContainer = this . _document . createElement ( 'div' ) ;
178
206
179
- messagesContainer = this . _document . createElement ( 'div' ) ;
180
- messagesContainer . id = MESSAGES_CONTAINER_ID ;
181
- // We add `visibility: hidden` in order to prevent text in this container from
182
- // being searchable by the browser's Ctrl + F functionality .
183
- // Screen-readers will still read the description for elements with aria-describedby even
184
- // when the description element is not visible.
185
- messagesContainer . style . visibility = 'hidden' ;
186
- // Even though we use `visibility: hidden`, we still apply ` cdk-visually-hidden` so that
187
- // the description element doesn't impact page layout.
188
- messagesContainer . classList . add ( 'cdk-visually-hidden' ) ;
189
-
190
- this . _document . body . appendChild ( messagesContainer ) ;
207
+ // We add `visibility: hidden` in order to prevent text in this container from
208
+ // being searchable by the browser's Ctrl + F functionality.
209
+ // Screen-readers will still read the description for elements with aria-describedby even
210
+ // when the description element is not visible .
211
+ messagesContainer . style . visibility = 'hidden' ;
212
+ // Even though we use `visibility: hidden`, we still apply `cdk-visually-hidden` so that
213
+ // the description element doesn't impact page layout.
214
+ messagesContainer . classList . add ( containerClassName , ' cdk-visually-hidden' ) ;
215
+
216
+ // @breaking -change 14.0.0 Remove null check for `_platform`.
217
+ if ( this . _platform && ! this . _platform . isBrowser ) {
218
+ messagesContainer . setAttribute ( 'platform' , 'server' ) ;
191
219
}
220
+
221
+ this . _document . body . appendChild ( messagesContainer ) ;
222
+ this . _messagesContainer = messagesContainer ;
192
223
}
193
224
194
225
/** Deletes the global messages container. */
195
226
private _deleteMessagesContainer ( ) {
196
- if ( messagesContainer && messagesContainer . parentNode ) {
197
- messagesContainer . parentNode . removeChild ( messagesContainer ) ;
198
- messagesContainer = null ;
227
+ if ( this . _messagesContainer && this . _messagesContainer . parentNode ) {
228
+ this . _messagesContainer . parentNode . removeChild ( this . _messagesContainer ) ;
229
+ this . _messagesContainer = null ;
199
230
}
200
231
}
201
232
@@ -212,12 +243,12 @@ export class AriaDescriber implements OnDestroy {
212
243
* message's reference count.
213
244
*/
214
245
private _addMessageReference ( element : Element , key : string | Element ) {
215
- const registeredMessage = messageRegistry . get ( key ) ! ;
246
+ const registeredMessage = this . _messageRegistry . get ( key ) ! ;
216
247
217
248
// Add the aria-describedby reference and set the
218
249
// describedby_host attribute to mark the element.
219
250
addAriaReferencedId ( element , 'aria-describedby' , registeredMessage . messageElement . id ) ;
220
- element . setAttribute ( CDK_DESCRIBEDBY_HOST_ATTRIBUTE , '' ) ;
251
+ element . setAttribute ( CDK_DESCRIBEDBY_HOST_ATTRIBUTE , this . _id ) ;
221
252
registeredMessage . referenceCount ++ ;
222
253
}
223
254
@@ -226,7 +257,7 @@ export class AriaDescriber implements OnDestroy {
226
257
* and decrements the registered message's reference count.
227
258
*/
228
259
private _removeMessageReference ( element : Element , key : string | Element ) {
229
- const registeredMessage = messageRegistry . get ( key ) ! ;
260
+ const registeredMessage = this . _messageRegistry . get ( key ) ! ;
230
261
registeredMessage . referenceCount -- ;
231
262
232
263
removeAriaReferencedId ( element , 'aria-describedby' , registeredMessage . messageElement . id ) ;
@@ -236,7 +267,7 @@ export class AriaDescriber implements OnDestroy {
236
267
/** Returns true if the element has been described by the provided message ID. */
237
268
private _isElementDescribedByMessage ( element : Element , key : string | Element ) : boolean {
238
269
const referenceIds = getAriaReferenceIds ( element , 'aria-describedby' ) ;
239
- const registeredMessage = messageRegistry . get ( key ) ;
270
+ const registeredMessage = this . _messageRegistry . get ( key ) ;
240
271
const messageId = registeredMessage && registeredMessage . messageElement . id ;
241
272
242
273
return ! ! messageId && referenceIds . indexOf ( messageId ) != - 1 ;
0 commit comments