@@ -12,8 +12,11 @@ import {
12
12
ElementRef ,
13
13
EventEmitter ,
14
14
Injectable ,
15
+ Inject ,
16
+ InjectionToken ,
15
17
NgZone ,
16
18
OnDestroy ,
19
+ Optional ,
17
20
Output ,
18
21
} from '@angular/core' ;
19
22
import { Observable , of as observableOf , Subject , Subscription } from 'rxjs' ;
@@ -36,9 +39,38 @@ export interface FocusOptions {
36
39
preventScroll ?: boolean ;
37
40
}
38
41
42
+ /** A set of options to apply to calls of FocusMonitor.monitor(). */
43
+ export interface FocusMonitorOptions {
44
+ /** Whether to count the element as focused when its children are focused. */
45
+ checkChildren : boolean ;
46
+
47
+ /**
48
+ * The time window (in milliseconds) during which a mousedown/keydown/touchstart event is
49
+ * considered to be "in play" for the pupose of assigning a focus event's origin. Set to
50
+ * 'indefinite' to always attribute a focus event's origin to the last corresponding event's
51
+ * type, no matter how long ago it occured.
52
+ */
53
+ detectionWindow : number | 'indefinite' ;
54
+ }
55
+
56
+ /** Default options used when a dependency injected options object is not provided. */
57
+ const FOCUS_MONITOR_DEFAULT_OPTIONS : FocusMonitorOptions = {
58
+ checkChildren : false ,
59
+ // A default of 1ms is used because Firefox seems to focus *one* tick after the interaction
60
+ // event fired, causing the focus origin to be misinterpreted. To ensure the focus origin
61
+ // is always correct, the focus origin should be determined at least starting from the
62
+ // next tick.
63
+ detectionWindow : 1 ,
64
+ } ;
65
+
66
+ /** InjectionToken that can be used to specify the global FocusMonitorOptions. */
67
+ export const FOCUS_MONITOR_GLOBAL_OPTIONS =
68
+ new InjectionToken < Partial < FocusMonitorOptions > > ( 'cdk-focus-monitor-global-options' ) ;
69
+
39
70
type MonitoredElementInfo = {
40
71
unlisten : Function ,
41
72
checkChildren : boolean ,
73
+ detectionWindow : number | 'indefinite' ,
42
74
subject : Subject < FocusOrigin >
43
75
} ;
44
76
@@ -58,6 +90,9 @@ export class FocusMonitor implements OnDestroy {
58
90
/** The focus origin that the next focus event is a result of. */
59
91
private _origin : FocusOrigin = null ;
60
92
93
+ /** Timestamp that the current FocusOrigin was set at, in epoch milliseconds. */
94
+ private _originTimestamp = 0 ;
95
+
61
96
/** The FocusOrigin of the last focus event tracked by the FocusMonitor. */
62
97
private _lastFocusOrigin : FocusOrigin ;
63
98
@@ -73,15 +108,15 @@ export class FocusMonitor implements OnDestroy {
73
108
/** The timeout id of the window focus timeout. */
74
109
private _windowFocusTimeoutId : number ;
75
110
76
- /** The timeout id of the origin clearing timeout. */
77
- private _originTimeoutId : number ;
78
-
79
111
/** Map of elements being monitored to their info. */
80
112
private _elementInfo = new Map < HTMLElement , MonitoredElementInfo > ( ) ;
81
113
82
114
/** The number of elements currently being monitored. */
83
115
private _monitoredElementCount = 0 ;
84
116
117
+ /** Options to apply to all calls to monitor(). */
118
+ private _focusMonitorOptions : FocusMonitorOptions ;
119
+
85
120
/**
86
121
* Event listener for `keydown` events on the document.
87
122
* Needs to be an arrow function in order to preserve the context when it gets bound.
@@ -134,7 +169,13 @@ export class FocusMonitor implements OnDestroy {
134
169
this . _windowFocusTimeoutId = setTimeout ( ( ) => this . _windowFocused = false ) ;
135
170
}
136
171
137
- constructor ( private _ngZone : NgZone , private _platform : Platform ) { }
172
+ constructor (
173
+ private _ngZone : NgZone , private _platform : Platform ,
174
+ @Optional ( ) @Inject ( FOCUS_MONITOR_GLOBAL_OPTIONS ) globalFocusMonitorOptions ?:
175
+ Partial < FocusMonitorOptions > ) {
176
+ this . _focusMonitorOptions =
177
+ coerceFocusMonitorOptions ( globalFocusMonitorOptions , FOCUS_MONITOR_DEFAULT_OPTIONS ) ;
178
+ }
138
179
139
180
/**
140
181
* Monitors focus on an element and applies appropriate CSS classes.
@@ -154,26 +195,47 @@ export class FocusMonitor implements OnDestroy {
154
195
*/
155
196
monitor ( element : ElementRef < HTMLElement > , checkChildren ?: boolean ) : Observable < FocusOrigin > ;
156
197
157
- monitor ( element : HTMLElement | ElementRef < HTMLElement > ,
158
- checkChildren : boolean = false ) : Observable < FocusOrigin > {
198
+ /**
199
+ * Monitors focus on an element and applies appropriate CSS classes.
200
+ * @param element The element to monitor
201
+ * @param options Options to use for focus events on the provided element.
202
+ * @returns An observable that emits when the focus state of the element changes.
203
+ * When the element is blurred, null will be emitted.
204
+ */
205
+ monitor ( element : HTMLElement , options ?: FocusMonitorOptions ) : Observable < FocusOrigin > ;
206
+
207
+ /**
208
+ * Monitors focus on an element and applies appropriate CSS classes.
209
+ * @param element The element to monitor
210
+ * @param options Options to use for focus events on the provided element
211
+ * @returns An observable that emits when the focus state of the element changes.
212
+ * When the element is blurred, null will be emitted.
213
+ */
214
+ monitor ( element : ElementRef < HTMLElement > , options ?: FocusMonitorOptions ) : Observable < FocusOrigin > ;
215
+
216
+ monitor ( element : HTMLElement | ElementRef < HTMLElement > , options ?: FocusMonitorOptions | boolean ) :
217
+ Observable < FocusOrigin > {
159
218
// Do nothing if we're not on the browser platform.
160
219
if ( ! this . _platform . isBrowser ) {
161
220
return observableOf ( null ) ;
162
221
}
163
222
223
+ options = coerceFocusMonitorOptions ( options , this . _focusMonitorOptions ) ;
224
+
164
225
const nativeElement = coerceElement ( element ) ;
165
226
166
227
// Check if we're already monitoring this element.
167
228
if ( this . _elementInfo . has ( nativeElement ) ) {
168
229
let cachedInfo = this . _elementInfo . get ( nativeElement ) ;
169
- cachedInfo ! . checkChildren = checkChildren ;
230
+ cachedInfo ! . checkChildren = options . checkChildren ;
170
231
return cachedInfo ! . subject . asObservable ( ) ;
171
232
}
172
233
173
234
// Create monitored element info.
174
235
let info : MonitoredElementInfo = {
175
236
unlisten : ( ) => { } ,
176
- checkChildren : checkChildren ,
237
+ checkChildren : options . checkChildren ,
238
+ detectionWindow : options . detectionWindow ,
177
239
subject : new Subject < FocusOrigin > ( )
178
240
} ;
179
241
this . _elementInfo . set ( nativeElement , info ) ;
@@ -241,7 +303,6 @@ export class FocusMonitor implements OnDestroy {
241
303
focusVia ( element : HTMLElement | ElementRef < HTMLElement > ,
242
304
origin : FocusOrigin ,
243
305
options ?: FocusOptions ) : void {
244
-
245
306
const nativeElement = coerceElement ( element ) ;
246
307
247
308
this . _setOriginForCurrentEventQueue ( origin ) ;
@@ -283,16 +344,13 @@ export class FocusMonitor implements OnDestroy {
283
344
}
284
345
285
346
/**
286
- * Sets the origin and schedules an async function to clear it at the end of the event queue .
347
+ * Sets the origin and sets origin timestamp to now .
287
348
* @param origin The origin to set.
288
349
*/
289
350
private _setOriginForCurrentEventQueue ( origin : FocusOrigin ) : void {
290
351
this . _ngZone . runOutsideAngular ( ( ) => {
291
352
this . _origin = origin ;
292
- // Sometimes the focus origin won't be valid in Firefox because Firefox seems to focus *one*
293
- // tick after the interaction event fired. To ensure the focus origin is always correct,
294
- // the focus origin will be determined at the beginning of the next tick.
295
- this . _originTimeoutId = setTimeout ( ( ) => this . _origin = null , 1 ) ;
353
+ this . _originTimestamp = new Date ( ) . valueOf ( ) ;
296
354
} ) ;
297
355
}
298
356
@@ -349,7 +407,7 @@ export class FocusMonitor implements OnDestroy {
349
407
// 3) The element was programmatically focused, in which case we should mark the origin as
350
408
// 'program'.
351
409
let origin = this . _origin ;
352
- if ( ! origin ) {
410
+ if ( ! origin || this . _isOriginInvalid ( elementInfo . detectionWindow ) ) {
353
411
if ( this . _windowFocused && this . _lastFocusOrigin ) {
354
412
origin = this . _lastFocusOrigin ;
355
413
} else if ( this . _wasCausedByTouch ( event ) ) {
@@ -418,11 +476,41 @@ export class FocusMonitor implements OnDestroy {
418
476
// Clear timeouts for all potentially pending timeouts to prevent the leaks.
419
477
clearTimeout ( this . _windowFocusTimeoutId ) ;
420
478
clearTimeout ( this . _touchTimeoutId ) ;
421
- clearTimeout ( this . _originTimeoutId ) ;
422
479
}
423
480
}
481
+
482
+ /** A focus origin is invalid if it occured before (now - detectionWindow). */
483
+ private _isOriginInvalid ( detectionWindow : number | 'indefinite' ) : boolean {
484
+ if ( detectionWindow === 'indefinite' ) {
485
+ return false ;
486
+ }
487
+
488
+ const now = new Date ( ) . valueOf ( ) ;
489
+ return now - this . _originTimestamp > detectionWindow ;
490
+ }
424
491
}
425
492
493
+ /**
494
+ * Takes a partial set of options and a complete default set and merges them. A boolean value for
495
+ * options corresponds to the checkChildren parameter.
496
+ */
497
+ function coerceFocusMonitorOptions (
498
+ options : Partial < FocusMonitorOptions > | boolean | undefined ,
499
+ defaultOptions : FocusMonitorOptions ) : FocusMonitorOptions {
500
+ if ( ! options ) {
501
+ return defaultOptions ;
502
+ }
503
+ if ( typeof options === 'boolean' ) {
504
+ options = { checkChildren : options } ;
505
+ }
506
+
507
+ return {
508
+ checkChildren : options . checkChildren !== undefined ? options . checkChildren :
509
+ defaultOptions . checkChildren ,
510
+ detectionWindow : options . detectionWindow ? options . detectionWindow :
511
+ defaultOptions . detectionWindow ,
512
+ } ;
513
+ }
426
514
427
515
/**
428
516
* Directive that determines how a particular element was focused (via keyboard, mouse, touch, or
0 commit comments