@@ -36,7 +36,6 @@ type FocusState = {
36
36
isFocused : boolean ,
37
37
isFocusVisible : boolean ,
38
38
pointerType : PointerType ,
39
- isEmulatingMouseEvents : boolean ,
40
39
} ;
41
40
42
41
type FocusProps = {
@@ -70,28 +69,116 @@ type FocusWithinEventType =
70
69
*/
71
70
72
71
let isGlobalFocusVisible = true ;
72
+ let hasTrackedGlobalFocusVisible = false ;
73
+ let globalFocusVisiblePointerType = '' ;
74
+ let isEmulatingMouseEvents = false ;
73
75
74
76
const isMac =
75
77
typeof window !== 'undefined' && window . navigator != null
76
78
? / ^ M a c / . test ( window . navigator . platform )
77
79
: false ;
78
80
79
- const targetEventTypes = [ 'focus' , 'blur' , 'beforeblur' ] ;
81
+ export let passiveBrowserEventsSupported = false ;
82
+
83
+ const canUseDOM : boolean = ! ! (
84
+ typeof window !== 'undefined' &&
85
+ typeof window . document !== 'undefined' &&
86
+ typeof window . document . createElement !== 'undefined'
87
+ ) ;
88
+
89
+ // Check if browser support events with passive listeners
90
+ // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support
91
+ if ( canUseDOM ) {
92
+ try {
93
+ const options = { } ;
94
+ // $FlowFixMe: Ignore Flow complaining about needing a value
95
+ Object . defineProperty ( options , 'passive' , {
96
+ get : function ( ) {
97
+ passiveBrowserEventsSupported = true ;
98
+ } ,
99
+ } ) ;
100
+ window . addEventListener ( 'test' , options , options ) ;
101
+ window . removeEventListener ( 'test' , options , options ) ;
102
+ } catch ( e ) {
103
+ passiveBrowserEventsSupported = false ;
104
+ }
105
+ }
80
106
81
107
const hasPointerEvents =
82
108
typeof window !== 'undefined' && window . PointerEvent != null ;
83
109
84
- const rootEventTypes = hasPointerEvents
85
- ? [ 'keydown' , 'keyup' , 'pointermove' , 'pointerdown' , 'pointerup' , 'blur' ]
86
- : [
87
- 'keydown' ,
88
- 'keyup' ,
89
- 'mousedown' ,
90
- 'touchmove' ,
91
- 'touchstart' ,
92
- 'touchend' ,
93
- 'blur' ,
94
- ] ;
110
+ const focusVisibleEvents = hasPointerEvents
111
+ ? [ 'keydown' , 'keyup' , 'pointermove' , 'pointerdown' , 'pointerup' ]
112
+ : [ 'keydown' , 'keyup' , 'mousedown' , 'touchmove' , 'touchstart' , 'touchend' ] ;
113
+
114
+ const targetEventTypes = [ 'focus' , 'blur' , 'beforeblur' , ...focusVisibleEvents ] ;
115
+
116
+ // Used only for the blur "detachedTarget" logic
117
+ const rootEventTypes = [ 'blur' ] ;
118
+
119
+ function addWindowEventListener ( types , callback , options ) {
120
+ types . forEach ( type => {
121
+ window . addEventListener ( type , callback , options ) ;
122
+ } ) ;
123
+ }
124
+
125
+ function trackGlobalFocusVisible ( ) {
126
+ if ( ! hasTrackedGlobalFocusVisible ) {
127
+ hasTrackedGlobalFocusVisible = true ;
128
+ addWindowEventListener (
129
+ focusVisibleEvents ,
130
+ handleGlobalFocusVisibleEvent ,
131
+ passiveBrowserEventsSupported ? { capture : true , passive : true } : true ,
132
+ ) ;
133
+ }
134
+ }
135
+
136
+ function handleGlobalFocusVisibleEvent (
137
+ nativeEvent : MouseEvent | TouchEvent | KeyboardEvent ,
138
+ ) : void {
139
+ const { type} = nativeEvent ;
140
+
141
+ switch ( type ) {
142
+ case 'pointermove' :
143
+ case 'pointerdown' :
144
+ case 'pointerup' : {
145
+ isGlobalFocusVisible = false ;
146
+ globalFocusVisiblePointerType = ( nativeEvent : any ) . pointerType ;
147
+ break ;
148
+ }
149
+
150
+ case 'keydown' :
151
+ case 'keyup' : {
152
+ const { metaKey, altKey, ctrlKey} = nativeEvent ;
153
+ const validKey = ! ( metaKey || ( ! isMac && altKey ) || ctrlKey ) ;
154
+
155
+ if ( validKey ) {
156
+ globalFocusVisiblePointerType = 'keyboard' ;
157
+ isGlobalFocusVisible = true ;
158
+ }
159
+ break ;
160
+ }
161
+
162
+ // fallbacks for no PointerEvent support
163
+ case 'touchmove' :
164
+ case 'touchstart' :
165
+ case 'touchend' : {
166
+ isEmulatingMouseEvents = true ;
167
+ isGlobalFocusVisible = false ;
168
+ globalFocusVisiblePointerType = 'touch' ;
169
+ break ;
170
+ }
171
+ case 'mousedown' : {
172
+ if ( ! isEmulatingMouseEvents ) {
173
+ isGlobalFocusVisible = false ;
174
+ globalFocusVisiblePointerType = 'mouse' ;
175
+ } else {
176
+ isEmulatingMouseEvents = false ;
177
+ }
178
+ break ;
179
+ }
180
+ }
181
+ }
95
182
96
183
function isFunction ( obj ) : boolean {
97
184
return typeof obj === 'function ';
@@ -121,7 +208,7 @@ function createFocusEvent(
121
208
} ;
122
209
}
123
210
124
- function handleRootPointerEvent (
211
+ function handleFocusVisibleTargetEvent (
125
212
event : ReactDOMResponderEvent ,
126
213
context : ReactDOMResponderContext ,
127
214
state : FocusState ,
@@ -135,29 +222,26 @@ function handleRootPointerEvent(
135
222
const focusTarget = state . focusTarget ;
136
223
if (
137
224
focusTarget !== null &&
138
- context . isTargetWithinResponderScope ( focusTarget ) &&
139
225
( type === 'mousedown' || type === 'touchstart' || type === 'pointerdown' )
140
226
) {
141
227
callback ( false ) ;
142
228
}
143
229
}
144
230
145
- function handleRootEvent (
231
+ function handleFocusVisibleTargetEvents (
146
232
event : ReactDOMResponderEvent ,
147
233
context : ReactDOMResponderContext ,
148
234
state : FocusState ,
149
235
callback : boolean => void ,
150
236
) : void {
151
237
const { type } = event ;
238
+ state . pointerType = globalFocusVisiblePointerType ;
152
239
153
240
switch ( type ) {
154
241
case 'pointermove ':
155
242
case 'pointerdown ':
156
243
case 'pointerup ': {
157
- // $FlowFixMe: Flow doesn't know about PointerEvents
158
- const nativeEvent = ( ( event . nativeEvent : any ) : PointerEvent ) ;
159
- state . pointerType = nativeEvent . pointerType ;
160
- handleRootPointerEvent ( event , context , state , callback ) ;
244
+ handleFocusVisibleTargetEvent ( event , context , state , callback ) ;
161
245
break ;
162
246
}
163
247
@@ -169,12 +253,7 @@ function handleRootEvent(
169
253
const validKey = ! ( metaKey || ( ! isMac && altKey ) || ctrlKey ) ;
170
254
171
255
if ( validKey ) {
172
- state . pointerType = 'keyboard' ;
173
- isGlobalFocusVisible = true ;
174
- if (
175
- focusTarget !== null &&
176
- context . isTargetWithinResponderScope ( focusTarget )
177
- ) {
256
+ if ( focusTarget !== null ) {
178
257
callback ( true ) ;
179
258
}
180
259
}
@@ -185,17 +264,12 @@ function handleRootEvent(
185
264
case 'touchmove' :
186
265
case 'touchstart' :
187
266
case 'touchend' : {
188
- state . pointerType = 'touch' ;
189
- state . isEmulatingMouseEvents = true ;
190
- handleRootPointerEvent ( event , context , state , callback ) ;
267
+ handleFocusVisibleTargetEvent ( event , context , state , callback ) ;
191
268
break ;
192
269
}
193
270
case 'mousedown' : {
194
- if ( ! state . isEmulatingMouseEvents ) {
195
- state . pointerType = 'mouse' ;
196
- handleRootPointerEvent ( event , context , state , callback ) ;
197
- } else {
198
- state . isEmulatingMouseEvents = false ;
271
+ if ( ! isEmulatingMouseEvents ) {
272
+ handleFocusVisibleTargetEvent ( event , context , state , callback ) ;
199
273
}
200
274
break ;
201
275
}
@@ -332,17 +406,18 @@ function unmountFocusResponder(
332
406
const focusResponderImpl = {
333
407
targetEventTypes,
334
408
targetPortalPropagation : true ,
335
- rootEventTypes,
336
409
getInitialState ( ) : FocusState {
337
410
return {
338
411
detachedTarget : null ,
339
412
focusTarget : null ,
340
- isEmulatingMouseEvents : false ,
341
413
isFocused : false ,
342
414
isFocusVisible : false ,
343
415
pointerType : '' ,
344
416
} ;
345
417
} ,
418
+ onMount ( ) {
419
+ trackGlobalFocusVisible ( ) ;
420
+ } ,
346
421
onEvent (
347
422
event : ReactDOMResponderEvent ,
348
423
context : ReactDOMResponderContext ,
@@ -370,7 +445,7 @@ const focusResponderImpl = {
370
445
state . isFocusVisible = isGlobalFocusVisible ;
371
446
dispatchFocusEvents ( context , props , state ) ;
372
447
}
373
- state . isEmulatingMouseEvents = false ;
448
+ isEmulatingMouseEvents = false ;
374
449
break ;
375
450
}
376
451
case 'blur' : {
@@ -389,24 +464,23 @@ const focusResponderImpl = {
389
464
if ( event . nativeEvent . relatedTarget == null ) {
390
465
state . pointerType = '' ;
391
466
}
392
- state . isEmulatingMouseEvents = false ;
467
+ isEmulatingMouseEvents = false ;
393
468
break ;
394
469
}
470
+ default :
471
+ handleFocusVisibleTargetEvents (
472
+ event ,
473
+ context ,
474
+ state ,
475
+ isFocusVisible => {
476
+ if ( state . isFocused && state . isFocusVisible !== isFocusVisible ) {
477
+ state . isFocusVisible = isFocusVisible ;
478
+ dispatchFocusVisibleChangeEvent ( context , props , isFocusVisible ) ;
479
+ }
480
+ } ,
481
+ ) ;
395
482
}
396
483
} ,
397
- onRootEvent (
398
- event : ReactDOMResponderEvent ,
399
- context : ReactDOMResponderContext ,
400
- props : FocusProps ,
401
- state : FocusState ,
402
- ) : void {
403
- handleRootEvent ( event , context , state , isFocusVisible => {
404
- if ( state . isFocused && state . isFocusVisible !== isFocusVisible ) {
405
- state . isFocusVisible = isFocusVisible ;
406
- dispatchFocusVisibleChangeEvent ( context , props , isFocusVisible ) ;
407
- }
408
- } ) ;
409
- } ,
410
484
onUnmount (
411
485
context : ReactDOMResponderContext ,
412
486
props : FocusProps ,
@@ -471,17 +545,18 @@ function unmountFocusWithinResponder(
471
545
const focusWithinResponderImpl = {
472
546
targetEventTypes,
473
547
targetPortalPropagation : true ,
474
- rootEventTypes ,
475
548
getInitialState ( ) : FocusState {
476
549
return {
477
550
detachedTarget : null ,
478
551
focusTarget : null ,
479
- isEmulatingMouseEvents : false ,
480
552
isFocused : false ,
481
553
isFocusVisible : false ,
482
554
pointerType : '' ,
483
555
} ;
484
556
} ,
557
+ onMount ( ) {
558
+ trackGlobalFocusVisible ( ) ;
559
+ } ,
485
560
onEvent (
486
561
event : ReactDOMResponderEvent ,
487
562
context : ReactDOMResponderContext ,
@@ -544,12 +619,31 @@ const focusWithinResponderImpl = {
544
619
onBeforeBlurWithin ,
545
620
DiscreteEvent ,
546
621
) ;
622
+ context . addRootEventTypes ( rootEventTypes ) ;
547
623
} else {
548
624
// We want to propagate to next focusWithin responder
549
625
// if this responder doesn't handle beforeblur
550
626
context . continuePropagation ( ) ;
551
627
}
628
+ break ;
552
629
}
630
+ default :
631
+ handleFocusVisibleTargetEvents (
632
+ event ,
633
+ context ,
634
+ state ,
635
+ isFocusVisible => {
636
+ if ( state . isFocused && state . isFocusVisible !== isFocusVisible ) {
637
+ state . isFocusVisible = isFocusVisible ;
638
+ dispatchFocusWithinVisibleChangeEvent (
639
+ context ,
640
+ props ,
641
+ state ,
642
+ isFocusVisible ,
643
+ ) ;
644
+ }
645
+ } ,
646
+ ) ;
553
647
}
554
648
} ,
555
649
onRootEvent (
@@ -563,20 +657,9 @@ const focusWithinResponderImpl = {
563
657
if ( detachedTarget !== null && detachedTarget === event . target ) {
564
658
dispatchBlurWithinEvents ( context , event , props , state ) ;
565
659
state . detachedTarget = null ;
660
+ context . removeRootEventTypes ( rootEventTypes ) ;
566
661
}
567
- return ;
568
662
}
569
- handleRootEvent ( event , context , state , isFocusVisible => {
570
- if ( state . isFocused && state . isFocusVisible !== isFocusVisible ) {
571
- state . isFocusVisible = isFocusVisible ;
572
- dispatchFocusWithinVisibleChangeEvent (
573
- context ,
574
- props ,
575
- state ,
576
- isFocusVisible ,
577
- ) ;
578
- }
579
- } ) ;
580
663
} ,
581
664
onUnmount (
582
665
context : ReactDOMResponderContext ,
0 commit comments