66 * found in the LICENSE file at https://angular.io/license
77 */
88
9- import { Platform , normalizePassiveListenerOptions } from '@angular/cdk/platform' ;
9+ import { Platform , normalizePassiveListenerOptions , _getShadowRoot } from '@angular/cdk/platform' ;
1010import {
1111 Directive ,
1212 ElementRef ,
@@ -67,7 +67,8 @@ export const FOCUS_MONITOR_DEFAULT_OPTIONS =
6767
6868type MonitoredElementInfo = {
6969 checkChildren : boolean ,
70- subject : Subject < FocusOrigin >
70+ subject : Subject < FocusOrigin > ,
71+ rootNode : HTMLElement | Document
7172} ;
7273
7374/**
@@ -110,6 +111,14 @@ export class FocusMonitor implements OnDestroy {
110111 /** The number of elements currently being monitored. */
111112 private _monitoredElementCount = 0 ;
112113
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+
113122 /**
114123 * The specified detection mode, used for attributing the origin of a focus
115124 * event.
@@ -153,10 +162,7 @@ export class FocusMonitor implements OnDestroy {
153162 clearTimeout ( this . _touchTimeoutId ) ;
154163 }
155164
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 ) ;
160166 this . _touchTimeoutId = setTimeout ( ( ) => this . _lastTouchTarget = null , TOUCH_BUFFER_MS ) ;
161167 }
162168
@@ -188,13 +194,13 @@ export class FocusMonitor implements OnDestroy {
188194 * Event listener for `focus` and 'blur' events on the document.
189195 * Needs to be an arrow function in order to preserve the context when it gets bound.
190196 */
191- private _documentFocusAndBlurListener = ( event : FocusEvent ) => {
192- const target = event . target as HTMLElement | null ;
197+ private _rootNodeFocusAndBlurListener = ( event : Event ) => {
198+ const target = getTarget ( event ) ;
193199 const handler = event . type === 'focus' ? this . _onFocus : this . _onBlur ;
194200
195201 // 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 ) ;
198204 }
199205 }
200206
@@ -225,20 +231,26 @@ export class FocusMonitor implements OnDestroy {
225231
226232 const nativeElement = coerceElement ( element ) ;
227233
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+
228239 // Check if we're already monitoring this element.
229240 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 ( ) ;
233244 }
234245
235246 // Create monitored element info.
236247 const info : MonitoredElementInfo = {
237248 checkChildren : checkChildren ,
238- subject : new Subject < FocusOrigin > ( )
249+ subject : new Subject < FocusOrigin > ( ) ,
250+ rootNode
239251 } ;
240252 this . _elementInfo . set ( nativeElement , info ) ;
241- this . _incrementMonitoredElementCount ( ) ;
253+ this . _registerGlobalListeners ( info ) ;
242254
243255 return info . subject . asObservable ( ) ;
244256 }
@@ -264,7 +276,7 @@ export class FocusMonitor implements OnDestroy {
264276
265277 this . _setClasses ( nativeElement ) ;
266278 this . _elementInfo . delete ( nativeElement ) ;
267- this . _decrementMonitoredElementCount ( ) ;
279+ this . _removeGlobalListeners ( elementInfo ) ;
268280 }
269281 }
270282
@@ -396,7 +408,7 @@ export class FocusMonitor implements OnDestroy {
396408 // for the first focus event after the touchstart, and then the first blur event after that
397409 // focus event. When that blur event fires we know that whatever follows is not a result of the
398410 // touchstart.
399- let focusTarget = event . target ;
411+ const focusTarget = getTarget ( event ) ;
400412 return this . _lastTouchTarget instanceof Node && focusTarget instanceof Node &&
401413 ( focusTarget === this . _lastTouchTarget || focusTarget . contains ( this . _lastTouchTarget ) ) ;
402414 }
@@ -415,7 +427,7 @@ export class FocusMonitor implements OnDestroy {
415427 // If we are not counting child-element-focus as focused, make sure that the event target is the
416428 // monitored element itself.
417429 const elementInfo = this . _elementInfo . get ( element ) ;
418- if ( ! elementInfo || ( ! elementInfo . checkChildren && element !== event . target ) ) {
430+ if ( ! elementInfo || ( ! elementInfo . checkChildren && element !== getTarget ( event ) ) ) {
419431 return ;
420432 }
421433
@@ -448,19 +460,33 @@ export class FocusMonitor implements OnDestroy {
448460 this . _ngZone . run ( ( ) => subject . next ( origin ) ) ;
449461 }
450462
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+
452482 // Register global listeners when first element is monitored.
453- if ( ++ this . _monitoredElementCount == 1 && this . _platform . isBrowser ) {
483+ if ( ++ this . _monitoredElementCount === 1 ) {
454484 // Note: we listen to events in the capture phase so we
455485 // can detect them even if the user stops propagation.
456486 this . _ngZone . runOutsideAngular ( ( ) => {
457487 const document = this . _getDocument ( ) ;
458488 const window = this . _getWindow ( ) ;
459489
460- document . addEventListener ( 'focus' , this . _documentFocusAndBlurListener ,
461- captureEventListenerOptions ) ;
462- document . addEventListener ( 'blur' , this . _documentFocusAndBlurListener ,
463- captureEventListenerOptions ) ;
464490 document . addEventListener ( 'keydown' , this . _documentKeydownListener ,
465491 captureEventListenerOptions ) ;
466492 document . addEventListener ( 'mousedown' , this . _documentMousedownListener ,
@@ -472,16 +498,28 @@ export class FocusMonitor implements OnDestroy {
472498 }
473499 }
474500
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+
476518 // Unregister global listeners when last element is unmonitored.
477519 if ( ! -- this . _monitoredElementCount ) {
478520 const document = this . _getDocument ( ) ;
479521 const window = this . _getWindow ( ) ;
480522
481- document . removeEventListener ( 'focus' , this . _documentFocusAndBlurListener ,
482- captureEventListenerOptions ) ;
483- document . removeEventListener ( 'blur' , this . _documentFocusAndBlurListener ,
484- captureEventListenerOptions ) ;
485523 document . removeEventListener ( 'keydown' , this . _documentKeydownListener ,
486524 captureEventListenerOptions ) ;
487525 document . removeEventListener ( 'mousedown' , this . _documentMousedownListener ,
@@ -498,6 +536,13 @@ export class FocusMonitor implements OnDestroy {
498536 }
499537}
500538
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+
501546
502547/**
503548 * Directive that determines how a particular element was focused (via keyboard, mouse, touch, or
0 commit comments