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