@@ -24,10 +24,10 @@ import {Observable, of as observableOf, Subject, Subscription} from 'rxjs';
24
24
import { coerceElement } from '@angular/cdk/coercion' ;
25
25
import { DOCUMENT } from '@angular/common' ;
26
26
import {
27
- isFakeMousedownFromScreenReader ,
28
- isFakeTouchstartFromScreenReader ,
29
- } from '../fake-event-detection' ;
30
- import { TOUCH_BUFFER_MS } from '../input-modality/input-modality-detector' ;
27
+ InputModality ,
28
+ InputModalityDetector ,
29
+ TOUCH_BUFFER_MS ,
30
+ } from '../input-modality/input-modality-detector' ;
31
31
32
32
33
33
export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' | null ;
@@ -93,12 +93,6 @@ export class FocusMonitor implements OnDestroy {
93
93
/** Whether the window has just been focused. */
94
94
private _windowFocused = false ;
95
95
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
96
/** The timeout id of the window focus timeout. */
103
97
private _windowFocusTimeoutId : number ;
104
98
@@ -125,53 +119,6 @@ export class FocusMonitor implements OnDestroy {
125
119
*/
126
120
private readonly _detectionMode : FocusMonitorDetectionMode ;
127
121
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
122
/**
176
123
* Event listener for `focus` events on the window.
177
124
* Needs to be an arrow function in order to preserve the context when it gets bound.
@@ -189,12 +136,18 @@ export class FocusMonitor implements OnDestroy {
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 :
195
143
FocusMonitorOptions | null ) {
196
144
this . _document = document ;
197
145
this . _detectionMode = options ?. detectionMode || FocusMonitorDetectionMode . IMMEDIATE ;
146
+
147
+ this . _inputModalityDetector . inputModalityDetected
148
+ . subscribe ( ( modality : InputModality ) => {
149
+ this . _setOrigin ( modality ) ;
150
+ } ) ;
198
151
}
199
152
/**
200
153
* Event listener for `focus` and 'blur' events on the document.
@@ -322,7 +275,7 @@ export class FocusMonitor implements OnDestroy {
322
275
this . _getClosestElementsInfo ( nativeElement )
323
276
. forEach ( ( [ currentElement , info ] ) => this . _originChanged ( currentElement , origin , info ) ) ;
324
277
} else {
325
- this . _setOriginForCurrentEventQueue ( origin ) ;
278
+ this . _setOrigin ( origin ) ;
326
279
327
280
// `focus` isn't available on the server
328
281
if ( typeof nativeElement . focus === 'function' ) {
@@ -354,21 +307,18 @@ export class FocusMonitor implements OnDestroy {
354
307
}
355
308
}
356
309
357
- private _getFocusOrigin ( event : FocusEvent ) : FocusOrigin {
310
+ private _getFocusOrigin ( ) : FocusOrigin {
358
311
// If we couldn't detect a cause for the focus event, it's due to one of three reasons:
359
312
// 1) The window has just regained focus, in which case we want to restore the focused state of
360
313
// 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
314
+ // 2) The element was programmatically focused, in which case we should mark the origin as
363
315
// 'program'.
364
316
if ( this . _origin ) {
365
317
return this . _origin ;
366
318
}
367
319
368
320
if ( this . _windowFocused && this . _lastFocusOrigin ) {
369
321
return this . _lastFocusOrigin ;
370
- } else if ( this . _wasCausedByTouch ( event ) ) {
371
- return 'touch' ;
372
322
} else {
373
323
return 'program' ;
374
324
}
@@ -388,51 +338,26 @@ export class FocusMonitor implements OnDestroy {
388
338
}
389
339
390
340
/**
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.
341
+ * Updates the focus origin. If we're using immediate detection mode, we schedule an async
342
+ * function to clear the origin at the end of a timeout. The duration of the timeout depends on
343
+ * the origin being set.
393
344
* @param origin The origin to set.
394
345
*/
395
- private _setOriginForCurrentEventQueue ( origin : FocusOrigin ) : void {
346
+ private _setOrigin ( origin : FocusOrigin ) : void {
396
347
this . _ngZone . runOutsideAngular ( ( ) => {
397
348
this . _origin = origin ;
398
349
399
350
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 ) ;
351
+ // When a touch origin is received, we need to wait at least `TOUCH_BUFFER_MS` ms until
352
+ // clearing the origin. This is because when a touch event is fired, the associated focus
353
+ // event isn't yet in the event queue. Otherwise, clear the focus origin at the start of the
354
+ // next tick (because Firefox focuses one tick after the interaction event).
355
+ const ms = ( origin === 'touch' ) ? TOUCH_BUFFER_MS : 1 ;
356
+ this . _originTimeoutId = setTimeout ( ( ) => this . _origin = null , ms ) ;
404
357
}
405
358
} ) ;
406
359
}
407
360
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
361
/**
437
362
* Handles focus events on a registered element.
438
363
* @param event The focus event.
@@ -451,7 +376,7 @@ export class FocusMonitor implements OnDestroy {
451
376
return ;
452
377
}
453
378
454
- this . _originChanged ( element , this . _getFocusOrigin ( event ) , elementInfo ) ;
379
+ this . _originChanged ( element , this . _getFocusOrigin ( ) , elementInfo ) ;
455
380
}
456
381
457
382
/**
@@ -501,15 +426,7 @@ export class FocusMonitor implements OnDestroy {
501
426
// Note: we listen to events in the capture phase so we
502
427
// can detect them even if the user stops propagation.
503
428
this . _ngZone . runOutsideAngular ( ( ) => {
504
- const document = this . _getDocument ( ) ;
505
429
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
430
window . addEventListener ( 'focus' , this . _windowFocusListener ) ;
514
431
} ) ;
515
432
}
@@ -534,20 +451,11 @@ export class FocusMonitor implements OnDestroy {
534
451
535
452
// Unregister global listeners when last element is unmonitored.
536
453
if ( ! -- this . _monitoredElementCount ) {
537
- const document = this . _getDocument ( ) ;
538
454
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
455
window . removeEventListener ( 'focus' , this . _windowFocusListener ) ;
547
456
548
457
// Clear timeouts for all potentially pending timeouts to prevent the leaks.
549
458
clearTimeout ( this . _windowFocusTimeoutId ) ;
550
- clearTimeout ( this . _touchTimeoutId ) ;
551
459
clearTimeout ( this . _originTimeoutId ) ;
552
460
}
553
461
}
0 commit comments