88
99import { DOCUMENT } from '@angular/common' ;
1010import { Inject , Injectable , OnDestroy } from '@angular/core' ;
11+ import { Platform } from '@angular/cdk/platform' ;
1112import { addAriaReferencedId , getAriaReferenceIds , removeAriaReferencedId } from './aria-reference' ;
1213
1314
@@ -23,24 +24,30 @@ export interface RegisteredMessage {
2324 referenceCount : number ;
2425}
2526
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+ */
2732export const MESSAGES_CONTAINER_ID = 'cdk-describedby-message-container' ;
2833
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+ */
3039export const CDK_DESCRIBEDBY_ID_PREFIX = 'cdk-describedby-message' ;
3140
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+ */
3346export const CDK_DESCRIBEDBY_HOST_ATTRIBUTE = 'cdk-describedby-host' ;
3447
3548/** Global incremental identifier for each registered message element. */
3649let nextId = 0 ;
3750
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-
4451/**
4552 * Utility that creates visually hidden elements with a message content. Useful for elements that
4653 * want to use aria-describedby to further describe themselves without adding additional visual
@@ -50,8 +57,22 @@ let messagesContainer: HTMLElement | null = null;
5057export class AriaDescriber implements OnDestroy {
5158 private _document : Document ;
5259
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+
5369 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 ) {
5576 this . _document = _document ;
5677 }
5778
@@ -77,8 +98,8 @@ export class AriaDescriber implements OnDestroy {
7798 if ( typeof message !== 'string' ) {
7899 // We need to ensure that the element has an ID.
79100 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 ) ) {
82103 this . _createMessageElement ( message , role ) ;
83104 }
84105
@@ -107,32 +128,29 @@ export class AriaDescriber implements OnDestroy {
107128 // If the message is a string, it means that it's one that we created for the
108129 // consumer so we can remove it safely, otherwise we should leave it in place.
109130 if ( typeof message === 'string' ) {
110- const registeredMessage = messageRegistry . get ( key ) ;
131+ const registeredMessage = this . _messageRegistry . get ( key ) ;
111132 if ( registeredMessage && registeredMessage . referenceCount === 0 ) {
112133 this . _deleteMessageElement ( key ) ;
113134 }
114135 }
115136
116- if ( messagesContainer && messagesContainer . childNodes . length === 0 ) {
137+ if ( this . _messagesContainer ? .childNodes . length === 0 ) {
117138 this . _deleteMessagesContainer ( ) ;
118139 }
119140 }
120141
121142 /** Unregisters all created message elements and removes the message container. */
122143 ngOnDestroy ( ) {
123144 const describedElements =
124- this . _document . querySelectorAll ( `[${ CDK_DESCRIBEDBY_HOST_ATTRIBUTE } ]` ) ;
145+ this . _document . querySelectorAll ( `[${ CDK_DESCRIBEDBY_HOST_ATTRIBUTE } =" ${ this . _id } " ]` ) ;
125146
126147 for ( let i = 0 ; i < describedElements . length ; i ++ ) {
127148 this . _removeCdkDescribedByReferenceIds ( describedElements [ i ] ) ;
128149 describedElements [ i ] . removeAttribute ( CDK_DESCRIBEDBY_HOST_ATTRIBUTE ) ;
129150 }
130151
131- if ( messagesContainer ) {
132- this . _deleteMessagesContainer ( ) ;
133- }
134-
135- messageRegistry . clear ( ) ;
152+ this . _deleteMessagesContainer ( ) ;
153+ this . _messageRegistry . clear ( ) ;
136154 }
137155
138156 /**
@@ -149,53 +167,66 @@ export class AriaDescriber implements OnDestroy {
149167 }
150168
151169 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 } ) ;
154172 }
155173
156174 /** Deletes the message element from the global messages container. */
157175 private _deleteMessageElement ( key : string | Element ) {
158- const registeredMessage = messageRegistry . get ( key ) ;
176+ const registeredMessage = this . _messageRegistry . get ( key ) ;
159177 const messageElement = registeredMessage && registeredMessage . messageElement ;
160- if ( messagesContainer && messageElement ) {
161- messagesContainer . removeChild ( messageElement ) ;
178+ if ( this . _messagesContainer && messageElement ) {
179+ this . _messagesContainer . removeChild ( messageElement ) ;
162180 }
163- messageRegistry . delete ( key ) ;
181+ this . _messageRegistry . delete ( key ) ;
164182 }
165183
166184 /** Creates the global container for all aria-describedby messages. */
167185 private _createMessagesContainer ( ) {
168- if ( ! messagesContainer ) {
169- const preExistingContainer = this . _document . getElementById ( MESSAGES_CONTAINER_ID ) ;
186+ if ( this . _messagesContainer ) {
187+ return ;
188+ }
170189
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 ++ ) {
171195 // When going from the server to the client, we may end up in a situation where there's
172196 // already a container on the page, but we don't have a reference to it. Clear the
173197 // old container so we don't get duplicates. Doing this, instead of emptying the previous
174198 // 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 ) ;
177202 }
203+ }
204+
205+ const messagesContainer = this . _document . createElement ( 'div' ) ;
178206
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' ) ;
191219 }
220+
221+ this . _document . body . appendChild ( messagesContainer ) ;
222+ this . _messagesContainer = messagesContainer ;
192223 }
193224
194225 /** Deletes the global messages container. */
195226 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 ;
199230 }
200231 }
201232
@@ -212,12 +243,12 @@ export class AriaDescriber implements OnDestroy {
212243 * message's reference count.
213244 */
214245 private _addMessageReference ( element : Element , key : string | Element ) {
215- const registeredMessage = messageRegistry . get ( key ) ! ;
246+ const registeredMessage = this . _messageRegistry . get ( key ) ! ;
216247
217248 // Add the aria-describedby reference and set the
218249 // describedby_host attribute to mark the element.
219250 addAriaReferencedId ( element , 'aria-describedby' , registeredMessage . messageElement . id ) ;
220- element . setAttribute ( CDK_DESCRIBEDBY_HOST_ATTRIBUTE , '' ) ;
251+ element . setAttribute ( CDK_DESCRIBEDBY_HOST_ATTRIBUTE , this . _id ) ;
221252 registeredMessage . referenceCount ++ ;
222253 }
223254
@@ -226,7 +257,7 @@ export class AriaDescriber implements OnDestroy {
226257 * and decrements the registered message's reference count.
227258 */
228259 private _removeMessageReference ( element : Element , key : string | Element ) {
229- const registeredMessage = messageRegistry . get ( key ) ! ;
260+ const registeredMessage = this . _messageRegistry . get ( key ) ! ;
230261 registeredMessage . referenceCount -- ;
231262
232263 removeAriaReferencedId ( element , 'aria-describedby' , registeredMessage . messageElement . id ) ;
@@ -236,7 +267,7 @@ export class AriaDescriber implements OnDestroy {
236267 /** Returns true if the element has been described by the provided message ID. */
237268 private _isElementDescribedByMessage ( element : Element , key : string | Element ) : boolean {
238269 const referenceIds = getAriaReferenceIds ( element , 'aria-describedby' ) ;
239- const registeredMessage = messageRegistry . get ( key ) ;
270+ const registeredMessage = this . _messageRegistry . get ( key ) ;
240271 const messageId = registeredMessage && registeredMessage . messageElement . id ;
241272
242273 return ! ! messageId && referenceIds . indexOf ( messageId ) != - 1 ;
0 commit comments