ReactDOM.useEvent: add useEvent interaction hook (#18604)
Dominic Gannaway committed
Apr 14, 2020 at 22:06 UTC
d53988a9d108b9480ff161a6e85ca26760ada615
15 files changed
+1256
-17
packages/react-debug-tools/src/ReactDebugHooks.js
+1
-1
@@ -52,7 +52,7 @@ type ReactDebugListenerMap = {|
52
clear: () => void,
53
setListener: (
54
target: EventTarget | ReactScopeMethods,
55
- callback: ?(Event) => void,
55
+ callback: ?(SyntheticEvent<EventTarget>) => void,
56
) => void,
57
|};
58
packages/react-dom/src/client/ReactDOMHostConfig.js
+1
-1
@@ -1216,7 +1216,7 @@ export function unmountEventListener(listener: ReactDOMListener): void {
1216
1217
export function validateEventListenerTarget(
1218
target: EventTarget | ReactScopeMethods,
1219
- listener: ?(Event) => void,
1219
+ listener: ?(SyntheticEvent<EventTarget>) => void,
1220
): boolean {
1221
if (enableUseEventAPI) {
1222
if (
packages/react-dom/src/client/ReactDOMUseEvent.js
+4
-3
@@ -42,9 +42,10 @@ function resolveDispatcher() {
42
}
43
44
export function useEvent(
45
- type: DOMTopLevelEventType,
45
+ type: string,
46
options?: EventOptions,
47
): ReactDOMListenerMap {
48
+ const topLevelType = ((type: any): DOMTopLevelEventType);
49
const dispatcher = resolveDispatcher();
50
let capture = false;
51
let passive = undefined; // Undefined means to use the browser default
@@ -66,13 +67,13 @@ export function useEvent(
67
}
68
}
69
if (priority === undefined) {
69
- priority = getEventPriorityForListenerSystem(type);
70
+ priority = getEventPriorityForListenerSystem(topLevelType);
71
}
72
const event: ReactDOMListenerEvent = {
73
capture,
74
passive,
75
priority,
75
- type,
76
+ type: topLevelType,
77
};
78
return dispatcher.useEvent(event);
79
}
packages/react-dom/src/shared/ReactDOMTypes.js
+2
-2
@@ -89,12 +89,12 @@ export type ReactDOMListenerMap = {|
89
clear: () => void,
90
setListener: (
91
target: EventTarget | ReactScopeMethods,
92
- callback: ?(Event) => void,
92
+ callback: ?(SyntheticEvent<EventTarget>) => void,
93
) => void,
94
|};
95
96
export type ReactDOMListener = {|
97
- callback: Event => void,
97
+ callback: (SyntheticEvent<EventTarget>) => void,
98
destroy: Node => void,
99
event: ReactDOMListenerEvent,
100
target: EventTarget | ReactScopeMethods,
packages/react-interactions/events/deprecated-focus.js
new
+10
@@ -0,0 +1,10 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export * from './src/dom/DeprecatedFocus';
packages/react-interactions/events/focus.js
+1
-1
@@ -7,4 +7,4 @@
7
* @flow
8
*/
9
10
-export * from './src/dom/Focus';
10
+export * from './src/dom/use-event/Focus';
packages/react-interactions/events/src/dom/DeprecatedFocus.js
renamed
packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js
+3
-2
@@ -32,8 +32,9 @@ function initializeModules(hasPointerEvents) {
32
// TODO: This import throws outside of experimental mode. Figure out better
33
// strategy for gated imports.
34
if (__EXPERIMENTAL__) {
35
- FocusResponder = require('react-interactions/events/focus').FocusResponder;
36
- useFocus = require('react-interactions/events/focus').useFocus;
35
+ FocusResponder = require('react-interactions/events/deprecated-focus')
36
+ .FocusResponder;
37
+ useFocus = require('react-interactions/events/deprecated-focus').useFocus;
38
}
39
}
40
packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
+3
-2
@@ -31,9 +31,10 @@ const initializeModules = hasPointerEvents => {
31
// TODO: This import throws outside of experimental mode. Figure out better
32
// strategy for gated imports.
33
if (__EXPERIMENTAL__) {
34
- FocusWithinResponder = require('react-interactions/events/focus')
34
+ FocusWithinResponder = require('react-interactions/events/deprecated-focus')
35
.FocusWithinResponder;
36
- useFocusWithin = require('react-interactions/events/focus').useFocusWithin;
36
+ useFocusWithin = require('react-interactions/events/deprecated-focus')
37
+ .useFocusWithin;
38
}
39
};
40
packages/react-interactions/events/src/dom/use-event/Focus.js
new
+440
@@ -0,0 +1,440 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import * as React from 'react';
11
+import * as ReactDOM from 'react-dom';
12
+
13
+const useEvent = ReactDOM.unstable_useEvent;
14
+const {useEffect, useRef} = React;
15
+
16
+type UseFocusOptions = {|
17
+ disabled?: boolean,
18
+ onBlur?: (SyntheticEvent<EventTarget>) => void,
19
+ onFocus?: (SyntheticEvent<EventTarget>) => void,
20
+ onFocusChange?: boolean => void,
21
+ onFocusVisibleChange?: boolean => void,
22
+|};
23
+
24
+type UseFocusWithinOptions = {|
25
+ disabled?: boolean,
26
+ onAfterBlurWithin?: (SyntheticEvent<EventTarget>) => void,
27
+ onBeforeBlurWithin?: (SyntheticEvent<EventTarget>) => void,
28
+ onBlurWithin?: (SyntheticEvent<EventTarget>) => void,
29
+ onFocusWithin?: (SyntheticEvent<EventTarget>) => void,
30
+ onFocusWithinChange?: boolean => void,
31
+ onFocusWithinVisibleChange?: boolean => void,
32
+|};
33
+
34
+const isMac =
35
+ typeof window !== 'undefined' && window.navigator != null
36
+ ? /^Mac/.test(window.navigator.platform)
37
+ : false;
38
+
39
+const canUseDOM: boolean = !!(
40
+ typeof window !== 'undefined' &&
41
+ typeof window.document !== 'undefined' &&
42
+ typeof window.document.createElement !== 'undefined'
43
+);
44
+
45
+let passiveBrowserEventsSupported = false;
46
+
47
+// Check if browser support events with passive listeners
48
+// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support
49
+if (canUseDOM) {
50
+ try {
51
+ const options = {};
52
+ // $FlowFixMe: Ignore Flow complaining about needing a value
53
+ Object.defineProperty(options, 'passive', {
54
+ get: function() {
55
+ passiveBrowserEventsSupported = true;
56
+ },
57
+ });
58
+ window.addEventListener('test', options, options);
59
+ window.removeEventListener('test', options, options);
60
+ } catch (e) {
61
+ passiveBrowserEventsSupported = false;
62
+ }
63
+}
64
+
65
+const hasPointerEvents =
66
+ typeof window !== 'undefined' && window.PointerEvent != null;
67
+
68
+const globalFocusVisibleEvents = hasPointerEvents
69
+ ? ['keydown', 'pointermove', 'pointerdown', 'pointerup']
70
+ : ['keydown', 'mousedown', 'touchmove', 'touchstart', 'touchend'];
71
+
72
+// Global state for tracking focus visible and emulation of mouse
73
+let isGlobalFocusVisible = true;
74
+let hasTrackedGlobalFocusVisible = false;
75
+let isEmulatingMouseEvents = false;
76
+
77
+function trackGlobalFocusVisible() {
78
+ globalFocusVisibleEvents.forEach(type => {
79
+ window.addEventListener(
80
+ type,
81
+ handleGlobalFocusVisibleEvent,
82
+ passiveBrowserEventsSupported ? {capture: true, passive: true} : true,
83
+ );
84
+ });
85
+}
86
+
87
+function handleGlobalFocusVisibleEvent(
88
+ nativeEvent: MouseEvent | TouchEvent | KeyboardEvent,
89
+): void {
90
+ const {type} = nativeEvent;
91
+
92
+ switch (type) {
93
+ case 'pointermove':
94
+ case 'pointerdown':
95
+ case 'pointerup': {
96
+ isGlobalFocusVisible = false;
97
+ break;
98
+ }
99
+
100
+ case 'keydown': {
101
+ const {metaKey, altKey, ctrlKey} = nativeEvent;
102
+ const validKey = !(metaKey || (!isMac && altKey) || ctrlKey);
103
+
104
+ if (validKey) {
105
+ isGlobalFocusVisible = true;
106
+ }
107
+ break;
108
+ }
109
+
110
+ // fallbacks for no PointerEvent support
111
+ case 'touchmove':
112
+ case 'touchstart':
113
+ case 'touchend': {
114
+ isEmulatingMouseEvents = true;
115
+ isGlobalFocusVisible = false;
116
+ break;
117
+ }
118
+ case 'mousedown': {
119
+ if (!isEmulatingMouseEvents) {
120
+ isGlobalFocusVisible = false;
121
+ } else {
122
+ isEmulatingMouseEvents = false;
123
+ }
124
+ break;
125
+ }
126
+ }
127
+}
128
+
129
+const passiveObject = {passive: true};
130
+
131
+function handleFocusVisibleTargetEvent(
132
+ type: string,
133
+ focusTarget: EventTarget,
134
+ callback: boolean => void,
135
+): void {
136
+ isGlobalFocusVisible = false;
137
+
138
+ // Focus should stop being visible if a pointer is used on the element
139
+ // after it was focused using a keyboard.
140
+ if (
141
+ focusTarget !== null &&
142
+ (type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
143
+ ) {
144
+ callback(false);
145
+ }
146
+}
147
+
148
+function handleFocusVisibleTargetEvents(
149
+ event: SyntheticEvent<EventTarget>,
150
+ focusTarget,
151
+ callback,
152
+): void {
153
+ const {type} = event;
154
+
155
+ switch (type) {
156
+ case 'pointermove':
157
+ case 'pointerdown':
158
+ case 'pointerup': {
159
+ handleFocusVisibleTargetEvent(type, focusTarget, callback);
160
+ break;
161
+ }
162
+
163
+ case 'keydown':
164
+ case 'keyup': {
165
+ const {metaKey, altKey, ctrlKey} = (event: any);
166
+ const validKey = !(metaKey || (!isMac && altKey) || ctrlKey);
167
+
168
+ if (validKey) {
169
+ if (focusTarget !== null) {
170
+ callback(true);
171
+ }
172
+ }
173
+ break;
174
+ }
175
+
176
+ // fallbacks for no PointerEvent support
177
+ case 'touchmove':
178
+ case 'touchstart':
179
+ case 'touchend': {
180
+ handleFocusVisibleTargetEvent(type, focusTarget, callback);
181
+ break;
182
+ }
183
+ case 'mousedown': {
184
+ if (!isEmulatingMouseEvents) {
185
+ handleFocusVisibleTargetEvent(type, focusTarget, callback);
186
+ }
187
+ break;
188
+ }
189
+ }
190
+}
191
+
192
+function isRelatedTargetWithin(
193
+ focusWithinTarget: Node,
194
+ relatedTarget: null | Node,
195
+): boolean {
196
+ if (relatedTarget == null) {
197
+ return false;
198
+ }
199
+ // To support experimental scopes, which can be the target:
200
+ const containsNode = (focusWithinTarget: any).containsNode;
201
+ if (typeof containsNode === 'function') {
202
+ return containsNode(relatedTarget);
203
+ }
204
+ return focusWithinTarget.contains(relatedTarget);
205
+}
206
+
207
+function setFocusVisibleListeners(focusVisibleHandles, focusTarget, callback) {
208
+ focusVisibleHandles.forEach(focusVisibleHandle => {
209
+ focusVisibleHandle.setListener(focusTarget, event =>
210
+ handleFocusVisibleTargetEvents(event, focusTarget, callback),
211
+ );
212
+ });
213
+}
214
+
215
+function useFocusVisibleInputHandles() {
216
+ return [
217
+ useEvent('mousedown', passiveObject),
218
+ useEvent(hasPointerEvents ? 'pointerdown' : 'touchstart', passiveObject),
219
+ useEvent(hasPointerEvents ? 'pointermove' : 'touchmove', passiveObject),
220
+ useEvent(hasPointerEvents ? 'pointerup' : 'touchend', passiveObject),
221
+ useEvent('keydown', passiveObject),
222
+ ];
223
+}
224
+
225
+function useFocusLifecycles(stateRef) {
226
+ useEffect(() => {
227
+ if (!hasTrackedGlobalFocusVisible) {
228
+ hasTrackedGlobalFocusVisible = true;
229
+ trackGlobalFocusVisible();
230
+ }
231
+ }, []);
232
+}
233
+
234
+export function useFocus(
235
+ focusTargetRef: {current: null | Node},
236
+ {
237
+ disabled,
238
+ onBlur,
239
+ onFocus,
240
+ onFocusChange,
241
+ onFocusVisibleChange,
242
+ }: UseFocusOptions,
243
+): void {
244
+ // Setup controlled state for this useFocus hook
245
+ const stateRef = useRef({isFocused: false, isFocusVisible: false});
246
+ const focusHandle = useEvent('focus', passiveObject);
247
+ const blurHandle = useEvent('blur', passiveObject);
248
+ const focusVisibleHandles = useFocusVisibleInputHandles();
249
+
250
+ useEffect(() => {
251
+ const focusTarget = focusTargetRef.current;
252
+ const state = stateRef.current;
253
+
254
+ if (focusTarget !== null && state !== null) {
255
+ // Handle focus visible
256
+ setFocusVisibleListeners(
257
+ focusVisibleHandles,
258
+ focusTarget,
259
+ isFocusVisible => {
260
+ if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
261
+ state.isFocusVisible = isFocusVisible;
262
+ if (onFocusVisibleChange) {
263
+ onFocusVisibleChange(isFocusVisible);
264
+ }
265
+ }
266
+ },
267
+ );
268
+
269
+ // Handle focus
270
+ focusHandle.setListener(focusTarget, event => {
271
+ if (disabled) {
272
+ return;
273
+ }
274
+ // Limit focus events to the direct child of the event component.
275
+ // Browser focus is not expected to bubble.
276
+ if (!state.isFocused && focusTarget === event.target) {
277
+ state.isFocused = true;
278
+ state.isFocusVisible = isGlobalFocusVisible;
279
+ if (onFocus) {
280
+ onFocus(event);
281
+ }
282
+ if (onFocusChange) {
283
+ onFocusChange(true);
284
+ }
285
+ if (state.isFocusVisible && onFocusVisibleChange) {
286
+ onFocusVisibleChange(true);
287
+ }
288
+ isEmulatingMouseEvents = false;
289
+ }
290
+ });
291
+
292
+ // Handle blur
293
+ blurHandle.setListener(focusTarget, event => {
294
+ if (disabled) {
295
+ return;
296
+ }
297
+ if (state.isFocused) {
298
+ state.isFocused = false;
299
+ state.isFocusVisible = isGlobalFocusVisible;
300
+ if (onBlur) {
301
+ onBlur(event);
302
+ }
303
+ if (onFocusChange) {
304
+ onFocusChange(false);
305
+ }
306
+ if (state.isFocusVisible && onFocusVisibleChange) {
307
+ onFocusVisibleChange(false);
308
+ }
309
+ }
310
+ isEmulatingMouseEvents = false;
311
+ });
312
+ }
313
+ }, [disabled, onBlur, onFocus, onFocusChange, onFocusVisibleChange]);
314
+
315
+ // Mount/Unmount logic
316
+ useFocusLifecycles(stateRef);
317
+}
318
+
319
+export function useFocusWithin(
320
+ focusWithinTargetRef: {current: null | Node},
321
+ {
322
+ disabled,
323
+ onAfterBlurWithin,
324
+ onBeforeBlurWithin,
325
+ onBlurWithin,
326
+ onFocusWithin,
327
+ onFocusWithinChange,
328
+ onFocusWithinVisibleChange,
329
+ }: UseFocusWithinOptions,
330
+) {
331
+ // Setup controlled state for this useFocus hook
332
+ const stateRef = useRef({isFocused: false, isFocusVisible: false});
333
+ const focusHandle = useEvent('focus', passiveObject);
334
+ const blurHandle = useEvent('blur', passiveObject);
335
+ const afterBlurHandle = useEvent('afterblur', passiveObject);
336
+ const beforeBlurHandle = useEvent('beforeblur', passiveObject);
337
+ const focusVisibleHandles = useFocusVisibleInputHandles();
338
+
339
+ useEffect(() => {
340
+ const focusWithinTarget = focusWithinTargetRef.current;
341
+ const state = stateRef.current;
342
+
343
+ if (focusWithinTarget !== null && state !== null) {
344
+ // Handle focus visible
345
+ setFocusVisibleListeners(
346
+ focusVisibleHandles,
347
+ focusWithinTarget,
348
+ isFocusVisible => {
349
+ if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
350
+ state.isFocusVisible = isFocusVisible;
351
+ if (onFocusWithinVisibleChange) {
352
+ onFocusWithinVisibleChange(isFocusVisible);
353
+ }
354
+ }
355
+ },
356
+ );
357
+
358
+ // Handle focus
359
+ focusHandle.setListener(focusWithinTarget, event => {
360
+ if (disabled) {
361
+ return;
362
+ }
363
+ if (!state.isFocused) {
364
+ state.isFocused = true;
365
+ state.isFocusVisible = isGlobalFocusVisible;
366
+ if (onFocusWithinChange) {
367
+ onFocusWithinChange(true);
368
+ }
369
+ if (state.isFocusVisible && onFocusWithinVisibleChange) {
370
+ onFocusWithinVisibleChange(true);
371
+ }
372
+ }
373
+ if (!state.isFocusVisible && isGlobalFocusVisible) {
374
+ state.isFocusVisible = isGlobalFocusVisible;
375
+ if (onFocusWithinVisibleChange) {
376
+ onFocusWithinVisibleChange(true);
377
+ }
378
+ }
379
+ if (onFocusWithin) {
380
+ onFocusWithin(event);
381
+ }
382
+ isEmulatingMouseEvents = false;
383
+ });
384
+
385
+ // Handle blur
386
+ blurHandle.setListener(focusWithinTarget, event => {
387
+ if (disabled) {
388
+ return;
389
+ }
390
+ const {relatedTarget} = (event: any);
391
+
392
+ if (
393
+ state.isFocused &&
394
+ !isRelatedTargetWithin(focusWithinTarget, relatedTarget)
395
+ ) {
396
+ state.isFocused = false;
397
+ if (onFocusWithinChange) {
398
+ onFocusWithinChange(false);
399
+ }
400
+ if (state.isFocusVisible && onFocusWithinVisibleChange) {
401
+ onFocusWithinVisibleChange(false);
402
+ }
403
+ if (onBlurWithin) {
404
+ onBlurWithin(event);
405
+ }
406
+ }
407
+ isEmulatingMouseEvents = false;
408
+ });
409
+
410
+ // Handle before blur. This is a special
411
+ // React provided event.
412
+ beforeBlurHandle.setListener(focusWithinTarget, event => {
413
+ if (disabled) {
414
+ return;
415
+ }
416
+ if (onBeforeBlurWithin) {
417
+ onBeforeBlurWithin(event);
418
+ // Add an "afterblur" listener on document. This is a special
419
+ // React provided event.
420
+ afterBlurHandle.setListener(document, afterBlurEvent => {
421
+ if (onAfterBlurWithin) {
422
+ onAfterBlurWithin(afterBlurEvent);
423
+ }
424
+ // Clear listener on document
425
+ afterBlurHandle.setListener(document, null);
426
+ });
427
+ }
428
+ });
429
+ }
430
+ }, [
431
+ disabled,
432
+ onBlurWithin,
433
+ onFocusWithin,
434
+ onFocusWithinChange,
435
+ onFocusWithinVisibleChange,
436
+ ]);
437
+
438
+ // Mount/Unmount logic
439
+ useFocusLifecycles(stateRef);
440
+}
packages/react-interactions/events/src/dom/use-event/__tests__/useFocus-test.internal.js
new
+319
@@ -0,0 +1,319 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+import {createEventTarget, setPointerEvent} from 'dom-event-testing-library';
13
+
14
+let React;
15
+let ReactFeatureFlags;
16
+let ReactDOM;
17
+let useFocus;
18
+
19
+function initializeModules(hasPointerEvents) {
20
+ setPointerEvent(hasPointerEvents);
21
+ jest.resetModules();
22
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
23
+ ReactFeatureFlags.enableModernEventSystem = true;
24
+ ReactFeatureFlags.enableUseEventAPI = true;
25
+ React = require('react');
26
+ ReactDOM = require('react-dom');
27
+
28
+ // TODO: This import throws outside of experimental mode. Figure out better
29
+ // strategy for gated imports.
30
+ if (__EXPERIMENTAL__) {
31
+ useFocus = require('react-interactions/events/focus').useFocus;
32
+ }
33
+}
34
+
35
+const forcePointerEvents = true;
36
+const table = [[forcePointerEvents], [!forcePointerEvents]];
37
+
38
+describe.each(table)(`useFocus`, hasPointerEvents => {
39
+ let container;
40
+
41
+ beforeEach(() => {
42
+ initializeModules(hasPointerEvents);
43
+ container = document.createElement('div');
44
+ document.body.appendChild(container);
45
+ });
46
+
47
+ afterEach(() => {
48
+ ReactDOM.render(null, container);
49
+ document.body.removeChild(container);
50
+ container = null;
51
+ });
52
+
53
+ describe('disabled', () => {
54
+ let onBlur, onFocus, ref;
55
+
56
+ const componentInit = () => {
57
+ onBlur = jest.fn();
58
+ onFocus = jest.fn();
59
+ ref = React.createRef();
60
+ const Component = () => {
61
+ useFocus(ref, {
62
+ disabled: true,
63
+ onBlur,
64
+ onFocus,
65
+ });
66
+ return <div ref={ref} />;
67
+ };
68
+ ReactDOM.render(<Component />, container);
69
+ };
70
+
71
+ // @gate experimental
72
+ it('does not call callbacks', () => {
73
+ componentInit();
74
+ const target = createEventTarget(ref.current);
75
+ target.focus();
76
+ target.blur();
77
+ expect(onFocus).not.toBeCalled();
78
+ expect(onBlur).not.toBeCalled();
79
+ });
80
+ });
81
+
82
+ describe('onBlur', () => {
83
+ let onBlur, ref;
84
+
85
+ const componentInit = () => {
86
+ onBlur = jest.fn();
87
+ ref = React.createRef();
88
+ const Component = () => {
89
+ useFocus(ref, {
90
+ onBlur,
91
+ });
92
+ return <div ref={ref} />;
93
+ };
94
+ ReactDOM.render(<Component />, container);
95
+ };
96
+
97
+ // @gate experimental
98
+ it('is called after "blur" event', () => {
99
+ componentInit();
100
+ const target = createEventTarget(ref.current);
101
+ target.focus();
102
+ target.blur();
103
+ expect(onBlur).toHaveBeenCalledTimes(1);
104
+ });
105
+ });
106
+
107
+ describe('onFocus', () => {
108
+ let onFocus, ref, innerRef;
109
+
110
+ const componentInit = () => {
111
+ onFocus = jest.fn();
112
+ ref = React.createRef();
113
+ innerRef = React.createRef();
114
+ const Component = () => {
115
+ useFocus(ref, {
116
+ onFocus,
117
+ });
118
+ return (
119
+ <div ref={ref}>
120
+ <a ref={innerRef} />
121
+ </div>
122
+ );
123
+ };
124
+ ReactDOM.render(<Component />, container);
125
+ };
126
+
127
+ // @gate experimental
128
+ it('is called after "focus" event', () => {
129
+ componentInit();
130
+ const target = createEventTarget(ref.current);
131
+ target.focus();
132
+ expect(onFocus).toHaveBeenCalledTimes(1);
133
+ });
134
+
135
+ // @gate experimental
136
+ it('is not called if descendants of target receive focus', () => {
137
+ componentInit();
138
+ const target = createEventTarget(innerRef.current);
139
+ target.focus();
140
+ expect(onFocus).not.toBeCalled();
141
+ });
142
+ });
143
+
144
+ describe('onFocusChange', () => {
145
+ let onFocusChange, ref, innerRef;
146
+
147
+ const componentInit = () => {
148
+ onFocusChange = jest.fn();
149
+ ref = React.createRef();
150
+ innerRef = React.createRef();
151
+ const Component = () => {
152
+ useFocus(ref, {
153
+ onFocusChange,
154
+ });
155
+ return (
156
+ <div ref={ref}>
157
+ <div ref={innerRef} />
158
+ </div>
159
+ );
160
+ };
161
+ ReactDOM.render(<Component />, container);
162
+ };
163
+
164
+ // @gate experimental
165
+ it('is called after "blur" and "focus" events', () => {
166
+ componentInit();
167
+ const target = createEventTarget(ref.current);
168
+ target.focus();
169
+ expect(onFocusChange).toHaveBeenCalledTimes(1);
170
+ expect(onFocusChange).toHaveBeenCalledWith(true);
171
+ target.blur();
172
+ expect(onFocusChange).toHaveBeenCalledTimes(2);
173
+ expect(onFocusChange).toHaveBeenCalledWith(false);
174
+ });
175
+
176
+ // @gate experimental
177
+ it('is not called after "blur" and "focus" events on descendants', () => {
178
+ componentInit();
179
+ const target = createEventTarget(innerRef.current);
180
+ target.focus();
181
+ expect(onFocusChange).toHaveBeenCalledTimes(0);
182
+ target.blur();
183
+ expect(onFocusChange).toHaveBeenCalledTimes(0);
184
+ });
185
+ });
186
+
187
+ describe('onFocusVisibleChange', () => {
188
+ let onFocusVisibleChange, ref, innerRef;
189
+
190
+ const componentInit = () => {
191
+ onFocusVisibleChange = jest.fn();
192
+ ref = React.createRef();
193
+ innerRef = React.createRef();
194
+ const Component = () => {
195
+ useFocus(ref, {
196
+ onFocusVisibleChange,
197
+ });
198
+ return (
199
+ <div ref={ref}>
200
+ <div ref={innerRef} />
201
+ </div>
202
+ );
203
+ };
204
+ ReactDOM.render(<Component />, container);
205
+ };
206
+
207
+ // @gate experimental
208
+ it('is called after "focus" and "blur" if keyboard navigation is active', () => {
209
+ componentInit();
210
+ const target = createEventTarget(ref.current);
211
+ const containerTarget = createEventTarget(container);
212
+ // use keyboard first
213
+ containerTarget.keydown({key: 'Tab'});
214
+ target.focus();
215
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
216
+ expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
217
+ target.blur({relatedTarget: container});
218
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
219
+ expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
220
+ });
221
+
222
+ // @gate experimental
223
+ it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
224
+ componentInit();
225
+ const target = createEventTarget(ref.current);
226
+ const containerTarget = createEventTarget(container);
227
+ // use keyboard first
228
+ containerTarget.keydown({key: 'Tab'});
229
+ target.focus();
230
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
231
+ expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
232
+ // then use pointer on the target, focus should no longer be visible
233
+ target.pointerdown();
234
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
235
+ expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
236
+ // onFocusVisibleChange should not be called again
237
+ target.blur({relatedTarget: container});
238
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
239
+ });
240
+
241
+ // @gate experimental
242
+ it('is not called after "focus" and "blur" events without keyboard', () => {
243
+ componentInit();
244
+ const target = createEventTarget(ref.current);
245
+ const containerTarget = createEventTarget(container);
246
+ target.pointerdown();
247
+ target.pointerup();
248
+ containerTarget.pointerdown();
249
+ target.blur({relatedTarget: container});
250
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
251
+ });
252
+
253
+ // @gate experimental
254
+ it('is not called after "blur" and "focus" events on descendants', () => {
255
+ componentInit();
256
+ const innerTarget = createEventTarget(innerRef.current);
257
+ const containerTarget = createEventTarget(container);
258
+ containerTarget.keydown({key: 'Tab'});
259
+ innerTarget.focus();
260
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
261
+ innerTarget.blur({relatedTarget: container});
262
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
263
+ });
264
+ });
265
+
266
+ describe('nested Focus components', () => {
267
+ // @gate experimental
268
+ it('propagates events in the correct order', () => {
269
+ const events = [];
270
+ const innerRef = React.createRef();
271
+ const outerRef = React.createRef();
272
+ const createEventHandler = msg => () => {
273
+ events.push(msg);
274
+ };
275
+
276
+ const Inner = () => {
277
+ useFocus(innerRef, {
278
+ onBlur: createEventHandler('inner: onBlur'),
279
+ onFocus: createEventHandler('inner: onFocus'),
280
+ onFocusChange: createEventHandler('inner: onFocusChange'),
281
+ });
282
+ return <div ref={innerRef} />;
283
+ };
284
+
285
+ const Outer = () => {
286
+ useFocus(outerRef, {
287
+ onBlur: createEventHandler('outer: onBlur'),
288
+ onFocus: createEventHandler('outer: onFocus'),
289
+ onFocusChange: createEventHandler('outer: onFocusChange'),
290
+ });
291
+ return (
292
+ <div ref={outerRef}>
293
+ <Inner />
294
+ </div>
295
+ );
296
+ };
297
+
298
+ ReactDOM.render(<Outer />, container);
299
+
300
+ const innerTarget = createEventTarget(innerRef.current);
301
+ const outerTarget = createEventTarget(outerRef.current);
302
+
303
+ outerTarget.focus();
304
+ outerTarget.blur();
305
+ innerTarget.focus();
306
+ innerTarget.blur();
307
+ expect(events).toEqual([
308
+ 'outer: onFocus',
309
+ 'outer: onFocusChange',
310
+ 'outer: onBlur',
311
+ 'outer: onFocusChange',
312
+ 'inner: onFocus',
313
+ 'inner: onFocusChange',
314
+ 'inner: onBlur',
315
+ 'inner: onFocusChange',
316
+ ]);
317
+ });
318
+ });
319
+});
packages/react-interactions/events/src/dom/use-event/__tests__/useFocusWithin-test.internal.js
new
+467
@@ -0,0 +1,467 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+import {createEventTarget, setPointerEvent} from 'dom-event-testing-library';
13
+
14
+let React;
15
+let ReactFeatureFlags;
16
+let ReactDOM;
17
+let useFocusWithin;
18
+let ReactTestRenderer;
19
+let act;
20
+
21
+function initializeModules(hasPointerEvents) {
22
+ setPointerEvent(hasPointerEvents);
23
+ jest.resetModules();
24
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
25
+ ReactFeatureFlags.enableScopeAPI = true;
26
+ ReactFeatureFlags.enableModernEventSystem = true;
27
+ ReactFeatureFlags.enableUseEventAPI = true;
28
+ React = require('react');
29
+ ReactDOM = require('react-dom');
30
+ ReactTestRenderer = require('react-test-renderer');
31
+ act = ReactTestRenderer.act;
32
+
33
+ // TODO: This import throws outside of experimental mode. Figure out better
34
+ // strategy for gated imports.
35
+ if (__EXPERIMENTAL__) {
36
+ useFocusWithin = require('react-interactions/events/focus').useFocusWithin;
37
+ }
38
+}
39
+
40
+const forcePointerEvents = true;
41
+const table = [[forcePointerEvents], [!forcePointerEvents]];
42
+
43
+describe.each(table)(`useFocus`, hasPointerEvents => {
44
+ let container;
45
+
46
+ beforeEach(() => {
47
+ initializeModules(hasPointerEvents);
48
+ container = document.createElement('div');
49
+ document.body.appendChild(container);
50
+ });
51
+
52
+ afterEach(() => {
53
+ ReactDOM.render(null, container);
54
+ document.body.removeChild(container);
55
+ container = null;
56
+ });
57
+
58
+ describe('disabled', () => {
59
+ let onFocusWithinChange, onFocusWithinVisibleChange, ref;
60
+
61
+ const componentInit = () => {
62
+ onFocusWithinChange = jest.fn();
63
+ onFocusWithinVisibleChange = jest.fn();
64
+ ref = React.createRef();
65
+ const Component = () => {
66
+ useFocusWithin(ref, {
67
+ disabled: true,
68
+ onFocusWithinChange,
69
+ onFocusWithinVisibleChange,
70
+ });
71
+ return <div ref={ref} />;
72
+ };
73
+ ReactDOM.render(<Component />, container);
74
+ };
75
+
76
+ // @gate experimental
77
+ it('prevents custom events being dispatched', () => {
78
+ componentInit();
79
+ const target = createEventTarget(ref.current);
80
+ target.focus();
81
+ target.blur();
82
+ expect(onFocusWithinChange).not.toBeCalled();
83
+ expect(onFocusWithinVisibleChange).not.toBeCalled();
84
+ });
85
+ });
86
+
87
+ describe('onFocusWithinChange', () => {
88
+ let onFocusWithinChange, ref, innerRef, innerRef2;
89
+
90
+ const Component = ({show}) => {
91
+ useFocusWithin(ref, {
92
+ onFocusWithinChange,
93
+ });
94
+ return (
95
+ <div ref={ref}>
96
+ {show && <input ref={innerRef} />}
97
+ <div ref={innerRef2} />
98
+ </div>
99
+ );
100
+ };
101
+
102
+ const componentInit = () => {
103
+ onFocusWithinChange = jest.fn();
104
+ ref = React.createRef();
105
+ innerRef = React.createRef();
106
+ innerRef2 = React.createRef();
107
+ ReactDOM.render(<Component show={true} />, container);
108
+ };
109
+
110
+ // @gate experimental
111
+ it('is called after "blur" and "focus" events on focus target', () => {
112
+ componentInit();
113
+ const target = createEventTarget(ref.current);
114
+ target.focus();
115
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
116
+ expect(onFocusWithinChange).toHaveBeenCalledWith(true);
117
+ target.blur({relatedTarget: container});
118
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
119
+ expect(onFocusWithinChange).toHaveBeenCalledWith(false);
120
+ });
121
+
122
+ // @gate experimental
123
+ it('is called after "blur" and "focus" events on descendants', () => {
124
+ componentInit();
125
+ const target = createEventTarget(innerRef.current);
126
+ target.focus();
127
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
128
+ expect(onFocusWithinChange).toHaveBeenCalledWith(true);
129
+ target.blur({relatedTarget: container});
130
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
131
+ expect(onFocusWithinChange).toHaveBeenCalledWith(false);
132
+ });
133
+
134
+ // @gate experimental
135
+ it('is only called once when focus moves within and outside the subtree', () => {
136
+ componentInit();
137
+ const node = ref.current;
138
+ const innerNode1 = innerRef.current;
139
+ const innerNode2 = innerRef.current;
140
+ const target = createEventTarget(node);
141
+ const innerTarget1 = createEventTarget(innerNode1);
142
+ const innerTarget2 = createEventTarget(innerNode2);
143
+
144
+ // focus shifts into subtree
145
+ innerTarget1.focus();
146
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
147
+ expect(onFocusWithinChange).toHaveBeenCalledWith(true);
148
+ // focus moves around subtree
149
+ innerTarget1.blur({relatedTarget: innerNode2});
150
+ innerTarget2.focus();
151
+ innerTarget2.blur({relatedTarget: node});
152
+ target.focus();
153
+ target.blur({relatedTarget: innerNode1});
154
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
155
+ // focus shifts outside subtree
156
+ innerTarget1.blur({relatedTarget: container});
157
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
158
+ expect(onFocusWithinChange).toHaveBeenCalledWith(false);
159
+ });
160
+ });
161
+
162
+ describe('onFocusWithinVisibleChange', () => {
163
+ let onFocusWithinVisibleChange, ref, innerRef, innerRef2;
164
+
165
+ const Component = ({show}) => {
166
+ useFocusWithin(ref, {
167
+ onFocusWithinVisibleChange,
168
+ });
169
+ return (
170
+ <div ref={ref}>
171
+ {show && <input ref={innerRef} />}
172
+ <div ref={innerRef2} />
173
+ </div>
174
+ );
175
+ };
176
+
177
+ const componentInit = () => {
178
+ onFocusWithinVisibleChange = jest.fn();
179
+ ref = React.createRef();
180
+ innerRef = React.createRef();
181
+ innerRef2 = React.createRef();
182
+ ReactDOM.render(<Component show={true} />, container);
183
+ };
184
+
185
+ // @gate experimental
186
+ it('is called after "focus" and "blur" on focus target if keyboard was used', () => {
187
+ componentInit();
188
+ const target = createEventTarget(ref.current);
189
+ const containerTarget = createEventTarget(container);
190
+ // use keyboard first
191
+ containerTarget.keydown({key: 'Tab'});
192
+ target.focus();
193
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
194
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
195
+ target.blur({relatedTarget: container});
196
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
197
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
198
+ });
199
+
200
+ // @gate experimental
201
+ it('is called after "focus" and "blur" on descendants if keyboard was used', () => {
202
+ componentInit();
203
+ const innerTarget = createEventTarget(innerRef.current);
204
+ const containerTarget = createEventTarget(container);
205
+ // use keyboard first
206
+ containerTarget.keydown({key: 'Tab'});
207
+ innerTarget.focus();
208
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
209
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
210
+ innerTarget.blur({relatedTarget: container});
211
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
212
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
213
+ });
214
+
215
+ // @gate experimental
216
+ it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
217
+ componentInit();
218
+ const node = ref.current;
219
+ const innerNode1 = innerRef.current;
220
+ const innerNode2 = innerRef2.current;
221
+
222
+ const target = createEventTarget(node);
223
+ const innerTarget1 = createEventTarget(innerNode1);
224
+ const innerTarget2 = createEventTarget(innerNode2);
225
+ // use keyboard first
226
+ target.focus();
227
+ target.keydown({key: 'Tab'});
228
+ target.blur({relatedTarget: innerNode1});
229
+ innerTarget1.focus();
230
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
231
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
232
+ // then use pointer on the next target, focus should no longer be visible
233
+ innerTarget2.pointerdown();
234
+ innerTarget1.blur({relatedTarget: innerNode2});
235
+ innerTarget2.focus();
236
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
237
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
238
+ // then use keyboard again
239
+ innerTarget2.keydown({key: 'Tab', shiftKey: true});
240
+ innerTarget2.blur({relatedTarget: innerNode1});
241
+ innerTarget1.focus();
242
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(3);
243
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
244
+ // then use pointer on the target, focus should no longer be visible
245
+ innerTarget1.pointerdown();
246
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
247
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
248
+ // onFocusVisibleChange should not be called again
249
+ innerTarget1.blur({relatedTarget: container});
250
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
251
+ });
252
+
253
+ // @gate experimental
254
+ it('is not called after "focus" and "blur" events without keyboard', () => {
255
+ componentInit();
256
+ const innerTarget = createEventTarget(innerRef.current);
257
+ innerTarget.pointerdown();
258
+ innerTarget.pointerup();
259
+ innerTarget.blur({relatedTarget: container});
260
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(0);
261
+ });
262
+
263
+ // @gate experimental
264
+ it('is only called once when focus moves within and outside the subtree', () => {
265
+ componentInit();
266
+ const node = ref.current;
267
+ const innerNode1 = innerRef.current;
268
+ const innerNode2 = innerRef2.current;
269
+ const target = createEventTarget(node);
270
+ const innerTarget1 = createEventTarget(innerNode1);
271
+ const innerTarget2 = createEventTarget(innerNode2);
272
+
273
+ // focus shifts into subtree
274
+ innerTarget1.focus();
275
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
276
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
277
+ // focus moves around subtree
278
+ innerTarget1.blur({relatedTarget: innerNode2});
279
+ innerTarget2.focus();
280
+ innerTarget2.blur({relatedTarget: node});
281
+ target.focus();
282
+ target.blur({relatedTarget: innerNode1});
283
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
284
+ // focus shifts outside subtree
285
+ innerTarget1.blur({relatedTarget: container});
286
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
287
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
288
+ });
289
+ });
290
+
291
+ describe('onBeforeBlurWithin', () => {
292
+ let onBeforeBlurWithin, onAfterBlurWithin, ref, innerRef, innerRef2;
293
+
294
+ beforeEach(() => {
295
+ onBeforeBlurWithin = jest.fn();
296
+ onAfterBlurWithin = jest.fn(e => {
297
+ e.persist();
298
+ });
299
+ ref = React.createRef();
300
+ innerRef = React.createRef();
301
+ innerRef2 = React.createRef();
302
+ });
303
+
304
+ // @gate experimental
305
+ it('is called after a focused element is unmounted', () => {
306
+ const Component = ({show}) => {
307
+ useFocusWithin(ref, {
308
+ onBeforeBlurWithin,
309
+ onAfterBlurWithin,
310
+ });
311
+ return (
312
+ <div ref={ref}>
313
+ {show && <input ref={innerRef} />}
314
+ <div ref={innerRef2} />
315
+ </div>
316
+ );
317
+ };
318
+
319
+ ReactDOM.render(<Component show={true} />, container);
320
+
321
+ const inner = innerRef.current;
322
+ const target = createEventTarget(inner);
323
+ target.keydown({key: 'Tab'});
324
+ target.focus();
325
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
326
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
327
+ ReactDOM.render(<Component show={false} />, container);
328
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
329
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
330
+ expect(onAfterBlurWithin).toHaveBeenCalledWith(
331
+ expect.objectContaining({relatedTarget: inner}),
332
+ );
333
+ });
334
+
335
+ // @gate experimental
336
+ it('is called after a nested focused element is unmounted', () => {
337
+ const Component = ({show}) => {
338
+ useFocusWithin(ref, {
339
+ onBeforeBlurWithin,
340
+ onAfterBlurWithin,
341
+ });
342
+ return (
343
+ <div ref={ref}>
344
+ {show && (
345
+ <div>
346
+ <input ref={innerRef} />
347
+ </div>
348
+ )}
349
+ <div ref={innerRef2} />
350
+ </div>
351
+ );
352
+ };
353
+
354
+ ReactDOM.render(<Component show={true} />, container);
355
+
356
+ const inner = innerRef.current;
357
+ const target = createEventTarget(inner);
358
+ target.keydown({key: 'Tab'});
359
+ target.focus();
360
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
361
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
362
+ ReactDOM.render(<Component show={false} />, container);
363
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
364
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
365
+ expect(onAfterBlurWithin).toHaveBeenCalledWith(
366
+ expect.objectContaining({relatedTarget: inner}),
367
+ );
368
+ });
369
+
370
+ // @gate experimental
371
+ it('is called after a nested focused element is unmounted (with scope query)', () => {
372
+ const TestScope = React.unstable_createScope();
373
+ const testScopeQuery = (type, props) => true;
374
+ let targetNodes;
375
+ let targetNode;
376
+
377
+ const Component = ({show}) => {
378
+ const scopeRef = React.useRef(null);
379
+ useFocusWithin(scopeRef, {
380
+ onBeforeBlurWithin(event) {
381
+ const scope = scopeRef.current;
382
+ targetNode = innerRef.current;
383
+ targetNodes = scope.DO_NOT_USE_queryAllNodes(testScopeQuery);
384
+ },
385
+ });
386
+
387
+ return (
388
+ <TestScope ref={scopeRef}>
389
+ {show && <input ref={innerRef} />}
390
+ </TestScope>
391
+ );
392
+ };
393
+
394
+ ReactDOM.render(<Component show={true} />, container);
395
+
396
+ const inner = innerRef.current;
397
+ const target = createEventTarget(inner);
398
+ target.keydown({key: 'Tab'});
399
+ target.focus();
400
+ ReactDOM.render(<Component show={false} />, container);
401
+ expect(targetNodes).toEqual([targetNode]);
402
+ });
403
+
404
+ // @gate experimental
405
+ it('is called after a focused suspended element is hidden', () => {
406
+ const Suspense = React.Suspense;
407
+ let suspend = false;
408
+ let resolve;
409
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
410
+
411
+ function Child() {
412
+ if (suspend) {
413
+ throw promise;
414
+ } else {
415
+ return <input ref={innerRef} />;
416
+ }
417
+ }
418
+
419
+ const Component = ({show}) => {
420
+ useFocusWithin(ref, {
421
+ onBeforeBlurWithin,
422
+ onAfterBlurWithin,
423
+ });
424
+
425
+ return (
426
+ <div ref={ref}>
427
+ <Suspense fallback="Loading...">
428
+ <Child />
429
+ </Suspense>
430
+ </div>
431
+ );
432
+ };
433
+
434
+ const container2 = document.createElement('div');
435
+ document.body.appendChild(container2);
436
+
437
+ const root = ReactDOM.createRoot(container2);
438
+
439
+ act(() => {
440
+ root.render(<Component />);
441
+ });
442
+ jest.runAllTimers();
443
+ expect(container2.innerHTML).toBe('<div><input></div>');
444
+
445
+ const inner = innerRef.current;
446
+ const target = createEventTarget(inner);
447
+ target.keydown({key: 'Tab'});
448
+ target.focus();
449
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
450
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
451
+
452
+ suspend = true;
453
+ act(() => {
454
+ root.render(<Component />);
455
+ });
456
+ jest.runAllTimers();
457
+ expect(container2.innerHTML).toBe(
458
+ '<div><input style="display: none;">Loading...</div>',
459
+ );
460
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
461
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
462
+ resolve();
463
+
464
+ document.body.removeChild(container2);
465
+ });
466
+ });
467
+});
packages/react-reconciler/src/ReactFiberHooks.new.js
+2
-2
@@ -1757,7 +1757,7 @@ function validateNotInFunctionRender(): boolean {
1757
1758
function createReactListener(
1759
event: ReactListenerEvent,
1760
- callback: Event => void,
1760
+ callback: (SyntheticEvent<EventTarget>) => void,
1761
target: EventTarget | ReactScopeMethods,
1762
destroy: Node => void,
1763
): ReactListener {
@@ -1812,7 +1812,7 @@ function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
1812
clear,
1813
setListener(
1814
target: EventTarget | ReactScopeMethods,
1815
- callback: ?(Event) => void,
1815
+ callback: ?(SyntheticEvent<EventTarget>) => void,
1816
): void {
1817
if (
1818
validateNotInFunctionRender() &&
packages/react-reconciler/src/ReactFiberHooks.old.js
+2
-2
@@ -1752,7 +1752,7 @@ function validateNotInFunctionRender(): boolean {
1752
1753
function createReactListener(
1754
event: ReactListenerEvent,
1755
- callback: Event => void,
1755
+ callback: (SyntheticEvent<EventTarget>) => void,
1756
target: EventTarget | ReactScopeMethods,
1757
destroy: Node => void,
1758
): ReactListener {
@@ -1807,7 +1807,7 @@ function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
1807
clear,
1808
setListener(
1809
target: EventTarget | ReactScopeMethods,
1810
- callback: ?(Event) => void,
1810
+ callback: ?(SyntheticEvent<EventTarget>) => void,
1811
): void {
1812
if (
1813
validateNotInFunctionRender() &&
scripts/rollup/bundles.js
+1
-1
@@ -689,7 +689,7 @@ const bundles = [
689
FB_WWW_PROD,
690
],
691
moduleType: NON_FIBER_RENDERER,
692
- entry: 'react-interactions/events/focus',
692
+ entry: 'react-interactions/events/deprecated-focus',
693
global: 'ReactEventsFocus',
694
externals: ['react'],
695
},