[react-interactions] Remove Focus/FocusWithin root event types (#17555)
Dominic Gannaway committed
Dec 9, 2019 at 14:27 UTC
3c1efa0d771d3dfb2666b7f4a4392cc851146d44
1 file changed
+148
-65
packages/react-interactions/events/src/dom/Focus.js
+148
-65
@@ -36,7 +36,6 @@ type FocusState = {
36
isFocused: boolean,
37
isFocusVisible: boolean,
38
pointerType: PointerType,
39
- isEmulatingMouseEvents: boolean,
39
};
40
41
type FocusProps = {
@@ -70,28 +69,116 @@ type FocusWithinEventType =
69
*/
70
71
let isGlobalFocusVisible = true;
72
+let hasTrackedGlobalFocusVisible = false;
73
+let globalFocusVisiblePointerType = '';
74
+let isEmulatingMouseEvents = false;
75
76
const isMac =
77
typeof window !== 'undefined' && window.navigator != null
78
? /^Mac/.test(window.navigator.platform)
79
: false;
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
+}
106
107
const hasPointerEvents =
108
typeof window !== 'undefined' && window.PointerEvent != null;
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
+}
182
183
function isFunction(obj): boolean {
184
return typeof obj === 'function';
@@ -121,7 +208,7 @@ function createFocusEvent(
208
};
209
}
210
124
-function handleRootPointerEvent(
211
+function handleFocusVisibleTargetEvent(
212
event: ReactDOMResponderEvent,
213
context: ReactDOMResponderContext,
214
state: FocusState,
@@ -135,29 +222,26 @@ function handleRootPointerEvent(
222
const focusTarget = state.focusTarget;
223
if (
224
focusTarget !== null &&
138
- context.isTargetWithinResponderScope(focusTarget) &&
225
(type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
226
) {
227
callback(false);
228
}
229
}
230
145
-function handleRootEvent(
231
+function handleFocusVisibleTargetEvents(
232
event: ReactDOMResponderEvent,
233
context: ReactDOMResponderContext,
234
state: FocusState,
235
callback: boolean => void,
236
): void {
237
const {type} = event;
238
+ state.pointerType = globalFocusVisiblePointerType;
239
240
switch (type) {
241
case 'pointermove':
242
case 'pointerdown':
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);
245
break;
246
}
247
@@ -169,12 +253,7 @@ function handleRootEvent(
253
const validKey = !(metaKey || (!isMac && altKey) || ctrlKey);
254
255
if (validKey) {
172
- state.pointerType = 'keyboard';
173
- isGlobalFocusVisible = true;
174
- if (
175
- focusTarget !== null &&
176
- context.isTargetWithinResponderScope(focusTarget)
177
- ) {
256
+ if (focusTarget !== null) {
257
callback(true);
258
}
259
}
@@ -185,17 +264,12 @@ function handleRootEvent(
264
case 'touchmove':
265
case 'touchstart':
266
case 'touchend': {
188
- state.pointerType = 'touch';
189
- state.isEmulatingMouseEvents = true;
190
- handleRootPointerEvent(event, context, state, callback);
267
+ handleFocusVisibleTargetEvent(event, context, state, callback);
268
break;
269
}
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);
273
}
274
break;
275
}
@@ -332,17 +406,18 @@ function unmountFocusResponder(
406
const focusResponderImpl = {
407
targetEventTypes,
408
targetPortalPropagation: true,
335
- rootEventTypes,
409
getInitialState(): FocusState {
410
return {
411
detachedTarget: null,
412
focusTarget: null,
340
- isEmulatingMouseEvents: false,
413
isFocused: false,
414
isFocusVisible: false,
415
pointerType: '',
416
};
417
},
418
+ onMount() {
419
+ trackGlobalFocusVisible();
420
+ },
421
onEvent(
422
event: ReactDOMResponderEvent,
423
context: ReactDOMResponderContext,
@@ -370,7 +445,7 @@ const focusResponderImpl = {
445
state.isFocusVisible = isGlobalFocusVisible;
446
dispatchFocusEvents(context, props, state);
447
}
373
- state.isEmulatingMouseEvents = false;
448
+ isEmulatingMouseEvents = false;
449
break;
450
}
451
case 'blur': {
@@ -389,24 +464,23 @@ const focusResponderImpl = {
464
if (event.nativeEvent.relatedTarget == null) {
465
state.pointerType = '';
466
}
392
- state.isEmulatingMouseEvents = false;
467
+ isEmulatingMouseEvents = false;
468
break;
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
+ );
482
}
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
- },
484
onUnmount(
485
context: ReactDOMResponderContext,
486
props: FocusProps,
@@ -471,17 +545,18 @@ function unmountFocusWithinResponder(
545
const focusWithinResponderImpl = {
546
targetEventTypes,
547
targetPortalPropagation: true,
474
- rootEventTypes,
548
getInitialState(): FocusState {
549
return {
550
detachedTarget: null,
551
focusTarget: null,
479
- isEmulatingMouseEvents: false,
552
isFocused: false,
553
isFocusVisible: false,
554
pointerType: '',
555
};
556
},
557
+ onMount() {
558
+ trackGlobalFocusVisible();
559
+ },
560
onEvent(
561
event: ReactDOMResponderEvent,
562
context: ReactDOMResponderContext,
@@ -544,12 +619,31 @@ const focusWithinResponderImpl = {
619
onBeforeBlurWithin,
620
DiscreteEvent,
621
);
622
+ context.addRootEventTypes(rootEventTypes);
623
} else {
624
// We want to propagate to next focusWithin responder
625
// if this responder doesn't handle beforeblur
626
context.continuePropagation();
627
}
628
+ break;
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
+ );
647
}
648
},
649
onRootEvent(
@@ -563,20 +657,9 @@ const focusWithinResponderImpl = {
657
if (detachedTarget !== null && detachedTarget === event.target) {
658
dispatchBlurWithinEvents(context, event, props, state);
659
state.detachedTarget = null;
660
+ context.removeRootEventTypes(rootEventTypes);
661
}
567
- return;
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
- });
663
},
664
onUnmount(
665
context: ReactDOMResponderContext,