@@ -43,7 +43,7 @@ export const CDK_DESCRIBEDBY_HOST_ATTRIBUTE = 'cdk-describedby-host';
4343let nextId = 0 ;
4444
4545/** Global map of all registered message elements that have been placed into the document. */
46- const messageRegistry = new Map < string , RegisteredMessage > ( ) ;
46+ const messageRegistry = new Map < string | HTMLElement , RegisteredMessage > ( ) ;
4747
4848/** Container for all registered messages. */
4949let messagesContainer : HTMLElement | null = null ;
@@ -52,7 +52,6 @@ let messagesContainer: HTMLElement | null = null;
5252 * Utility that creates visually hidden elements with a message content. Useful for elements that
5353 * want to use aria-describedby to further describe themselves without adding additional visual
5454 * content.
55- * @docs -private
5655 */
5756@Injectable ( { providedIn : 'root' } )
5857export class AriaDescriber implements OnDestroy {
@@ -67,12 +66,16 @@ export class AriaDescriber implements OnDestroy {
6766 * the message. If the same message has already been registered, then it will reuse the created
6867 * message element.
6968 */
70- describe ( hostElement : Element , message : string ) {
69+ describe ( hostElement : Element , message : string | HTMLElement ) {
7170 if ( ! this . _canBeDescribed ( hostElement , message ) ) {
7271 return ;
7372 }
7473
75- if ( ! messageRegistry . has ( message ) ) {
74+ if ( typeof message !== 'string' ) {
75+ // We need to ensure that the element has an ID.
76+ this . _setMessageId ( message ) ;
77+ messageRegistry . set ( message , { messageElement : message , referenceCount : 0 } ) ;
78+ } else if ( ! messageRegistry . has ( message ) ) {
7679 this . _createMessageElement ( message ) ;
7780 }
7881
@@ -82,7 +85,7 @@ export class AriaDescriber implements OnDestroy {
8285 }
8386
8487 /** Removes the host element's aria-describedby reference to the message element. */
85- removeDescription ( hostElement : Element , message : string ) {
88+ removeDescription ( hostElement : Element , message : string | HTMLElement ) {
8689 if ( ! this . _isElementNode ( hostElement ) ) {
8790 return ;
8891 }
@@ -91,9 +94,13 @@ export class AriaDescriber implements OnDestroy {
9194 this . _removeMessageReference ( hostElement , message ) ;
9295 }
9396
94- const registeredMessage = messageRegistry . get ( message ) ;
95- if ( registeredMessage && registeredMessage . referenceCount === 0 ) {
96- this . _deleteMessageElement ( message ) ;
97+ // If the message is a string, it means that it's one that we created for the
98+ // consumer so we can remove it safely, otherwise we should leave it in place.
99+ if ( typeof message === 'string' ) {
100+ const registeredMessage = messageRegistry . get ( message ) ;
101+ if ( registeredMessage && registeredMessage . referenceCount === 0 ) {
102+ this . _deleteMessageElement ( message ) ;
103+ }
97104 }
98105
99106 if ( messagesContainer && messagesContainer . childNodes . length === 0 ) {
@@ -124,15 +131,22 @@ export class AriaDescriber implements OnDestroy {
124131 */
125132 private _createMessageElement ( message : string ) {
126133 const messageElement = this . _document . createElement ( 'div' ) ;
127- messageElement . setAttribute ( 'id' , ` ${ CDK_DESCRIBEDBY_ID_PREFIX } - ${ nextId ++ } ` ) ;
128- messageElement . appendChild ( this . _document . createTextNode ( message ) ! ) ;
134+ this . _setMessageId ( messageElement ) ;
135+ messageElement . textContent = message ;
129136
130137 this . _createMessagesContainer ( ) ;
131138 messagesContainer ! . appendChild ( messageElement ) ;
132139
133140 messageRegistry . set ( message , { messageElement, referenceCount : 0 } ) ;
134141 }
135142
143+ /** Assigns a unique ID to an element, if it doesn't have one already. */
144+ private _setMessageId ( element : HTMLElement ) {
145+ if ( ! element . id ) {
146+ element . id = `${ CDK_DESCRIBEDBY_ID_PREFIX } -${ nextId ++ } ` ;
147+ }
148+ }
149+
136150 /** Deletes the message element from the global messages container. */
137151 private _deleteMessageElement ( message : string ) {
138152 const registeredMessage = messageRegistry . get ( message ) ;
@@ -184,7 +198,7 @@ export class AriaDescriber implements OnDestroy {
184198 * Adds a message reference to the element using aria-describedby and increments the registered
185199 * message's reference count.
186200 */
187- private _addMessageReference ( element : Element , message : string ) {
201+ private _addMessageReference ( element : Element , message : string | HTMLElement ) {
188202 const registeredMessage = messageRegistry . get ( message ) ! ;
189203
190204 // Add the aria-describedby reference and set the
@@ -199,7 +213,7 @@ export class AriaDescriber implements OnDestroy {
199213 * Removes a message reference from the element using aria-describedby
200214 * and decrements the registered message's reference count.
201215 */
202- private _removeMessageReference ( element : Element , message : string ) {
216+ private _removeMessageReference ( element : Element , message : string | HTMLElement ) {
203217 const registeredMessage = messageRegistry . get ( message ) ! ;
204218 registeredMessage . referenceCount -- ;
205219
@@ -208,7 +222,7 @@ export class AriaDescriber implements OnDestroy {
208222 }
209223
210224 /** Returns true if the element has been described by the provided message ID. */
211- private _isElementDescribedByMessage ( element : Element , message : string ) : boolean {
225+ private _isElementDescribedByMessage ( element : Element , message : string | HTMLElement ) : boolean {
212226 const referenceIds = getAriaReferenceIds ( element , 'aria-describedby' ) ;
213227 const registeredMessage = messageRegistry . get ( message ) ;
214228 const messageId = registeredMessage && registeredMessage . messageElement . id ;
@@ -217,16 +231,23 @@ export class AriaDescriber implements OnDestroy {
217231 }
218232
219233 /** Determines whether a message can be described on a particular element. */
220- private _canBeDescribed ( element : Element , message : string ) : boolean {
234+ private _canBeDescribed ( element : Element , message : string | HTMLElement | void ) : boolean {
221235 if ( ! this . _isElementNode ( element ) ) {
222236 return false ;
223237 }
224238
239+ if ( message && typeof message === 'object' ) {
240+ // We'd have to make some assumptions about the description element's text, if the consumer
241+ // passed in an element. Assume that if an element is passed in, the consumer has verified
242+ // that it can be used as a description.
243+ return true ;
244+ }
245+
225246 const trimmedMessage = message == null ? '' : `${ message } ` . trim ( ) ;
226247 const ariaLabel = element . getAttribute ( 'aria-label' ) ;
227248
228- // We shouldn't set descriptions if they're exactly the same as the `aria-label` of the element,
229- // because screen readers will end up reading out the same text twice in a row.
249+ // We shouldn't set descriptions if they're exactly the same as the `aria-label` of the
250+ // element, because screen readers will end up reading out the same text twice in a row.
230251 return trimmedMessage ? ( ! ariaLabel || ariaLabel . trim ( ) !== trimmedMessage ) : false ;
231252 }
232253
0 commit comments