@@ -24,7 +24,11 @@ import {Observable, of as observableOf, Subject, Subscription} from 'rxjs';
24
24
import { takeUntil } from 'rxjs/operators' ;
25
25
import { coerceElement } from '@angular/cdk/coercion' ;
26
26
import { DOCUMENT } from '@angular/common' ;
27
- import { InputModalityDetector , TOUCH_BUFFER_MS } from '../input-modality/input-modality-detector' ;
27
+ import {
28
+ getTarget ,
29
+ InputModalityDetector ,
30
+ TOUCH_BUFFER_MS ,
31
+ } from '../input-modality/input-modality-detector' ;
28
32
29
33
30
34
export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' | null ;
@@ -96,6 +100,12 @@ export class FocusMonitor implements OnDestroy {
96
100
/** The timeout id of the origin clearing timeout. */
97
101
private _originTimeoutId : number ;
98
102
103
+ /**
104
+ * Whether the origin was determined via a touch interaction. Necessary as properly attributing
105
+ * focus events to touch interactions requires special logic.
106
+ */
107
+ private _originFromTouchInteraction = false ;
108
+
99
109
/** Map of elements being monitored to their info. */
100
110
private _elementInfo = new Map < HTMLElement , MonitoredElementInfo > ( ) ;
101
111
@@ -302,9 +312,15 @@ export class FocusMonitor implements OnDestroy {
302
312
}
303
313
}
304
314
305
- private _getFocusOrigin ( ) : FocusOrigin {
315
+ private _getFocusOrigin ( focusEventTarget : HTMLElement | null ) : FocusOrigin {
306
316
if ( this . _origin ) {
307
- return this . _origin ;
317
+ // If the origin was realized via a touch interaction, we need to perform additional checks
318
+ // to determine whether the focus origin should be attributed to touch or program.
319
+ if ( this . _originFromTouchInteraction ) {
320
+ return this . _shouldBeAttributedToTouch ( focusEventTarget ) ? 'touch' : 'program' ;
321
+ } else {
322
+ return this . _origin ;
323
+ }
308
324
}
309
325
310
326
// If the window has just regained focus, we can restore the most recent origin from before the
@@ -319,6 +335,29 @@ export class FocusMonitor implements OnDestroy {
319
335
return ( this . _windowFocused && this . _lastFocusOrigin ) ? this . _lastFocusOrigin : 'program' ;
320
336
}
321
337
338
+ /**
339
+ * Returns whether the focus event should be attributed to touch. Recall that in IMMEDIATE mode, a
340
+ * touch origin isn't immediately reset at the next tick (see _setOrigin). This means that when we
341
+ * handle a focus event following a touch interaction, we need to determine whether (1) the focus
342
+ * event was directly caused by the touch interaction or (2) the focus event was caused by a
343
+ * subsequent programmatic focus call triggered by the touch interaction.
344
+ * @param focusEventTarget The target of the focus event under examination.
345
+ */
346
+ private _shouldBeAttributedToTouch ( focusEventTarget : HTMLElement | null ) : boolean {
347
+ // Please note that this check is not perfect. Consider the following edge case:
348
+ //
349
+ // <div #parent tabindex="0">
350
+ // <div #child tabindex="0" (click)="#parent.focus()"></div>
351
+ // </div>
352
+ //
353
+ // Suppose there is a FocusMonitor in IMMEDIATE mode attached to #parent. When the user touches
354
+ // #child, #parent is programmatically focused. This code will attribute the focus to touch
355
+ // instead of program. This is a relatively minor edge-case that can be worked around by using
356
+ // focusVia(parent, 'program') to focus #parent.
357
+ return ( this . _detectionMode === FocusMonitorDetectionMode . EVENTUAL ) ||
358
+ ! ! focusEventTarget ?. contains ( this . _inputModalityDetector . _mostRecentTarget ) ;
359
+ }
360
+
322
361
/**
323
362
* Sets the focus classes on the element based on the given focus origin.
324
363
* @param element The element to update the classes on.
@@ -337,11 +376,12 @@ export class FocusMonitor implements OnDestroy {
337
376
* function to clear the origin at the end of a timeout. The duration of the timeout depends on
338
377
* the origin being set.
339
378
* @param origin The origin to set.
340
- * @param isFromInteractionEvent Whether we are setting the origin from an interaction event.
379
+ * @param isFromInteraction Whether we are setting the origin from an interaction event.
341
380
*/
342
- private _setOrigin ( origin : FocusOrigin , isFromInteractionEvent = false ) : void {
381
+ private _setOrigin ( origin : FocusOrigin , isFromInteraction = false ) : void {
343
382
this . _ngZone . runOutsideAngular ( ( ) => {
344
383
this . _origin = origin ;
384
+ this . _originFromTouchInteraction = ( origin === 'touch' ) && isFromInteraction ;
345
385
346
386
// If we're in IMMEDIATE mode, reset the origin at the next tick (or in `TOUCH_BUFFER_MS` ms
347
387
// for a touch event). We reset the origin at the next tick because Firefox focuses one tick
@@ -350,7 +390,7 @@ export class FocusMonitor implements OnDestroy {
350
390
// the event queue. Before doing so, clear any pending timeouts.
351
391
if ( this . _detectionMode === FocusMonitorDetectionMode . IMMEDIATE ) {
352
392
clearTimeout ( this . _originTimeoutId ) ;
353
- const ms = ( ( origin === 'touch' ) && isFromInteractionEvent ) ? TOUCH_BUFFER_MS : 1 ;
393
+ const ms = this . _originFromTouchInteraction ? TOUCH_BUFFER_MS : 1 ;
354
394
this . _originTimeoutId = setTimeout ( ( ) => this . _origin = null , ms ) ;
355
395
}
356
396
} ) ;
@@ -370,11 +410,12 @@ export class FocusMonitor implements OnDestroy {
370
410
// If we are not counting child-element-focus as focused, make sure that the event target is the
371
411
// monitored element itself.
372
412
const elementInfo = this . _elementInfo . get ( element ) ;
373
- if ( ! elementInfo || ( ! elementInfo . checkChildren && element !== getTarget ( event ) ) ) {
413
+ const focusEventTarget = getTarget ( event ) ;
414
+ if ( ! elementInfo || ( ! elementInfo . checkChildren && element !== focusEventTarget ) ) {
374
415
return ;
375
416
}
376
417
377
- this . _originChanged ( element , this . _getFocusOrigin ( ) , elementInfo ) ;
418
+ this . _originChanged ( element , this . _getFocusOrigin ( focusEventTarget ) , elementInfo ) ;
378
419
}
379
420
380
421
/**
@@ -431,7 +472,7 @@ export class FocusMonitor implements OnDestroy {
431
472
// The InputModalityDetector is also just a collection of global listeners.
432
473
this . _inputModalityDetector . modalityDetected
433
474
. pipe ( takeUntil ( this . _stopInputModalityDetector ) )
434
- . subscribe ( modality => { this . _setOrigin ( modality , true /* isFromInteractionEvent */ ) ; } ) ;
475
+ . subscribe ( modality => { this . _setOrigin ( modality , true /* isFromInteraction */ ) ; } ) ;
435
476
}
436
477
}
437
478
@@ -492,14 +533,6 @@ export class FocusMonitor implements OnDestroy {
492
533
}
493
534
}
494
535
495
- /** Gets the target of an event, accounting for Shadow DOM. */
496
- function getTarget ( event : Event ) : HTMLElement | null {
497
- // If an event is bound outside the Shadow DOM, the `event.target` will
498
- // point to the shadow root so we have to use `composedPath` instead.
499
- return ( event . composedPath ? event . composedPath ( ) [ 0 ] : event . target ) as HTMLElement | null ;
500
- }
501
-
502
-
503
536
/**
504
537
* Directive that determines how a particular element was focused (via keyboard, mouse, touch, or
505
538
* programmatically) and adds corresponding classes to the element.
0 commit comments