Skip to content

Commit 3c1efa0

Browse files
authored
[react-interactions] Remove Focus/FocusWithin root event types (#17555)
1 parent 9e937e7 commit 3c1efa0

File tree

1 file changed

+148
-65
lines changed
  • packages/react-interactions/events/src/dom

1 file changed

+148
-65
lines changed

packages/react-interactions/events/src/dom/Focus.js

+148-65
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ type FocusState = {
3636
isFocused: boolean,
3737
isFocusVisible: boolean,
3838
pointerType: PointerType,
39-
isEmulatingMouseEvents: boolean,
4039
};
4140

4241
type FocusProps = {
@@ -70,28 +69,116 @@ type FocusWithinEventType =
7069
*/
7170

7271
let isGlobalFocusVisible = true;
72+
let hasTrackedGlobalFocusVisible = false;
73+
let globalFocusVisiblePointerType = '';
74+
let isEmulatingMouseEvents = false;
7375

7476
const isMac =
7577
typeof window !== 'undefined' && window.navigator != null
7678
? /^Mac/.test(window.navigator.platform)
7779
: false;
7880

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+
}
80106

81107
const hasPointerEvents =
82108
typeof window !== 'undefined' && window.PointerEvent != null;
83109

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+
}
95182

96183
function isFunction(obj): boolean {
97184
return typeof obj === 'function';
@@ -121,7 +208,7 @@ function createFocusEvent(
121208
};
122209
}
123210

124-
function handleRootPointerEvent(
211+
function handleFocusVisibleTargetEvent(
125212
event: ReactDOMResponderEvent,
126213
context: ReactDOMResponderContext,
127214
state: FocusState,
@@ -135,29 +222,26 @@ function handleRootPointerEvent(
135222
const focusTarget = state.focusTarget;
136223
if (
137224
focusTarget !== null &&
138-
context.isTargetWithinResponderScope(focusTarget) &&
139225
(type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
140226
) {
141227
callback(false);
142228
}
143229
}
144230

145-
function handleRootEvent(
231+
function handleFocusVisibleTargetEvents(
146232
event: ReactDOMResponderEvent,
147233
context: ReactDOMResponderContext,
148234
state: FocusState,
149235
callback: boolean => void,
150236
): void {
151237
const {type} = event;
238+
state.pointerType = globalFocusVisiblePointerType;
152239

153240
switch (type) {
154241
case 'pointermove':
155242
case 'pointerdown':
156243
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);
161245
break;
162246
}
163247

@@ -169,12 +253,7 @@ function handleRootEvent(
169253
const validKey = !(metaKey || (!isMac && altKey) || ctrlKey);
170254

171255
if (validKey) {
172-
state.pointerType = 'keyboard';
173-
isGlobalFocusVisible = true;
174-
if (
175-
focusTarget !== null &&
176-
context.isTargetWithinResponderScope(focusTarget)
177-
) {
256+
if (focusTarget !== null) {
178257
callback(true);
179258
}
180259
}
@@ -185,17 +264,12 @@ function handleRootEvent(
185264
case 'touchmove':
186265
case 'touchstart':
187266
case 'touchend': {
188-
state.pointerType = 'touch';
189-
state.isEmulatingMouseEvents = true;
190-
handleRootPointerEvent(event, context, state, callback);
267+
handleFocusVisibleTargetEvent(event, context, state, callback);
191268
break;
192269
}
193270
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);
199273
}
200274
break;
201275
}
@@ -332,17 +406,18 @@ function unmountFocusResponder(
332406
const focusResponderImpl = {
333407
targetEventTypes,
334408
targetPortalPropagation: true,
335-
rootEventTypes,
336409
getInitialState(): FocusState {
337410
return {
338411
detachedTarget: null,
339412
focusTarget: null,
340-
isEmulatingMouseEvents: false,
341413
isFocused: false,
342414
isFocusVisible: false,
343415
pointerType: '',
344416
};
345417
},
418+
onMount() {
419+
trackGlobalFocusVisible();
420+
},
346421
onEvent(
347422
event: ReactDOMResponderEvent,
348423
context: ReactDOMResponderContext,
@@ -370,7 +445,7 @@ const focusResponderImpl = {
370445
state.isFocusVisible = isGlobalFocusVisible;
371446
dispatchFocusEvents(context, props, state);
372447
}
373-
state.isEmulatingMouseEvents = false;
448+
isEmulatingMouseEvents = false;
374449
break;
375450
}
376451
case 'blur': {
@@ -389,24 +464,23 @@ const focusResponderImpl = {
389464
if (event.nativeEvent.relatedTarget == null) {
390465
state.pointerType = '';
391466
}
392-
state.isEmulatingMouseEvents = false;
467+
isEmulatingMouseEvents = false;
393468
break;
394469
}
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+
);
395482
}
396483
},
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-
},
410484
onUnmount(
411485
context: ReactDOMResponderContext,
412486
props: FocusProps,
@@ -471,17 +545,18 @@ function unmountFocusWithinResponder(
471545
const focusWithinResponderImpl = {
472546
targetEventTypes,
473547
targetPortalPropagation: true,
474-
rootEventTypes,
475548
getInitialState(): FocusState {
476549
return {
477550
detachedTarget: null,
478551
focusTarget: null,
479-
isEmulatingMouseEvents: false,
480552
isFocused: false,
481553
isFocusVisible: false,
482554
pointerType: '',
483555
};
484556
},
557+
onMount() {
558+
trackGlobalFocusVisible();
559+
},
485560
onEvent(
486561
event: ReactDOMResponderEvent,
487562
context: ReactDOMResponderContext,
@@ -544,12 +619,31 @@ const focusWithinResponderImpl = {
544619
onBeforeBlurWithin,
545620
DiscreteEvent,
546621
);
622+
context.addRootEventTypes(rootEventTypes);
547623
} else {
548624
// We want to propagate to next focusWithin responder
549625
// if this responder doesn't handle beforeblur
550626
context.continuePropagation();
551627
}
628+
break;
552629
}
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+
);
553647
}
554648
},
555649
onRootEvent(
@@ -563,20 +657,9 @@ const focusWithinResponderImpl = {
563657
if (detachedTarget !== null && detachedTarget === event.target) {
564658
dispatchBlurWithinEvents(context, event, props, state);
565659
state.detachedTarget = null;
660+
context.removeRootEventTypes(rootEventTypes);
566661
}
567-
return;
568662
}
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-
});
580663
},
581664
onUnmount(
582665
context: ReactDOMResponderContext,

0 commit comments

Comments
 (0)