@@ -28,7 +28,6 @@ export const TOUCH_BUFFER_MS = 650;
28
28
29
29
export type FocusOrigin = 'touch' | 'mouse' | 'keyboard' | 'program' | null ;
30
30
31
-
32
31
/**
33
32
* Corresponds to the options that can be passed to the native `focus` event.
34
33
* via https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
@@ -38,13 +37,21 @@ export interface FocusOptions {
38
37
preventScroll ?: boolean ;
39
38
}
40
39
41
-
42
40
type MonitoredElementInfo = {
43
41
unlisten : Function ,
44
42
checkChildren : boolean ,
45
43
subject : Subject < FocusOrigin >
46
44
} ;
47
45
46
+ /**
47
+ * Event listener options that enable capturing and also
48
+ * mark the the listener as passive if the browser supports it.
49
+ */
50
+ const captureEventListenerOptions = normalizePassiveListenerOptions ( {
51
+ passive : true ,
52
+ capture : true
53
+ } ) ;
54
+
48
55
49
56
/** Monitors mouse and keyboard events to determine the cause of focus events. */
50
57
@Injectable ( { providedIn : 'root' } )
@@ -73,12 +80,57 @@ export class FocusMonitor implements OnDestroy {
73
80
/** Map of elements being monitored to their info. */
74
81
private _elementInfo = new Map < HTMLElement , MonitoredElementInfo > ( ) ;
75
82
76
- /** A map of global objects to lists of current listeners. */
77
- private _unregisterGlobalListeners = ( ) => { } ;
78
-
79
83
/** The number of elements currently being monitored. */
80
84
private _monitoredElementCount = 0 ;
81
85
86
+ /**
87
+ * Event listener for `keydown` events on the document.
88
+ * Needs to be an arrow function in order to preserve the context when it gets bound.
89
+ */
90
+ private _documentKeydownListener = ( ) => {
91
+ // On keydown record the origin and clear any touch event that may be in progress.
92
+ this . _lastTouchTarget = null ;
93
+ this . _setOriginForCurrentEventQueue ( 'keyboard' ) ;
94
+ }
95
+
96
+ /**
97
+ * Event listener for `mousedown` events on the document.
98
+ * Needs to be an arrow function in order to preserve the context when it gets bound.
99
+ */
100
+ private _documentMousedownListener = ( ) => {
101
+ // On mousedown record the origin only if there is not touch
102
+ // target, since a mousedown can happen as a result of a touch event.
103
+ if ( ! this . _lastTouchTarget ) {
104
+ this . _setOriginForCurrentEventQueue ( 'mouse' ) ;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Event listener for `touchstart` events on the document.
110
+ * Needs to be an arrow function in order to preserve the context when it gets bound.
111
+ */
112
+ private _documentTouchstartListener = ( event : TouchEvent ) => {
113
+ // When the touchstart event fires the focus event is not yet in the event queue. This means
114
+ // we can't rely on the trick used above (setting timeout of 1ms). Instead we wait 650ms to
115
+ // see if a focus happens.
116
+ if ( this . _touchTimeoutId != null ) {
117
+ clearTimeout ( this . _touchTimeoutId ) ;
118
+ }
119
+ this . _lastTouchTarget = event . target ;
120
+ this . _touchTimeoutId = setTimeout ( ( ) => this . _lastTouchTarget = null , TOUCH_BUFFER_MS ) ;
121
+ }
122
+
123
+ /**
124
+ * Event listener for `focus` events on the window.
125
+ * Needs to be an arrow function in order to preserve the context when it gets bound.
126
+ */
127
+ private _windowFocusListener = ( ) => {
128
+ // Make a note of when the window regains focus, so we can
129
+ // restore the origin info for the focused element.
130
+ this . _windowFocused = true ;
131
+ this . _windowFocusTimeoutId = setTimeout ( ( ) => this . _windowFocused = false ) ;
132
+ }
133
+
82
134
constructor ( private _ngZone : NgZone , private _platform : Platform ) { }
83
135
84
136
/**
@@ -202,78 +254,6 @@ export class FocusMonitor implements OnDestroy {
202
254
this . _elementInfo . forEach ( ( _info , element ) => this . stopMonitoring ( element ) ) ;
203
255
}
204
256
205
- /** Register necessary event listeners on the document and window. */
206
- private _registerGlobalListeners ( ) {
207
- // Do nothing if we're not on the browser platform.
208
- if ( ! this . _platform . isBrowser ) {
209
- return ;
210
- }
211
-
212
- // On keydown record the origin and clear any touch event that may be in progress.
213
- let documentKeydownListener = ( ) => {
214
- this . _lastTouchTarget = null ;
215
- this . _setOriginForCurrentEventQueue ( 'keyboard' ) ;
216
- } ;
217
-
218
- // On mousedown record the origin only if there is not touch target, since a mousedown can
219
- // happen as a result of a touch event.
220
- let documentMousedownListener = ( ) => {
221
- if ( ! this . _lastTouchTarget ) {
222
- this . _setOriginForCurrentEventQueue ( 'mouse' ) ;
223
- }
224
- } ;
225
-
226
- // When the touchstart event fires the focus event is not yet in the event queue. This means
227
- // we can't rely on the trick used above (setting timeout of 1ms). Instead we wait 650ms to
228
- // see if a focus happens.
229
- let documentTouchstartListener = ( event : TouchEvent ) => {
230
- if ( this . _touchTimeoutId != null ) {
231
- clearTimeout ( this . _touchTimeoutId ) ;
232
- }
233
- this . _lastTouchTarget = event . target ;
234
- this . _touchTimeoutId = setTimeout ( ( ) => this . _lastTouchTarget = null , TOUCH_BUFFER_MS ) ;
235
- } ;
236
-
237
- // Make a note of when the window regains focus, so we can restore the origin info for the
238
- // focused element.
239
- let windowFocusListener = ( ) => {
240
- this . _windowFocused = true ;
241
- this . _windowFocusTimeoutId = setTimeout ( ( ) => this . _windowFocused = false ) ;
242
- } ;
243
-
244
- // Event listener options that enable capturing and also mark the the listener as passive
245
- // if the browser supports it.
246
- const captureEventListenerOptions = normalizePassiveListenerOptions ( {
247
- passive : true ,
248
- capture : true
249
- } ) ;
250
-
251
- // Note: we listen to events in the capture phase so we can detect them even if the user stops
252
- // propagation.
253
- this . _ngZone . runOutsideAngular ( ( ) => {
254
- document . addEventListener ( 'keydown' , documentKeydownListener , captureEventListenerOptions ) ;
255
- document . addEventListener ( 'mousedown' , documentMousedownListener ,
256
- captureEventListenerOptions ) ;
257
- document . addEventListener ( 'touchstart' , documentTouchstartListener ,
258
- captureEventListenerOptions ) ;
259
- window . addEventListener ( 'focus' , windowFocusListener ) ;
260
- } ) ;
261
-
262
- this . _unregisterGlobalListeners = ( ) => {
263
- document . removeEventListener ( 'keydown' , documentKeydownListener , captureEventListenerOptions ) ;
264
- document . removeEventListener ( 'mousedown' , documentMousedownListener ,
265
- captureEventListenerOptions ) ;
266
- document . removeEventListener ( 'touchstart' , documentTouchstartListener ,
267
- captureEventListenerOptions ) ;
268
- window . removeEventListener ( 'focus' , windowFocusListener ) ;
269
-
270
- // Clear timeouts for all potentially pending timeouts to prevent the leaks.
271
- clearTimeout ( this . _windowFocusTimeoutId ) ;
272
- clearTimeout ( this . _touchTimeoutId ) ;
273
- clearTimeout ( this . _originTimeoutId ) ;
274
- } ;
275
- }
276
-
277
257
private _toggleClass ( element : Element , className : string , shouldSet : boolean ) {
278
258
if ( shouldSet ) {
279
259
element . classList . add ( className ) ;
@@ -406,16 +386,36 @@ export class FocusMonitor implements OnDestroy {
406
386
407
387
private _incrementMonitoredElementCount ( ) {
408
388
// Register global listeners when first element is monitored.
409
- if ( ++ this . _monitoredElementCount == 1 ) {
410
- this . _registerGlobalListeners ( ) ;
389
+ if ( ++ this . _monitoredElementCount == 1 && this . _platform . isBrowser ) {
390
+ // Note: we listen to events in the capture phase so we
391
+ // can detect them even if the user stops propagation.
392
+ this . _ngZone . runOutsideAngular ( ( ) => {
393
+ document . addEventListener ( 'keydown' , this . _documentKeydownListener ,
394
+ captureEventListenerOptions ) ;
395
+ document . addEventListener ( 'mousedown' , this . _documentMousedownListener ,
396
+ captureEventListenerOptions ) ;
397
+ document . addEventListener ( 'touchstart' , this . _documentTouchstartListener ,
398
+ captureEventListenerOptions ) ;
399
+ window . addEventListener ( 'focus' , this . _windowFocusListener ) ;
400
+ } ) ;
411
401
}
412
402
}
413
403
414
404
private _decrementMonitoredElementCount ( ) {
415
405
// Unregister global listeners when last element is unmonitored.
416
406
if ( ! -- this . _monitoredElementCount ) {
417
- this . _unregisterGlobalListeners ( ) ;
418
- this . _unregisterGlobalListeners = ( ) => { } ;
407
+ document . removeEventListener ( 'keydown' , this . _documentKeydownListener ,
408
+ captureEventListenerOptions ) ;
409
+ document . removeEventListener ( 'mousedown' , this . _documentMousedownListener ,
410
+ captureEventListenerOptions ) ;
411
+ document . removeEventListener ( 'touchstart' , this . _documentTouchstartListener ,
412
+ captureEventListenerOptions ) ;
413
+ window . removeEventListener ( 'focus' , this . _windowFocusListener ) ;
414
+
415
+ // Clear timeouts for all potentially pending timeouts to prevent the leaks.
416
+ clearTimeout ( this . _windowFocusTimeoutId ) ;
417
+ clearTimeout ( this . _touchTimeoutId ) ;
418
+ clearTimeout ( this . _originTimeoutId ) ;
419
419
}
420
420
}
421
421
0 commit comments