@samitouri / QOS-React / commits / 3c1efa0d77

[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,