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/**
@@ -22,24 +23,30 @@ export interface RegisteredMessage {
2223 referenceCount : number ;
2324}
2425
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+ */
2631export const MESSAGES_CONTAINER_ID = 'cdk-describedby-message-container' ;
2732
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+ */
2938export const CDK_DESCRIBEDBY_ID_PREFIX = 'cdk-describedby-message' ;
3039
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+ */
3245export const CDK_DESCRIBEDBY_HOST_ATTRIBUTE = 'cdk-describedby-host' ;
3346
3447/** Global incremental identifier for each registered message element. */
3548let nextId = 0 ;
3649
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-
4350/**
4451 * Utility that creates visually hidden elements with a message content. Useful for elements that
4552 * want to use aria-describedby to further describe themselves without adding additional visual
@@ -49,7 +56,23 @@ let messagesContainer: HTMLElement | null = null;
4956export class AriaDescriber implements OnDestroy {
5057 private _document : Document ;
5158
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+ ) {
5376 this . _document = _document ;
5477 }
5578
@@ -75,8 +98,8 @@ export class AriaDescriber implements OnDestroy {
7598 if ( typeof message !== 'string' ) {
7699 // We need to ensure that the element has an ID.
77100 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 ) ) {
80103 this . _createMessageElement ( message , role ) ;
81104 }
82105
@@ -105,33 +128,32 @@ export class AriaDescriber implements OnDestroy {
105128 // If the message is a string, it means that it's one that we created for the
106129 // consumer so we can remove it safely, otherwise we should leave it in place.
107130 if ( typeof message === 'string' ) {
108- const registeredMessage = messageRegistry . get ( key ) ;
131+ const registeredMessage = this . _messageRegistry . get ( key ) ;
109132 if ( registeredMessage && registeredMessage . referenceCount === 0 ) {
110133 this . _deleteMessageElement ( key ) ;
111134 }
112135 }
113136
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 ;
116140 }
117141 }
118142
119143 /** Unregisters all created message elements and removes the message container. */
120144 ngOnDestroy ( ) {
121145 const describedElements = this . _document . querySelectorAll (
122- `[${ CDK_DESCRIBEDBY_HOST_ATTRIBUTE } ]` ,
146+ `[${ CDK_DESCRIBEDBY_HOST_ATTRIBUTE } =" ${ this . _id } " ]` ,
123147 ) ;
124148
125149 for ( let i = 0 ; i < describedElements . length ; i ++ ) {
126150 this . _removeCdkDescribedByReferenceIds ( describedElements [ i ] ) ;
127151 describedElements [ i ] . removeAttribute ( CDK_DESCRIBEDBY_HOST_ATTRIBUTE ) ;
128152 }
129153
130- if ( messagesContainer ) {
131- this . _deleteMessagesContainer ( ) ;
132- }
133-
134- messageRegistry . clear ( ) ;
154+ this . _messagesContainer ?. remove ( ) ;
155+ this . _messagesContainer = null ;
156+ this . _messageRegistry . clear ( ) ;
135157 }
136158
137159 /**
@@ -148,49 +170,54 @@ export class AriaDescriber implements OnDestroy {
148170 }
149171
150172 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 } ) ;
153175 }
154176
155177 /** Deletes the message element from the global messages container. */
156178 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 ) ;
160181 }
161182
162183 /** Creates the global container for all aria-describedby messages. */
163184 private _createMessagesContainer ( ) {
164- if ( ! messagesContainer ) {
165- const preExistingContainer = this . _document . getElementById ( MESSAGES_CONTAINER_ID ) ;
185+ if ( this . _messagesContainer ) {
186+ return ;
187+ }
166188
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 ++ ) {
167195 // When going from the server to the client, we may end up in a situation where there's
168196 // already a container on the page, but we don't have a reference to it. Clear the
169197 // old container so we don't get duplicates. Doing this, instead of emptying the previous
170198 // 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 ( ) ;
185200 }
186- }
187201
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' ) ;
193217 }
218+
219+ this . _document . body . appendChild ( messagesContainer ) ;
220+ this . _messagesContainer = messagesContainer ;
194221 }
195222
196223 /** Removes all cdk-describedby messages that are hosted through the element. */
@@ -207,12 +234,12 @@ export class AriaDescriber implements OnDestroy {
207234 * message's reference count.
208235 */
209236 private _addMessageReference ( element : Element , key : string | Element ) {
210- const registeredMessage = messageRegistry . get ( key ) ! ;
237+ const registeredMessage = this . _messageRegistry . get ( key ) ! ;
211238
212239 // Add the aria-describedby reference and set the
213240 // describedby_host attribute to mark the element.
214241 addAriaReferencedId ( element , 'aria-describedby' , registeredMessage . messageElement . id ) ;
215- element . setAttribute ( CDK_DESCRIBEDBY_HOST_ATTRIBUTE , '' ) ;
242+ element . setAttribute ( CDK_DESCRIBEDBY_HOST_ATTRIBUTE , this . _id ) ;
216243 registeredMessage . referenceCount ++ ;
217244 }
218245
@@ -221,7 +248,7 @@ export class AriaDescriber implements OnDestroy {
221248 * and decrements the registered message's reference count.
222249 */
223250 private _removeMessageReference ( element : Element , key : string | Element ) {
224- const registeredMessage = messageRegistry . get ( key ) ! ;
251+ const registeredMessage = this . _messageRegistry . get ( key ) ! ;
225252 registeredMessage . referenceCount -- ;
226253
227254 removeAriaReferencedId ( element , 'aria-describedby' , registeredMessage . messageElement . id ) ;
@@ -231,7 +258,7 @@ export class AriaDescriber implements OnDestroy {
231258 /** Returns true if the element has been described by the provided message ID. */
232259 private _isElementDescribedByMessage ( element : Element , key : string | Element ) : boolean {
233260 const referenceIds = getAriaReferenceIds ( element , 'aria-describedby' ) ;
234- const registeredMessage = messageRegistry . get ( key ) ;
261+ const registeredMessage = this . _messageRegistry . get ( key ) ;
235262 const messageId = registeredMessage && registeredMessage . messageElement . id ;
236263
237264 return ! ! messageId && referenceIds . indexOf ( messageId ) != - 1 ;
0 commit comments