@@ -43,7 +43,7 @@ export const CDK_DESCRIBEDBY_HOST_ATTRIBUTE = 'cdk-describedby-host';
43
43
let nextId = 0 ;
44
44
45
45
/** 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 > ( ) ;
47
47
48
48
/** Container for all registered messages. */
49
49
let messagesContainer : HTMLElement | null = null ;
@@ -52,7 +52,6 @@ let messagesContainer: HTMLElement | null = null;
52
52
* Utility that creates visually hidden elements with a message content. Useful for elements that
53
53
* want to use aria-describedby to further describe themselves without adding additional visual
54
54
* content.
55
- * @docs -private
56
55
*/
57
56
@Injectable ( { providedIn : 'root' } )
58
57
export class AriaDescriber implements OnDestroy {
@@ -67,12 +66,16 @@ export class AriaDescriber implements OnDestroy {
67
66
* the message. If the same message has already been registered, then it will reuse the created
68
67
* message element.
69
68
*/
70
- describe ( hostElement : Element , message : string ) {
69
+ describe ( hostElement : Element , message : string | HTMLElement ) {
71
70
if ( ! this . _canBeDescribed ( hostElement , message ) ) {
72
71
return ;
73
72
}
74
73
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 ) ) {
76
79
this . _createMessageElement ( message ) ;
77
80
}
78
81
@@ -82,7 +85,7 @@ export class AriaDescriber implements OnDestroy {
82
85
}
83
86
84
87
/** 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 ) {
86
89
if ( ! this . _isElementNode ( hostElement ) ) {
87
90
return ;
88
91
}
@@ -91,9 +94,13 @@ export class AriaDescriber implements OnDestroy {
91
94
this . _removeMessageReference ( hostElement , message ) ;
92
95
}
93
96
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
+ }
97
104
}
98
105
99
106
if ( messagesContainer && messagesContainer . childNodes . length === 0 ) {
@@ -124,15 +131,22 @@ export class AriaDescriber implements OnDestroy {
124
131
*/
125
132
private _createMessageElement ( message : string ) {
126
133
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 ;
129
136
130
137
this . _createMessagesContainer ( ) ;
131
138
messagesContainer ! . appendChild ( messageElement ) ;
132
139
133
140
messageRegistry . set ( message , { messageElement, referenceCount : 0 } ) ;
134
141
}
135
142
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
+
136
150
/** Deletes the message element from the global messages container. */
137
151
private _deleteMessageElement ( message : string ) {
138
152
const registeredMessage = messageRegistry . get ( message ) ;
@@ -184,7 +198,7 @@ export class AriaDescriber implements OnDestroy {
184
198
* Adds a message reference to the element using aria-describedby and increments the registered
185
199
* message's reference count.
186
200
*/
187
- private _addMessageReference ( element : Element , message : string ) {
201
+ private _addMessageReference ( element : Element , message : string | HTMLElement ) {
188
202
const registeredMessage = messageRegistry . get ( message ) ! ;
189
203
190
204
// Add the aria-describedby reference and set the
@@ -199,7 +213,7 @@ export class AriaDescriber implements OnDestroy {
199
213
* Removes a message reference from the element using aria-describedby
200
214
* and decrements the registered message's reference count.
201
215
*/
202
- private _removeMessageReference ( element : Element , message : string ) {
216
+ private _removeMessageReference ( element : Element , message : string | HTMLElement ) {
203
217
const registeredMessage = messageRegistry . get ( message ) ! ;
204
218
registeredMessage . referenceCount -- ;
205
219
@@ -208,7 +222,7 @@ export class AriaDescriber implements OnDestroy {
208
222
}
209
223
210
224
/** 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 {
212
226
const referenceIds = getAriaReferenceIds ( element , 'aria-describedby' ) ;
213
227
const registeredMessage = messageRegistry . get ( message ) ;
214
228
const messageId = registeredMessage && registeredMessage . messageElement . id ;
@@ -217,16 +231,23 @@ export class AriaDescriber implements OnDestroy {
217
231
}
218
232
219
233
/** 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 {
221
235
if ( ! this . _isElementNode ( element ) ) {
222
236
return false ;
223
237
}
224
238
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
+
225
246
const trimmedMessage = message == null ? '' : `${ message } ` . trim ( ) ;
226
247
const ariaLabel = element . getAttribute ( 'aria-label' ) ;
227
248
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.
230
251
return trimmedMessage ? ( ! ariaLabel || ariaLabel . trim ( ) !== trimmedMessage ) : false ;
231
252
}
232
253
0 commit comments