6
6
* found in the LICENSE file at https://angular.io/license
7
7
*/
8
8
9
- import { Platform , normalizePassiveListenerOptions } from '@angular/cdk/platform' ;
9
+ import { Platform , normalizePassiveListenerOptions , _getShadowRoot } from '@angular/cdk/platform' ;
10
10
import {
11
11
Directive ,
12
12
ElementRef ,
@@ -67,7 +67,8 @@ export const FOCUS_MONITOR_DEFAULT_OPTIONS =
67
67
68
68
type MonitoredElementInfo = {
69
69
checkChildren : boolean ,
70
- subject : Subject < FocusOrigin >
70
+ subject : Subject < FocusOrigin > ,
71
+ rootNode : HTMLElement | Document
71
72
} ;
72
73
73
74
/**
@@ -110,6 +111,14 @@ export class FocusMonitor implements OnDestroy {
110
111
/** The number of elements currently being monitored. */
111
112
private _monitoredElementCount = 0 ;
112
113
114
+ /**
115
+ * Keeps track of the root nodes to which we've currently bound a focus/blur handler,
116
+ * as well as the number of monitored elements that they contain. We have to treat focus/blur
117
+ * handlers differently from the rest of the events, because the browser won't emit events
118
+ * to the document when focus moves inside of a shadow root.
119
+ */
120
+ private _rootNodeFocusListenerCount = new Map < HTMLElement | Document , number > ( ) ;
121
+
113
122
/**
114
123
* The specified detection mode, used for attributing the origin of a focus
115
124
* event.
@@ -153,10 +162,7 @@ export class FocusMonitor implements OnDestroy {
153
162
clearTimeout ( this . _touchTimeoutId ) ;
154
163
}
155
164
156
- // Since this listener is bound on the `document` level, any events coming from the shadow DOM
157
- // will have their `target` set to the shadow root. If available, use `composedPath` to
158
- // figure out the event target.
159
- this . _lastTouchTarget = event . composedPath ? event . composedPath ( ) [ 0 ] : event . target ;
165
+ this . _lastTouchTarget = getTarget ( event ) ;
160
166
this . _touchTimeoutId = setTimeout ( ( ) => this . _lastTouchTarget = null , TOUCH_BUFFER_MS ) ;
161
167
}
162
168
@@ -188,13 +194,13 @@ export class FocusMonitor implements OnDestroy {
188
194
* Event listener for `focus` and 'blur' events on the document.
189
195
* Needs to be an arrow function in order to preserve the context when it gets bound.
190
196
*/
191
- private _documentFocusAndBlurListener = ( event : FocusEvent ) => {
192
- const target = event . target as HTMLElement | null ;
197
+ private _rootNodeFocusAndBlurListener = ( event : Event ) => {
198
+ const target = getTarget ( event ) ;
193
199
const handler = event . type === 'focus' ? this . _onFocus : this . _onBlur ;
194
200
195
201
// We need to walk up the ancestor chain in order to support `checkChildren`.
196
- for ( let el = target ; el ; el = el . parentElement ) {
197
- handler . call ( this , event , el ) ;
202
+ for ( let element = target ; element ; element = element . parentElement ) {
203
+ handler . call ( this , event as FocusEvent , element ) ;
198
204
}
199
205
}
200
206
@@ -225,20 +231,26 @@ export class FocusMonitor implements OnDestroy {
225
231
226
232
const nativeElement = coerceElement ( element ) ;
227
233
234
+ // If the element is inside the shadow DOM, we need to bind our focus/blur listeners to
235
+ // the shadow root, rather than the `document`, because the browser won't emit focus events
236
+ // to the `document`, if focus is moving within the same shadow root.
237
+ const rootNode = ( _getShadowRoot ( nativeElement ) as HTMLElement | null ) || this . _getDocument ( ) ;
238
+
228
239
// Check if we're already monitoring this element.
229
240
if ( this . _elementInfo . has ( nativeElement ) ) {
230
- const cachedInfo = this . _elementInfo . get ( nativeElement ) ;
231
- cachedInfo ! . checkChildren = checkChildren ;
232
- return cachedInfo ! . subject . asObservable ( ) ;
241
+ const cachedInfo = this . _elementInfo . get ( nativeElement ) ! ;
242
+ cachedInfo . checkChildren = checkChildren ;
243
+ return cachedInfo . subject . asObservable ( ) ;
233
244
}
234
245
235
246
// Create monitored element info.
236
247
const info : MonitoredElementInfo = {
237
248
checkChildren : checkChildren ,
238
- subject : new Subject < FocusOrigin > ( )
249
+ subject : new Subject < FocusOrigin > ( ) ,
250
+ rootNode
239
251
} ;
240
252
this . _elementInfo . set ( nativeElement , info ) ;
241
- this . _incrementMonitoredElementCount ( ) ;
253
+ this . _registerGlobalListeners ( info ) ;
242
254
243
255
return info . subject . asObservable ( ) ;
244
256
}
@@ -264,7 +276,7 @@ export class FocusMonitor implements OnDestroy {
264
276
265
277
this . _setClasses ( nativeElement ) ;
266
278
this . _elementInfo . delete ( nativeElement ) ;
267
- this . _decrementMonitoredElementCount ( ) ;
279
+ this . _removeGlobalListeners ( elementInfo ) ;
268
280
}
269
281
}
270
282
@@ -396,7 +408,7 @@ export class FocusMonitor implements OnDestroy {
396
408
// for the first focus event after the touchstart, and then the first blur event after that
397
409
// focus event. When that blur event fires we know that whatever follows is not a result of the
398
410
// touchstart.
399
- let focusTarget = event . target ;
411
+ const focusTarget = getTarget ( event ) ;
400
412
return this . _lastTouchTarget instanceof Node && focusTarget instanceof Node &&
401
413
( focusTarget === this . _lastTouchTarget || focusTarget . contains ( this . _lastTouchTarget ) ) ;
402
414
}
@@ -415,7 +427,7 @@ export class FocusMonitor implements OnDestroy {
415
427
// If we are not counting child-element-focus as focused, make sure that the event target is the
416
428
// monitored element itself.
417
429
const elementInfo = this . _elementInfo . get ( element ) ;
418
- if ( ! elementInfo || ( ! elementInfo . checkChildren && element !== event . target ) ) {
430
+ if ( ! elementInfo || ( ! elementInfo . checkChildren && element !== getTarget ( event ) ) ) {
419
431
return ;
420
432
}
421
433
@@ -448,19 +460,33 @@ export class FocusMonitor implements OnDestroy {
448
460
this . _ngZone . run ( ( ) => subject . next ( origin ) ) ;
449
461
}
450
462
451
- private _incrementMonitoredElementCount ( ) {
463
+ private _registerGlobalListeners ( elementInfo : MonitoredElementInfo ) {
464
+ if ( ! this . _platform . isBrowser ) {
465
+ return ;
466
+ }
467
+
468
+ const rootNode = elementInfo . rootNode ;
469
+ const rootNodeFocusListeners = this . _rootNodeFocusListenerCount . get ( rootNode ) || 0 ;
470
+
471
+ if ( ! rootNodeFocusListeners ) {
472
+ this . _ngZone . runOutsideAngular ( ( ) => {
473
+ rootNode . addEventListener ( 'focus' , this . _rootNodeFocusAndBlurListener ,
474
+ captureEventListenerOptions ) ;
475
+ rootNode . addEventListener ( 'blur' , this . _rootNodeFocusAndBlurListener ,
476
+ captureEventListenerOptions ) ;
477
+ } ) ;
478
+ }
479
+
480
+ this . _rootNodeFocusListenerCount . set ( rootNode , rootNodeFocusListeners + 1 ) ;
481
+
452
482
// Register global listeners when first element is monitored.
453
- if ( ++ this . _monitoredElementCount == 1 && this . _platform . isBrowser ) {
483
+ if ( ++ this . _monitoredElementCount === 1 ) {
454
484
// Note: we listen to events in the capture phase so we
455
485
// can detect them even if the user stops propagation.
456
486
this . _ngZone . runOutsideAngular ( ( ) => {
457
487
const document = this . _getDocument ( ) ;
458
488
const window = this . _getWindow ( ) ;
459
489
460
- document . addEventListener ( 'focus' , this . _documentFocusAndBlurListener ,
461
- captureEventListenerOptions ) ;
462
- document . addEventListener ( 'blur' , this . _documentFocusAndBlurListener ,
463
- captureEventListenerOptions ) ;
464
490
document . addEventListener ( 'keydown' , this . _documentKeydownListener ,
465
491
captureEventListenerOptions ) ;
466
492
document . addEventListener ( 'mousedown' , this . _documentMousedownListener ,
@@ -472,16 +498,28 @@ export class FocusMonitor implements OnDestroy {
472
498
}
473
499
}
474
500
475
- private _decrementMonitoredElementCount ( ) {
501
+ private _removeGlobalListeners ( elementInfo : MonitoredElementInfo ) {
502
+ const rootNode = elementInfo . rootNode ;
503
+
504
+ if ( this . _rootNodeFocusListenerCount . has ( rootNode ) ) {
505
+ const rootNodeFocusListeners = this . _rootNodeFocusListenerCount . get ( rootNode ) ! ;
506
+
507
+ if ( rootNodeFocusListeners > 1 ) {
508
+ this . _rootNodeFocusListenerCount . set ( rootNode , rootNodeFocusListeners - 1 ) ;
509
+ } else {
510
+ rootNode . removeEventListener ( 'focus' , this . _rootNodeFocusAndBlurListener ,
511
+ captureEventListenerOptions ) ;
512
+ rootNode . removeEventListener ( 'blur' , this . _rootNodeFocusAndBlurListener ,
513
+ captureEventListenerOptions ) ;
514
+ this . _rootNodeFocusListenerCount . delete ( rootNode ) ;
515
+ }
516
+ }
517
+
476
518
// Unregister global listeners when last element is unmonitored.
477
519
if ( ! -- this . _monitoredElementCount ) {
478
520
const document = this . _getDocument ( ) ;
479
521
const window = this . _getWindow ( ) ;
480
522
481
- document . removeEventListener ( 'focus' , this . _documentFocusAndBlurListener ,
482
- captureEventListenerOptions ) ;
483
- document . removeEventListener ( 'blur' , this . _documentFocusAndBlurListener ,
484
- captureEventListenerOptions ) ;
485
523
document . removeEventListener ( 'keydown' , this . _documentKeydownListener ,
486
524
captureEventListenerOptions ) ;
487
525
document . removeEventListener ( 'mousedown' , this . _documentMousedownListener ,
@@ -498,6 +536,13 @@ export class FocusMonitor implements OnDestroy {
498
536
}
499
537
}
500
538
539
+ /** Gets the target of an event, accounting for Shadow DOM. */
540
+ function getTarget ( event : Event ) : HTMLElement | null {
541
+ // If an event is bound outside the Shadow DOM, the `event.target` will
542
+ // point to the shadow root so we have to use `composedPath` instead.
543
+ return ( event . composedPath ? event . composedPath ( ) [ 0 ] : event . target ) as HTMLElement | null ;
544
+ }
545
+
501
546
502
547
/**
503
548
* Directive that determines how a particular element was focused (via keyboard, mouse, touch, or
0 commit comments