@@ -21,13 +21,10 @@ import {
21
21
AfterViewInit ,
22
22
} from '@angular/core' ;
23
23
import { Observable , of as observableOf , Subject , Subscription } from 'rxjs' ;
24
+ import { takeUntil } from 'rxjs/operators' ;
24
25
import { coerceElement } from '@angular/cdk/coercion' ;
25
26
import { DOCUMENT } from '@angular/common' ;
26
- import {
27
- isFakeMousedownFromScreenReader ,
28
- isFakeTouchstartFromScreenReader ,
29
- } from '../fake-event-detection' ;
30
- import { TOUCH_BUFFER_MS } from '../input-modality/input-modality-detector' ;
27
+ import { InputModalityDetector , TOUCH_BUFFER_MS } from '../input-modality/input-modality-detector' ;
31
28
32
29
33
30
export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' | null ;
@@ -51,7 +48,7 @@ export const enum FocusMonitorDetectionMode {
51
48
IMMEDIATE ,
52
49
/**
53
50
* A focus event's origin is always attributed to the last corresponding
54
- * mousedown, keydown, or touchstart event, no matter how long ago it occured .
51
+ * mousedown, keydown, or touchstart event, no matter how long ago it occurred .
55
52
*/
56
53
EVENTUAL
57
54
}
@@ -93,12 +90,6 @@ export class FocusMonitor implements OnDestroy {
93
90
/** Whether the window has just been focused. */
94
91
private _windowFocused = false ;
95
92
96
- /** The target of the last touch event. */
97
- private _lastTouchTarget : EventTarget | null ;
98
-
99
- /** The timeout id of the touch timeout, used to cancel timeout later. */
100
- private _touchTimeoutId : number ;
101
-
102
93
/** The timeout id of the window focus timeout. */
103
94
private _windowFocusTimeoutId : number ;
104
95
@@ -125,53 +116,6 @@ export class FocusMonitor implements OnDestroy {
125
116
*/
126
117
private readonly _detectionMode : FocusMonitorDetectionMode ;
127
118
128
- /**
129
- * Event listener for `keydown` events on the document.
130
- * Needs to be an arrow function in order to preserve the context when it gets bound.
131
- */
132
- private _documentKeydownListener = ( ) => {
133
- // On keydown record the origin and clear any touch event that may be in progress.
134
- this . _lastTouchTarget = null ;
135
- this . _setOriginForCurrentEventQueue ( 'keyboard' ) ;
136
- }
137
-
138
- /**
139
- * Event listener for `mousedown` events on the document.
140
- * Needs to be an arrow function in order to preserve the context when it gets bound.
141
- */
142
- private _documentMousedownListener = ( event : MouseEvent ) => {
143
- // On mousedown record the origin only if there is not touch
144
- // target, since a mousedown can happen as a result of a touch event.
145
- if ( ! this . _lastTouchTarget ) {
146
- // In some cases screen readers fire fake `mousedown` events instead of `keydown`.
147
- // Resolve the focus source to `keyboard` if we detect one of them.
148
- const source = isFakeMousedownFromScreenReader ( event ) ? 'keyboard' : 'mouse' ;
149
- this . _setOriginForCurrentEventQueue ( source ) ;
150
- }
151
- }
152
-
153
- /**
154
- * Event listener for `touchstart` events on the document.
155
- * Needs to be an arrow function in order to preserve the context when it gets bound.
156
- */
157
- private _documentTouchstartListener = ( event : TouchEvent ) => {
158
- // Some screen readers will fire a fake `touchstart` event if an element is activated using
159
- // the keyboard while on a device with a touchsreen. Consider such events as keyboard focus.
160
- if ( ! isFakeTouchstartFromScreenReader ( event ) ) {
161
- // When the touchstart event fires the focus event is not yet in the event queue. This means
162
- // we can't rely on the trick used above (setting timeout of 1ms). Instead we wait 650ms to
163
- // see if a focus happens.
164
- if ( this . _touchTimeoutId != null ) {
165
- clearTimeout ( this . _touchTimeoutId ) ;
166
- }
167
-
168
- this . _lastTouchTarget = getTarget ( event ) ;
169
- this . _touchTimeoutId = setTimeout ( ( ) => this . _lastTouchTarget = null , TOUCH_BUFFER_MS ) ;
170
- } else if ( ! this . _lastTouchTarget ) {
171
- this . _setOriginForCurrentEventQueue ( 'keyboard' ) ;
172
- }
173
- }
174
-
175
119
/**
176
120
* Event listener for `focus` events on the window.
177
121
* Needs to be an arrow function in order to preserve the context when it gets bound.
@@ -186,9 +130,13 @@ export class FocusMonitor implements OnDestroy {
186
130
/** Used to reference correct document/window */
187
131
protected _document ?: Document ;
188
132
133
+ /** Subject for stopping our InputModalityDetector subscription. */
134
+ private readonly _stopInputModalityDetector = new Subject < void > ( ) ;
135
+
189
136
constructor (
190
137
private _ngZone : NgZone ,
191
138
private _platform : Platform ,
139
+ private readonly _inputModalityDetector : InputModalityDetector ,
192
140
/** @breaking -change 11.0.0 make document required */
193
141
@Optional ( ) @Inject ( DOCUMENT ) document : any | null ,
194
142
@Optional ( ) @Inject ( FOCUS_MONITOR_DEFAULT_OPTIONS ) options :
@@ -322,7 +270,7 @@ export class FocusMonitor implements OnDestroy {
322
270
this . _getClosestElementsInfo ( nativeElement )
323
271
. forEach ( ( [ currentElement , info ] ) => this . _originChanged ( currentElement , origin , info ) ) ;
324
272
} else {
325
- this . _setOriginForCurrentEventQueue ( origin ) ;
273
+ this . _setOrigin ( origin ) ;
326
274
327
275
// `focus` isn't available on the server
328
276
if ( typeof nativeElement . focus === 'function' ) {
@@ -354,24 +302,21 @@ export class FocusMonitor implements OnDestroy {
354
302
}
355
303
}
356
304
357
- private _getFocusOrigin ( event : FocusEvent ) : FocusOrigin {
358
- // If we couldn't detect a cause for the focus event, it's due to one of three reasons:
359
- // 1) The window has just regained focus, in which case we want to restore the focused state of
360
- // the element from before the window blurred.
361
- // 2) It was caused by a touch event, in which case we mark the origin as 'touch'.
362
- // 3) The element was programmatically focused, in which case we should mark the origin as
363
- // 'program'.
305
+ private _getFocusOrigin ( ) : FocusOrigin {
364
306
if ( this . _origin ) {
365
307
return this . _origin ;
366
308
}
367
309
368
- if ( this . _windowFocused && this . _lastFocusOrigin ) {
369
- return this . _lastFocusOrigin ;
370
- } else if ( this . _wasCausedByTouch ( event ) ) {
371
- return 'touch' ;
372
- } else {
373
- return 'program' ;
374
- }
310
+ // If the window has just regained focus, we can restore the most recent origin from before the
311
+ // window blurred. Otherwise, we've reached the point where we can't identify the source of the
312
+ // focus. This typically means one of two things happened:
313
+ //
314
+ // 1) The element was programmatically focused, or
315
+ // 2) The element was focused via screen reader navigation (which generally doesn't fire
316
+ // events).
317
+ //
318
+ // Because we can't distinguish between these two cases, we default to setting `program`.
319
+ return ( this . _windowFocused && this . _lastFocusOrigin ) ? this . _lastFocusOrigin : 'program' ;
375
320
}
376
321
377
322
/**
@@ -388,51 +333,29 @@ export class FocusMonitor implements OnDestroy {
388
333
}
389
334
390
335
/**
391
- * Sets the origin and schedules an async function to clear it at the end of the event queue.
392
- * If the detection mode is 'eventual', the origin is never cleared.
336
+ * Updates the focus origin. If we're using immediate detection mode, we schedule an async
337
+ * function to clear the origin at the end of a timeout. The duration of the timeout depends on
338
+ * the origin being set.
393
339
* @param origin The origin to set.
340
+ * @param isFromInteractionEvent Whether we are setting the origin from an interaction event.
394
341
*/
395
- private _setOriginForCurrentEventQueue ( origin : FocusOrigin ) : void {
342
+ private _setOrigin ( origin : FocusOrigin , isFromInteractionEvent = false ) : void {
396
343
this . _ngZone . runOutsideAngular ( ( ) => {
397
344
this . _origin = origin ;
398
345
346
+ // If we're in IMMEDIATE mode, reset the origin at the next tick (or in `TOUCH_BUFFER_MS` ms
347
+ // for a touch event). We reset the origin at the next tick because Firefox focuses one tick
348
+ // after the interaction event. We wait `TOUCH_BUFFER_MS` ms before resetting the origin for
349
+ // a touch event because when a touch event is fired, the associated focus event isn't yet in
350
+ // the event queue. Before doing so, clear any pending timeouts.
399
351
if ( this . _detectionMode === FocusMonitorDetectionMode . IMMEDIATE ) {
400
- // Sometimes the focus origin won't be valid in Firefox because Firefox seems to focus *one*
401
- // tick after the interaction event fired. To ensure the focus origin is always correct,
402
- // the focus origin will be determined at the beginning of the next tick.
403
- this . _originTimeoutId = setTimeout ( ( ) => this . _origin = null , 1 ) ;
352
+ clearTimeout ( this . _originTimeoutId ) ;
353
+ const ms = ( ( origin === 'touch' ) && isFromInteractionEvent ) ? TOUCH_BUFFER_MS : 1 ;
354
+ this . _originTimeoutId = setTimeout ( ( ) => this . _origin = null , ms ) ;
404
355
}
405
356
} ) ;
406
357
}
407
358
408
- /**
409
- * Checks whether the given focus event was caused by a touchstart event.
410
- * @param event The focus event to check.
411
- * @returns Whether the event was caused by a touch.
412
- */
413
- private _wasCausedByTouch ( event : FocusEvent ) : boolean {
414
- // Note(mmalerba): This implementation is not quite perfect, there is a small edge case.
415
- // Consider the following dom structure:
416
- //
417
- // <div #parent tabindex="0" cdkFocusClasses>
418
- // <div #child (click)="#parent.focus()"></div>
419
- // </div>
420
- //
421
- // If the user touches the #child element and the #parent is programmatically focused as a
422
- // result, this code will still consider it to have been caused by the touch event and will
423
- // apply the cdk-touch-focused class rather than the cdk-program-focused class. This is a
424
- // relatively small edge-case that can be worked around by using
425
- // focusVia(parentEl, 'program') to focus the parent element.
426
- //
427
- // If we decide that we absolutely must handle this case correctly, we can do so by listening
428
- // for the first focus event after the touchstart, and then the first blur event after that
429
- // focus event. When that blur event fires we know that whatever follows is not a result of the
430
- // touchstart.
431
- const focusTarget = getTarget ( event ) ;
432
- return this . _lastTouchTarget instanceof Node && focusTarget instanceof Node &&
433
- ( focusTarget === this . _lastTouchTarget || focusTarget . contains ( this . _lastTouchTarget ) ) ;
434
- }
435
-
436
359
/**
437
360
* Handles focus events on a registered element.
438
361
* @param event The focus event.
@@ -451,7 +374,7 @@ export class FocusMonitor implements OnDestroy {
451
374
return ;
452
375
}
453
376
454
- this . _originChanged ( element , this . _getFocusOrigin ( event ) , elementInfo ) ;
377
+ this . _originChanged ( element , this . _getFocusOrigin ( ) , elementInfo ) ;
455
378
}
456
379
457
380
/**
@@ -501,17 +424,14 @@ export class FocusMonitor implements OnDestroy {
501
424
// Note: we listen to events in the capture phase so we
502
425
// can detect them even if the user stops propagation.
503
426
this . _ngZone . runOutsideAngular ( ( ) => {
504
- const document = this . _getDocument ( ) ;
505
427
const window = this . _getWindow ( ) ;
506
-
507
- document . addEventListener ( 'keydown' , this . _documentKeydownListener ,
508
- captureEventListenerOptions ) ;
509
- document . addEventListener ( 'mousedown' , this . _documentMousedownListener ,
510
- captureEventListenerOptions ) ;
511
- document . addEventListener ( 'touchstart' , this . _documentTouchstartListener ,
512
- captureEventListenerOptions ) ;
513
428
window . addEventListener ( 'focus' , this . _windowFocusListener ) ;
514
429
} ) ;
430
+
431
+ // The InputModalityDetector is also just a collection of global listeners.
432
+ this . _inputModalityDetector . modalityDetected
433
+ . pipe ( takeUntil ( this . _stopInputModalityDetector ) )
434
+ . subscribe ( modality => { this . _setOrigin ( modality , true /* isFromInteractionEvent */ ) ; } ) ;
515
435
}
516
436
}
517
437
@@ -534,20 +454,14 @@ export class FocusMonitor implements OnDestroy {
534
454
535
455
// Unregister global listeners when last element is unmonitored.
536
456
if ( ! -- this . _monitoredElementCount ) {
537
- const document = this . _getDocument ( ) ;
538
457
const window = this . _getWindow ( ) ;
539
-
540
- document . removeEventListener ( 'keydown' , this . _documentKeydownListener ,
541
- captureEventListenerOptions ) ;
542
- document . removeEventListener ( 'mousedown' , this . _documentMousedownListener ,
543
- captureEventListenerOptions ) ;
544
- document . removeEventListener ( 'touchstart' , this . _documentTouchstartListener ,
545
- captureEventListenerOptions ) ;
546
458
window . removeEventListener ( 'focus' , this . _windowFocusListener ) ;
547
459
460
+ // Equivalently, stop our InputModalityDetector subscription.
461
+ this . _stopInputModalityDetector . next ( ) ;
462
+
548
463
// Clear timeouts for all potentially pending timeouts to prevent the leaks.
549
464
clearTimeout ( this . _windowFocusTimeoutId ) ;
550
- clearTimeout ( this . _touchTimeoutId ) ;
551
465
clearTimeout ( this . _originTimeoutId ) ;
552
466
}
553
467
}
0 commit comments