Add experimental ReactDOM.createEventHandle (#18756)
Dominic Gannaway committed
May 12, 2020 at 20:24 UTC
61f2a560e0c5a0a7d218c52a1b74b3f3592acb9b
16 files changed
+3449
-13
packages/react-dom/index.classic.fb.js
+1
@@ -36,4 +36,5 @@ export {
36
unstable_scheduleHydration,
37
unstable_renderSubtreeIntoContainer,
38
unstable_createPortal,
39
+ unstable_createEventHandle,
40
} from './src/client/ReactDOM';
packages/react-dom/index.js
+1
@@ -27,4 +27,5 @@ export {
27
unstable_scheduleHydration,
28
unstable_renderSubtreeIntoContainer,
29
unstable_createPortal,
30
+ unstable_createEventHandle,
31
} from './src/client/ReactDOM';
packages/react-dom/index.modern.fb.js
+1
@@ -19,4 +19,5 @@ export {
19
createBlockingRoot as unstable_createBlockingRoot,
20
unstable_flushControlled,
21
unstable_scheduleHydration,
22
+ unstable_createEventHandle,
23
} from './src/client/ReactDOM';
packages/react-dom/src/client/ReactDOM.js
+3
@@ -20,6 +20,7 @@ import {
20
unmountComponentAtNode,
21
} from './ReactDOMLegacy';
22
import {createRoot, createBlockingRoot, isValidContainer} from './ReactDOMRoot';
23
+import {createEventHandle} from './ReactDOMEventHandle';
24
25
import {
26
batchedEventUpdates,
@@ -208,6 +209,8 @@ export {
209
// Temporary alias since we already shipped React 16 RC with it.
210
// TODO: remove in React 17.
211
unstable_createPortal,
212
+ // enableCreateEventHandleAPI
213
+ createEventHandle as unstable_createEventHandle,
214
};
215
216
const foundDevTools = injectIntoDevTools({
packages/react-dom/src/client/ReactDOMEventHandle.js
new
+247
@@ -0,0 +1,247 @@
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 type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
11
+import type {EventPriority, ReactScopeInstance} from 'shared/ReactTypes';
12
+import type {
13
+ ReactDOMEventHandle,
14
+ ReactDOMEventHandleListener,
15
+} from '../shared/ReactDOMTypes';
16
+
17
+import {getEventPriorityForListenerSystem} from '../events/DOMEventProperties';
18
+import {
19
+ getClosestInstanceFromNode,
20
+ getEventHandlerListeners,
21
+ setEventHandlerListeners,
22
+ getEventListenerMap,
23
+ getFiberFromScopeInstance,
24
+} from './ReactDOMComponentTree';
25
+import {ELEMENT_NODE} from '../shared/HTMLNodeType';
26
+import {
27
+ listenToTopLevelEvent,
28
+ addEventTypeToDispatchConfig,
29
+} from '../events/DOMModernPluginEventSystem';
30
+
31
+import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags';
32
+import {
33
+ PLUGIN_EVENT_SYSTEM,
34
+ IS_TARGET_PHASE_ONLY,
35
+} from '../events/EventSystemFlags';
36
+
37
+import {
38
+ enableScopeAPI,
39
+ enableCreateEventHandleAPI,
40
+} from 'shared/ReactFeatureFlags';
41
+import invariant from 'shared/invariant';
42
+
43
+type EventHandleOptions = {|
44
+ capture?: boolean,
45
+ passive?: boolean,
46
+ priority?: EventPriority,
47
+|};
48
+
49
+function getNearestRootOrPortalContainer(node: Fiber): null | Element {
50
+ while (node !== null) {
51
+ const tag = node.tag;
52
+ // Once we encounter a host container or root container
53
+ // we can return their DOM instance.
54
+ if (tag === HostRoot || tag === HostPortal) {
55
+ return node.stateNode.containerInfo;
56
+ }
57
+ node = node.return;
58
+ }
59
+ return null;
60
+}
61
+
62
+function isValidEventTarget(target: EventTarget | ReactScopeInstance): boolean {
63
+ return typeof (target: Object).addEventListener === 'function';
64
+}
65
+
66
+function isReactScope(target: EventTarget | ReactScopeInstance): boolean {
67
+ return typeof (target: Object).getChildContextValues === 'function';
68
+}
69
+
70
+function createEventHandleListener(
71
+ type: DOMTopLevelEventType,
72
+ capture: boolean,
73
+ callback: (SyntheticEvent<EventTarget>) => void,
74
+ destroy: (target: EventTarget | ReactScopeInstance) => void,
75
+): ReactDOMEventHandleListener {
76
+ return {
77
+ callback,
78
+ capture,
79
+ destroy,
80
+ type,
81
+ };
82
+}
83
+
84
+function registerEventOnNearestTargetContainer(
85
+ targetFiber: Fiber,
86
+ topLevelType: DOMTopLevelEventType,
87
+ passive: boolean | void,
88
+ priority: EventPriority | void,
89
+): void {
90
+ // If it is, find the nearest root or portal and make it
91
+ // our event handle target container.
92
+ const targetContainer = getNearestRootOrPortalContainer(targetFiber);
93
+ if (targetContainer === null) {
94
+ invariant(
95
+ false,
96
+ 'ReactDOM.createEventHandle: setListener called on an target ' +
97
+ 'that did not have a corresponding root. This is likely a bug in React.',
98
+ );
99
+ }
100
+ const listenerMap = getEventListenerMap(targetContainer);
101
+ listenToTopLevelEvent(
102
+ topLevelType,
103
+ targetContainer,
104
+ listenerMap,
105
+ PLUGIN_EVENT_SYSTEM,
106
+ passive,
107
+ priority,
108
+ );
109
+}
110
+
111
+export function createEventHandle(
112
+ type: string,
113
+ options?: EventHandleOptions,
114
+): ReactDOMEventHandle {
115
+ if (enableCreateEventHandleAPI) {
116
+ const topLevelType = ((type: any): DOMTopLevelEventType);
117
+ let capture = false;
118
+ let passive = undefined; // Undefined means to use the browser default
119
+ let priority;
120
+
121
+ if (options != null) {
122
+ const optionsCapture = options.capture;
123
+ const optionsPassive = options.passive;
124
+ const optionsPriority = options.priority;
125
+
126
+ if (typeof optionsCapture === 'boolean') {
127
+ capture = optionsCapture;
128
+ }
129
+ if (typeof optionsPassive === 'boolean') {
130
+ passive = optionsPassive;
131
+ }
132
+ if (typeof optionsPriority === 'number') {
133
+ priority = optionsPriority;
134
+ }
135
+ }
136
+ if (priority === undefined) {
137
+ priority = getEventPriorityForListenerSystem(topLevelType);
138
+ }
139
+
140
+ const listeners = new Map();
141
+
142
+ const destroy = (target: EventTarget | ReactScopeInstance): void => {
143
+ const listener = listeners.get(target);
144
+ if (listener !== undefined) {
145
+ listeners.delete(target);
146
+ const targetListeners = getEventHandlerListeners(target);
147
+ if (targetListeners !== null) {
148
+ targetListeners.delete(listener);
149
+ }
150
+ }
151
+ };
152
+
153
+ const clear = (): void => {
154
+ const eventTargetsArr = Array.from(listeners.keys());
155
+ for (let i = 0; i < eventTargetsArr.length; i++) {
156
+ destroy(eventTargetsArr[i]);
157
+ }
158
+ };
159
+
160
+ return {
161
+ setListener(
162
+ target: EventTarget | ReactScopeInstance,
163
+ callback: null | ((SyntheticEvent<EventTarget>) => void),
164
+ ): void {
165
+ // Check if the target is a DOM element.
166
+ if ((target: any).nodeType === ELEMENT_NODE) {
167
+ const targetElement = ((target: any): Element);
168
+ // Check if the DOM element is managed by React.
169
+ const targetFiber = getClosestInstanceFromNode(targetElement);
170
+ if (targetFiber === null) {
171
+ invariant(
172
+ false,
173
+ 'ReactDOM.createEventHandle: setListener called on an element ' +
174
+ 'target that is not managed by React. Ensure React rendered the DOM element.',
175
+ );
176
+ }
177
+ registerEventOnNearestTargetContainer(
178
+ targetFiber,
179
+ topLevelType,
180
+ passive,
181
+ priority,
182
+ );
183
+ } else if (enableScopeAPI && isReactScope(target)) {
184
+ const scopeTarget = ((target: any): ReactScopeInstance);
185
+ const targetFiber = getFiberFromScopeInstance(scopeTarget);
186
+ if (targetFiber === null) {
187
+ // Scope is unmounted, do not proceed.
188
+ return;
189
+ }
190
+ registerEventOnNearestTargetContainer(
191
+ targetFiber,
192
+ topLevelType,
193
+ passive,
194
+ priority,
195
+ );
196
+ } else if (isValidEventTarget(target)) {
197
+ const eventTarget = ((target: any): EventTarget);
198
+ const listenerMap = getEventListenerMap(eventTarget);
199
+ listenToTopLevelEvent(
200
+ topLevelType,
201
+ eventTarget,
202
+ listenerMap,
203
+ PLUGIN_EVENT_SYSTEM | IS_TARGET_PHASE_ONLY,
204
+ passive,
205
+ priority,
206
+ capture,
207
+ );
208
+ } else {
209
+ invariant(
210
+ false,
211
+ 'ReactDOM.createEventHandle: setListener called on an invalid ' +
212
+ 'target. Provide a vaid EventTarget or an element managed by React.',
213
+ );
214
+ }
215
+ let listener = listeners.get(target);
216
+ if (listener === undefined) {
217
+ if (callback === null) {
218
+ return;
219
+ }
220
+ listener = createEventHandleListener(
221
+ topLevelType,
222
+ capture,
223
+ callback,
224
+ destroy,
225
+ );
226
+ listeners.set(target, listener);
227
+
228
+ let targetListeners = getEventHandlerListeners(target);
229
+ if (targetListeners === null) {
230
+ targetListeners = new Set();
231
+ setEventHandlerListeners(target, targetListeners);
232
+ }
233
+ targetListeners.add(listener);
234
+ // Finally, add the event to our known event types list.
235
+ addEventTypeToDispatchConfig(topLevelType);
236
+ } else if (callback !== null) {
237
+ listener.callback = callback;
238
+ } else {
239
+ // Remove listener
240
+ destroy(target);
241
+ }
242
+ },
243
+ clear,
244
+ };
245
+ }
246
+ return (null: any);
247
+}
packages/react-dom/src/client/ReactDOMHostConfig.js
+14
-5
@@ -76,12 +76,15 @@ import {
76
enableDeprecatedFlareAPI,
77
enableFundamentalAPI,
78
enableModernEventSystem,
79
- enableScopeAPI,
79
enableCreateEventHandleAPI,
80
+ enableScopeAPI,
81
} from 'shared/ReactFeatureFlags';
82
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
83
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
84
-import {listenToEvent} from '../events/DOMModernPluginEventSystem';
84
+import {
85
+ listenToEvent,
86
+ clearEventHandleListenersForTarget,
87
+} from '../events/DOMModernPluginEventSystem';
88
89
export type Type = string;
90
export type Props = {
@@ -539,7 +542,9 @@ function dispatchAfterDetachedBlur(target: HTMLElement): void {
542
export function removeInstanceEventHandles(
543
instance: Instance | TextInstance | SuspenseInstance,
544
) {
542
- // TODO for ReactDOM.createEventInstance
545
+ if (enableCreateEventHandleAPI) {
546
+ clearEventHandleListenersForTarget(instance);
547
+ }
548
}
549
550
export function removeChild(
@@ -1136,8 +1141,12 @@ export function prepareScopeUpdate(
1141
}
1142
}
1143
1139
-export function removeScopeEventHandles(scopeInstance: Object): void {
1140
- // TODO when we add createEventHandle
1144
+export function removeScopeEventHandles(
1145
+ scopeInstance: ReactScopeInstance,
1146
+): void {
1147
+ if (enableScopeAPI && enableCreateEventHandleAPI) {
1148
+ clearEventHandleListenersForTarget(scopeInstance);
1149
+ }
1150
}
1151
1152
export function getInstanceFromScope(
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+184
-3
@@ -14,7 +14,7 @@ import type {
14
ElementListenerMapEntry,
15
} from '../client/ReactDOMComponentTree';
16
import type {EventSystemFlags} from './EventSystemFlags';
17
-import type {EventPriority} from 'shared/ReactTypes';
17
+import type {EventPriority, ReactScopeInstance} from 'shared/ReactTypes';
18
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
19
import type {
20
ModernPluginModule,
@@ -23,7 +23,10 @@ import type {
23
DispatchQueueItemPhase,
24
DispatchQueueItemPhaseEntry,
25
} from 'legacy-events/PluginModuleType';
26
-import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
26
+import type {
27
+ ReactSyntheticEvent,
28
+ CustomDispatchConfig,
29
+} from 'legacy-events/ReactSyntheticEventType';
30
31
import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
32
import {plugins} from 'legacy-events/EventPluginRegistry';
@@ -39,6 +42,7 @@ import {
42
HostPortal,
43
HostComponent,
44
HostText,
45
+ ScopeComponent,
46
} from 'react-reconciler/src/ReactWorkTags';
47
48
import getEventTarget from './getEventTarget';
@@ -84,6 +88,7 @@ import {
88
import {
89
getClosestInstanceFromNode,
90
getEventListenerMap,
91
+ getEventHandlerListeners,
92
} from '../client/ReactDOMComponentTree';
93
import {COMMENT_NODE} from '../shared/HTMLNodeType';
94
import {batchedEventUpdates} from './ReactDOMUpdateBatching';
@@ -93,6 +98,7 @@ import {passiveBrowserEventsSupported} from './checkPassiveEvents';
98
import {
99
enableLegacyFBSupport,
100
enableCreateEventHandleAPI,
101
+ enableScopeAPI,
102
} from 'shared/ReactFeatureFlags';
103
import {
104
invokeGuardedCallbackAndCatchFirstError,
@@ -106,6 +112,8 @@ import {
112
addEventBubbleListenerWithPassiveFlag,
113
addEventCaptureListenerWithPassiveFlag,
114
} from './EventListener';
115
+import {removeTrappedEventListener} from './DeprecatedDOMEventResponderSystem';
116
+import {topLevelEventsToDispatchConfig} from './DOMEventProperties';
117
118
export const capturePhaseEvents: Set<DOMTopLevelEventType> = new Set([
119
TOP_FOCUS,
@@ -148,6 +156,14 @@ if (enableCreateEventHandleAPI) {
156
capturePhaseEvents.add(TOP_AFTER_BLUR);
157
}
158
159
+const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
160
+ customEvent: true,
161
+ phasedRegistrationNames: {
162
+ bubbled: null,
163
+ captured: null,
164
+ },
165
+};
166
+
167
function executeDispatch(
168
event: ReactSyntheticEvent,
169
listener: Function,
@@ -226,6 +242,15 @@ function dispatchEventsForPlugins(
242
dispatchEventsInBatch(dispatchQueue);
243
}
244
245
+function shouldUpgradeListener(
246
+ listenerEntry: void | ElementListenerMapEntry,
247
+ passive: void | boolean,
248
+): boolean {
249
+ return (
250
+ listenerEntry !== undefined && listenerEntry.passive === true && !passive
251
+ );
252
+}
253
+
254
export function listenToTopLevelEvent(
255
topLevelType: DOMTopLevelEventType,
256
target: EventTarget,
@@ -248,7 +273,21 @@ export function listenToTopLevelEvent(
273
const listenerEntry: ElementListenerMapEntry | void = listenerMap.get(
274
listenerMapKey,
275
);
251
- if (listenerEntry === undefined) {
276
+ const shouldUpgrade = shouldUpgradeListener(listenerEntry, passive);
277
+
278
+ // If the listener entry is empty or we should upgrade, then
279
+ // we need to trap an event listener onto the target.
280
+ if (listenerEntry === undefined || shouldUpgrade) {
281
+ // If we should upgrade, then we need to remove the existing trapped
282
+ // event listener for the target container.
283
+ if (shouldUpgrade) {
284
+ removeTrappedEventListener(
285
+ target,
286
+ topLevelType,
287
+ capture,
288
+ ((listenerEntry: any): ElementListenerMapEntry).listener,
289
+ );
290
+ }
291
const listener = addTrappedEventListener(
292
target,
293
topLevelType,
@@ -539,6 +578,7 @@ export function accumulateTwoPhaseListeners(
578
targetFiber: Fiber | null,
579
dispatchQueue: DispatchQueue,
580
event: ReactSyntheticEvent,
581
+ accumulateEventHandleListeners?: boolean,
582
): void {
583
const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
584
const capturePhase: DispatchQueueItemPhase = [];
@@ -549,6 +589,8 @@ export function accumulateTwoPhaseListeners(
589
// usual two phase accumulation using the React fiber tree to pick up
590
// all relevant useEvent and on* prop events.
591
let instance = targetFiber;
592
+ let lastHostComponent = null;
593
+ const targetType = event.type;
594
595
// Accumulate all instances and listeners via the target -> root path.
596
while (instance !== null) {
@@ -556,6 +598,38 @@ export function accumulateTwoPhaseListeners(
598
// Handle listeners that are on HostComponents (i.e. <div>)
599
if (tag === HostComponent && stateNode !== null) {
600
const currentTarget = stateNode;
601
+ lastHostComponent = currentTarget;
602
+ // For Event Handle listeners
603
+ if (enableCreateEventHandleAPI && accumulateEventHandleListeners) {
604
+ const listeners = getEventHandlerListeners(currentTarget);
605
+
606
+ if (listeners !== null) {
607
+ const listenersArr = Array.from(listeners);
608
+ for (let i = 0; i < listenersArr.length; i++) {
609
+ const listener = listenersArr[i];
610
+ const {callback, capture, type} = listener;
611
+ if (type === targetType) {
612
+ if (capture === true) {
613
+ capturePhase.push(
614
+ createDispatchQueueItemPhaseEntry(
615
+ instance,
616
+ callback,
617
+ currentTarget,
618
+ ),
619
+ );
620
+ } else {
621
+ bubblePhase.push(
622
+ createDispatchQueueItemPhaseEntry(
623
+ instance,
624
+ callback,
625
+ currentTarget,
626
+ ),
627
+ );
628
+ }
629
+ }
630
+ }
631
+ }
632
+ }
633
// Standard React on* listeners, i.e. onClick prop
634
if (captured !== null) {
635
const captureListener = getListener(instance, captured);
@@ -581,6 +655,43 @@ export function accumulateTwoPhaseListeners(
655
);
656
}
657
}
658
+ } else if (
659
+ enableCreateEventHandleAPI &&
660
+ enableScopeAPI &&
661
+ accumulateEventHandleListeners &&
662
+ tag === ScopeComponent &&
663
+ lastHostComponent !== null
664
+ ) {
665
+ const reactScopeInstance = stateNode;
666
+ const listeners = getEventHandlerListeners(reactScopeInstance);
667
+ const lastCurrentTarget = ((lastHostComponent: any): Element);
668
+
669
+ if (listeners !== null) {
670
+ const listenersArr = Array.from(listeners);
671
+ for (let i = 0; i < listenersArr.length; i++) {
672
+ const listener = listenersArr[i];
673
+ const {callback, capture, type} = listener;
674
+ if (type === targetType) {
675
+ if (capture === true) {
676
+ capturePhase.push(
677
+ createDispatchQueueItemPhaseEntry(
678
+ instance,
679
+ callback,
680
+ lastCurrentTarget,
681
+ ),
682
+ );
683
+ } else {
684
+ bubblePhase.push(
685
+ createDispatchQueueItemPhaseEntry(
686
+ instance,
687
+ callback,
688
+ lastCurrentTarget,
689
+ ),
690
+ );
691
+ }
692
+ }
693
+ }
694
+ }
695
}
696
instance = instance.return;
697
}
@@ -736,6 +847,76 @@ export function accumulateEnterLeaveListeners(
847
}
848
}
849
850
+export function accumulateEventTargetListeners(
851
+ dispatchQueue: DispatchQueue,
852
+ event: ReactSyntheticEvent,
853
+ currentTarget: EventTarget,
854
+): void {
855
+ const capturePhase: DispatchQueueItemPhase = [];
856
+ const bubblePhase: DispatchQueueItemPhase = [];
857
+
858
+ const eventListeners = getEventHandlerListeners(currentTarget);
859
+ if (eventListeners !== null) {
860
+ const listenersArr = Array.from(eventListeners);
861
+ const targetType = ((event.type: any): DOMTopLevelEventType);
862
+ const isCapturePhase = (event: any).eventPhase === 1;
863
+
864
+ for (let i = 0; i < listenersArr.length; i++) {
865
+ const listener = listenersArr[i];
866
+ const {callback, capture, type} = listener;
867
+ if (type === targetType) {
868
+ if (isCapturePhase && capture) {
869
+ capturePhase.push(
870
+ createDispatchQueueItemPhaseEntry(null, callback, currentTarget),
871
+ );
872
+ } else if (!isCapturePhase && !capture) {
873
+ bubblePhase.push(
874
+ createDispatchQueueItemPhaseEntry(null, callback, currentTarget),
875
+ );
876
+ }
877
+ }
878
+ }
879
+ }
880
+ if (capturePhase.length !== 0 || bubblePhase.length !== 0) {
881
+ dispatchQueue.push(
882
+ createDispatchQueueItem(event, capturePhase, bubblePhase),
883
+ );
884
+ }
885
+}
886
+
887
+export function addEventTypeToDispatchConfig(type: DOMTopLevelEventType): void {
888
+ const dispatchConfig = topLevelEventsToDispatchConfig.get(type);
889
+ // If we don't have a dispatchConfig, then we're dealing with
890
+ // an event type that React does not know about (i.e. a custom event).
891
+ // We need to register an event config for this or the SimpleEventPlugin
892
+ // will not appropriately provide a SyntheticEvent, so we use out empty
893
+ // dispatch config for custom events.
894
+ if (dispatchConfig === undefined) {
895
+ topLevelEventsToDispatchConfig.set(
896
+ type,
897
+ emptyDispatchConfigForCustomEvents,
898
+ );
899
+ }
900
+}
901
+
902
+export function clearEventHandleListenersForTarget(
903
+ target: EventTarget | ReactScopeInstance,
904
+): void {
905
+ // It's unfortunate that we have to do this cleanup, but
906
+ // it's necessary otherwise we will leak the host instances
907
+ // on the createEventHandle API "listeners" Map. We call destroy
908
+ // on each listener to ensure we properly remove the instance
909
+ // from the listeners Map. Note: we have this Map so that we
910
+ // can track listeners for the handle.clear() API call.
911
+ const listeners = getEventHandlerListeners(target);
912
+ if (listeners !== null) {
913
+ const listenersArr = Array.from(listeners);
914
+ for (let i = 0; i < listenersArr.length; i++) {
915
+ listenersArr[i].destroy(target);
916
+ }
917
+ }
918
+}
919
+
920
export function getListenerMapKey(
921
topLevelType: DOMTopLevelEventType,
922
capture: boolean,
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+1582
@@ -9,11 +9,14 @@
9
10
'use strict';
11
12
+import {createEventTarget} from 'dom-event-testing-library';
13
+
14
let React;
15
let ReactFeatureFlags;
16
let ReactDOM;
17
let ReactDOMServer;
18
let Scheduler;
19
+let ReactTestUtils;
20
21
function dispatchEvent(element, type) {
22
const event = document.createEvent('Event');
@@ -1167,6 +1170,1585 @@ describe('DOMModernPluginEventSystem', () => {
1170
]);
1171
}
1172
});
1173
+
1174
+ describe('ReactDOM.createEventHandle', () => {
1175
+ beforeEach(() => {
1176
+ jest.resetModules();
1177
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
1178
+ ReactFeatureFlags.enableLegacyFBSupport = enableLegacyFBSupport;
1179
+ ReactFeatureFlags.enableModernEventSystem = true;
1180
+ ReactFeatureFlags.enableCreateEventHandleAPI = true;
1181
+
1182
+ React = require('react');
1183
+ ReactDOM = require('react-dom');
1184
+ Scheduler = require('scheduler');
1185
+ ReactDOMServer = require('react-dom/server');
1186
+ ReactTestUtils = require('react-dom/test-utils');
1187
+ });
1188
+
1189
+ // @gate experimental
1190
+ it('can render correctly with the ReactDOMServer', () => {
1191
+ const clickEvent = jest.fn();
1192
+ const click = ReactDOM.unstable_createEventHandle('click');
1193
+
1194
+ function Test() {
1195
+ const divRef = React.useRef(null);
1196
+
1197
+ React.useEffect(() => {
1198
+ click.setListener(divRef.current, clickEvent);
1199
+ });
1200
+
1201
+ return <div ref={divRef}>Hello world</div>;
1202
+ }
1203
+ const output = ReactDOMServer.renderToString(<Test />);
1204
+ expect(output).toBe(`<div data-reactroot="">Hello world</div>`);
1205
+ });
1206
+
1207
+ // @gate experimental
1208
+ it('can render correctly with the ReactDOMServer hydration', () => {
1209
+ const clickEvent = jest.fn();
1210
+ const spanRef = React.createRef();
1211
+ const click = ReactDOM.unstable_createEventHandle('click');
1212
+
1213
+ function Test() {
1214
+ React.useEffect(() => {
1215
+ click.setListener(spanRef.current, clickEvent);
1216
+ });
1217
+
1218
+ return (
1219
+ <div>
1220
+ <span ref={spanRef}>Hello world</span>
1221
+ </div>
1222
+ );
1223
+ }
1224
+ const output = ReactDOMServer.renderToString(<Test />);
1225
+ expect(output).toBe(
1226
+ `<div data-reactroot=""><span>Hello world</span></div>`,
1227
+ );
1228
+ container.innerHTML = output;
1229
+ ReactDOM.hydrate(<Test />, container);
1230
+ Scheduler.unstable_flushAll();
1231
+ dispatchClickEvent(spanRef.current);
1232
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1233
+ });
1234
+
1235
+ // @gate experimental
1236
+ it('should correctly work for a basic "click" listener', () => {
1237
+ let log = [];
1238
+ const clickEvent = jest.fn(event => {
1239
+ log.push({
1240
+ eventPhase: event.eventPhase,
1241
+ type: event.type,
1242
+ currentTarget: event.currentTarget,
1243
+ target: event.target,
1244
+ });
1245
+ });
1246
+ const divRef = React.createRef();
1247
+ const buttonRef = React.createRef();
1248
+ const click = ReactDOM.unstable_createEventHandle('click');
1249
+
1250
+ function Test() {
1251
+ React.useEffect(() => {
1252
+ click.setListener(buttonRef.current, clickEvent);
1253
+ });
1254
+
1255
+ return (
1256
+ <button ref={buttonRef}>
1257
+ <div ref={divRef}>Click me!</div>
1258
+ </button>
1259
+ );
1260
+ }
1261
+
1262
+ ReactDOM.render(<Test />, container);
1263
+ Scheduler.unstable_flushAll();
1264
+
1265
+ expect(container.innerHTML).toBe(
1266
+ '<button><div>Click me!</div></button>',
1267
+ );
1268
+
1269
+ // Clicking the button should trigger the event callback
1270
+ let divElement = divRef.current;
1271
+ dispatchClickEvent(divElement);
1272
+ expect(log).toEqual([
1273
+ {
1274
+ eventPhase: 3,
1275
+ type: 'click',
1276
+ currentTarget: buttonRef.current,
1277
+ target: divRef.current,
1278
+ },
1279
+ ]);
1280
+ expect(clickEvent).toBeCalledTimes(1);
1281
+
1282
+ // Unmounting the container and clicking should not work
1283
+ ReactDOM.render(null, container);
1284
+ Scheduler.unstable_flushAll();
1285
+
1286
+ dispatchClickEvent(divElement);
1287
+ expect(clickEvent).toBeCalledTimes(1);
1288
+
1289
+ // Re-rendering the container and clicking should work
1290
+ ReactDOM.render(<Test />, container);
1291
+ Scheduler.unstable_flushAll();
1292
+
1293
+ divElement = divRef.current;
1294
+ dispatchClickEvent(divElement);
1295
+ expect(clickEvent).toBeCalledTimes(2);
1296
+
1297
+ log = [];
1298
+
1299
+ // Clicking the button should also work
1300
+ const buttonElement = buttonRef.current;
1301
+ dispatchClickEvent(buttonElement);
1302
+ expect(log).toEqual([
1303
+ {
1304
+ eventPhase: 3,
1305
+ type: 'click',
1306
+ currentTarget: buttonRef.current,
1307
+ target: buttonRef.current,
1308
+ },
1309
+ ]);
1310
+
1311
+ const click2 = ReactDOM.unstable_createEventHandle('click');
1312
+
1313
+ function Test2({clickEvent2}) {
1314
+ React.useEffect(() => {
1315
+ click2.setListener(buttonRef.current, clickEvent2);
1316
+ });
1317
+
1318
+ return (
1319
+ <button ref={buttonRef}>
1320
+ <div ref={divRef}>Click me!</div>
1321
+ </button>
1322
+ );
1323
+ }
1324
+
1325
+ let clickEvent2 = jest.fn();
1326
+ ReactDOM.render(<Test2 clickEvent2={clickEvent2} />, container);
1327
+ Scheduler.unstable_flushAll();
1328
+
1329
+ divElement = divRef.current;
1330
+ dispatchClickEvent(divElement);
1331
+ expect(clickEvent2).toBeCalledTimes(1);
1332
+
1333
+ // Reset the function we pass in, so it's different
1334
+ clickEvent2 = jest.fn();
1335
+ ReactDOM.render(<Test2 clickEvent2={clickEvent2} />, container);
1336
+ Scheduler.unstable_flushAll();
1337
+
1338
+ divElement = divRef.current;
1339
+ dispatchClickEvent(divElement);
1340
+ expect(clickEvent2).toBeCalledTimes(1);
1341
+ });
1342
+
1343
+ // @gate experimental
1344
+ it('should correctly work for setting and clearing a basic "click" listener', () => {
1345
+ const clickEvent = jest.fn();
1346
+ const divRef = React.createRef();
1347
+ const buttonRef = React.createRef();
1348
+ const click = ReactDOM.unstable_createEventHandle('click');
1349
+
1350
+ function Test({off}) {
1351
+ React.useEffect(() => {
1352
+ click.setListener(buttonRef.current, clickEvent);
1353
+ });
1354
+
1355
+ React.useEffect(() => {
1356
+ if (off) {
1357
+ click.setListener(buttonRef.current, null);
1358
+ }
1359
+ }, [off]);
1360
+
1361
+ return (
1362
+ <button ref={buttonRef}>
1363
+ <div ref={divRef}>Click me!</div>
1364
+ </button>
1365
+ );
1366
+ }
1367
+
1368
+ ReactDOM.render(<Test off={false} />, container);
1369
+ Scheduler.unstable_flushAll();
1370
+
1371
+ let divElement = divRef.current;
1372
+ dispatchClickEvent(divElement);
1373
+ expect(clickEvent).toBeCalledTimes(1);
1374
+
1375
+ // The listener should get unmounted in the second effect
1376
+ ReactDOM.render(<Test off={true} />, container);
1377
+ Scheduler.unstable_flushAll();
1378
+
1379
+ clickEvent.mockClear();
1380
+
1381
+ divElement = divRef.current;
1382
+ dispatchClickEvent(divElement);
1383
+ expect(clickEvent).toBeCalledTimes(0);
1384
+ });
1385
+
1386
+ // @gate experimental
1387
+ it('should handle the target being a text node', () => {
1388
+ const clickEvent = jest.fn();
1389
+ const buttonRef = React.createRef();
1390
+ const click = ReactDOM.unstable_createEventHandle('click');
1391
+
1392
+ function Test() {
1393
+ React.useEffect(() => {
1394
+ click.setListener(buttonRef.current, clickEvent);
1395
+ });
1396
+
1397
+ return <button ref={buttonRef}>Click me!</button>;
1398
+ }
1399
+
1400
+ ReactDOM.render(<Test />, container);
1401
+ Scheduler.unstable_flushAll();
1402
+
1403
+ const textNode = buttonRef.current.firstChild;
1404
+ dispatchClickEvent(textNode);
1405
+ expect(clickEvent).toBeCalledTimes(1);
1406
+ });
1407
+
1408
+ // @gate experimental
1409
+ it('handle propagation of click events', () => {
1410
+ const buttonRef = React.createRef();
1411
+ const divRef = React.createRef();
1412
+ const log = [];
1413
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
1414
+ const onClickCapture = jest.fn(e =>
1415
+ log.push(['capture', e.currentTarget]),
1416
+ );
1417
+ const click = ReactDOM.unstable_createEventHandle('click');
1418
+ const clickCapture = ReactDOM.unstable_createEventHandle('click', {
1419
+ capture: true,
1420
+ });
1421
+
1422
+ function Test() {
1423
+ React.useEffect(() => {
1424
+ click.setListener(buttonRef.current, onClick);
1425
+ clickCapture.setListener(buttonRef.current, onClickCapture);
1426
+ click.setListener(divRef.current, onClick);
1427
+ clickCapture.setListener(divRef.current, onClickCapture);
1428
+ });
1429
+
1430
+ return (
1431
+ <button ref={buttonRef}>
1432
+ <div ref={divRef}>Click me!</div>
1433
+ </button>
1434
+ );
1435
+ }
1436
+
1437
+ ReactDOM.render(<Test />, container);
1438
+ Scheduler.unstable_flushAll();
1439
+
1440
+ const buttonElement = buttonRef.current;
1441
+ dispatchClickEvent(buttonElement);
1442
+ expect(onClick).toHaveBeenCalledTimes(1);
1443
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
1444
+ expect(log[0]).toEqual(['capture', buttonElement]);
1445
+ expect(log[1]).toEqual(['bubble', buttonElement]);
1446
+
1447
+ log.length = 0;
1448
+ onClick.mockClear();
1449
+ onClickCapture.mockClear();
1450
+
1451
+ const divElement = divRef.current;
1452
+ dispatchClickEvent(divElement);
1453
+ expect(onClick).toHaveBeenCalledTimes(2);
1454
+ expect(onClickCapture).toHaveBeenCalledTimes(2);
1455
+ expect(log[0]).toEqual(['capture', buttonElement]);
1456
+ expect(log[1]).toEqual(['capture', divElement]);
1457
+ expect(log[2]).toEqual(['bubble', divElement]);
1458
+ expect(log[3]).toEqual(['bubble', buttonElement]);
1459
+ });
1460
+
1461
+ // @gate experimental
1462
+ it('handle propagation of click events mixed with onClick events', () => {
1463
+ const buttonRef = React.createRef();
1464
+ const divRef = React.createRef();
1465
+ const log = [];
1466
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
1467
+ const onClickCapture = jest.fn(e =>
1468
+ log.push(['capture', e.currentTarget]),
1469
+ );
1470
+ const click = ReactDOM.unstable_createEventHandle('click');
1471
+ const clickCapture = ReactDOM.unstable_createEventHandle('click', {
1472
+ capture: true,
1473
+ });
1474
+
1475
+ function Test() {
1476
+ React.useEffect(() => {
1477
+ click.setListener(buttonRef.current, onClick);
1478
+ clickCapture.setListener(buttonRef.current, onClickCapture);
1479
+ });
1480
+
1481
+ return (
1482
+ <button ref={buttonRef}>
1483
+ <div
1484
+ ref={divRef}
1485
+ onClick={onClick}
1486
+ onClickCapture={onClickCapture}>
1487
+ Click me!
1488
+ </div>
1489
+ </button>
1490
+ );
1491
+ }
1492
+
1493
+ ReactDOM.render(<Test />, container);
1494
+ Scheduler.unstable_flushAll();
1495
+
1496
+ const buttonElement = buttonRef.current;
1497
+ dispatchClickEvent(buttonElement);
1498
+ expect(onClick).toHaveBeenCalledTimes(1);
1499
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
1500
+ expect(log[0]).toEqual(['capture', buttonElement]);
1501
+ expect(log[1]).toEqual(['bubble', buttonElement]);
1502
+
1503
+ const divElement = divRef.current;
1504
+ dispatchClickEvent(divElement);
1505
+ expect(onClick).toHaveBeenCalledTimes(3);
1506
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
1507
+ expect(log[2]).toEqual(['capture', buttonElement]);
1508
+ expect(log[3]).toEqual(['capture', divElement]);
1509
+ expect(log[4]).toEqual(['bubble', divElement]);
1510
+ expect(log[5]).toEqual(['bubble', buttonElement]);
1511
+ });
1512
+
1513
+ // @gate experimental
1514
+ it('should correctly work for a basic "click" listener on the outer target', () => {
1515
+ const log = [];
1516
+ const clickEvent = jest.fn(event => {
1517
+ log.push({
1518
+ eventPhase: event.eventPhase,
1519
+ type: event.type,
1520
+ currentTarget: event.currentTarget,
1521
+ target: event.target,
1522
+ });
1523
+ });
1524
+ const divRef = React.createRef();
1525
+ const buttonRef = React.createRef();
1526
+ const click = ReactDOM.unstable_createEventHandle('click');
1527
+
1528
+ function Test() {
1529
+ React.useEffect(() => {
1530
+ click.setListener(divRef.current, clickEvent);
1531
+ });
1532
+
1533
+ return (
1534
+ <button ref={buttonRef}>
1535
+ <div ref={divRef}>Click me!</div>
1536
+ </button>
1537
+ );
1538
+ }
1539
+
1540
+ ReactDOM.render(<Test />, container);
1541
+ Scheduler.unstable_flushAll();
1542
+
1543
+ expect(container.innerHTML).toBe(
1544
+ '<button><div>Click me!</div></button>',
1545
+ );
1546
+
1547
+ // Clicking the button should trigger the event callback
1548
+ let divElement = divRef.current;
1549
+ dispatchClickEvent(divElement);
1550
+ expect(log).toEqual([
1551
+ {
1552
+ eventPhase: 3,
1553
+ type: 'click',
1554
+ currentTarget: divRef.current,
1555
+ target: divRef.current,
1556
+ },
1557
+ ]);
1558
+
1559
+ // Unmounting the container and clicking should not work
1560
+ ReactDOM.render(null, container);
1561
+ dispatchClickEvent(divElement);
1562
+ expect(clickEvent).toBeCalledTimes(1);
1563
+
1564
+ // Re-rendering the container and clicking should work
1565
+ ReactDOM.render(<Test />, container);
1566
+ Scheduler.unstable_flushAll();
1567
+
1568
+ divElement = divRef.current;
1569
+ dispatchClickEvent(divElement);
1570
+ expect(clickEvent).toBeCalledTimes(2);
1571
+
1572
+ // Clicking the button should not work
1573
+ const buttonElement = buttonRef.current;
1574
+ dispatchClickEvent(buttonElement);
1575
+ expect(clickEvent).toBeCalledTimes(2);
1576
+ });
1577
+
1578
+ // @gate experimental
1579
+ it('should correctly handle many nested target listeners', () => {
1580
+ const buttonRef = React.createRef();
1581
+ const targetListener1 = jest.fn();
1582
+ const targetListener2 = jest.fn();
1583
+ const targetListener3 = jest.fn();
1584
+ const targetListener4 = jest.fn();
1585
+ let click1 = ReactDOM.unstable_createEventHandle('click', {
1586
+ capture: true,
1587
+ });
1588
+ let click2 = ReactDOM.unstable_createEventHandle('click', {
1589
+ capture: true,
1590
+ });
1591
+ let click3 = ReactDOM.unstable_createEventHandle('click');
1592
+ let click4 = ReactDOM.unstable_createEventHandle('click');
1593
+
1594
+ function Test() {
1595
+ React.useEffect(() => {
1596
+ click1.setListener(buttonRef.current, targetListener1);
1597
+ click2.setListener(buttonRef.current, targetListener2);
1598
+ click3.setListener(buttonRef.current, targetListener3);
1599
+ click4.setListener(buttonRef.current, targetListener4);
1600
+ });
1601
+
1602
+ return <button ref={buttonRef}>Click me!</button>;
1603
+ }
1604
+
1605
+ ReactDOM.render(<Test />, container);
1606
+ Scheduler.unstable_flushAll();
1607
+
1608
+ let buttonElement = buttonRef.current;
1609
+ dispatchClickEvent(buttonElement);
1610
+
1611
+ expect(targetListener1).toHaveBeenCalledTimes(1);
1612
+ expect(targetListener2).toHaveBeenCalledTimes(1);
1613
+ expect(targetListener3).toHaveBeenCalledTimes(1);
1614
+ expect(targetListener4).toHaveBeenCalledTimes(1);
1615
+
1616
+ click1 = ReactDOM.unstable_createEventHandle('click');
1617
+ click2 = ReactDOM.unstable_createEventHandle('click');
1618
+ click3 = ReactDOM.unstable_createEventHandle('click');
1619
+ click4 = ReactDOM.unstable_createEventHandle('click');
1620
+
1621
+ function Test2() {
1622
+ React.useEffect(() => {
1623
+ click1.setListener(buttonRef.current, targetListener1);
1624
+ click2.setListener(buttonRef.current, targetListener2);
1625
+ click3.setListener(buttonRef.current, targetListener3);
1626
+ click4.setListener(buttonRef.current, targetListener4);
1627
+ });
1628
+
1629
+ return <button ref={buttonRef}>Click me!</button>;
1630
+ }
1631
+
1632
+ ReactDOM.render(<Test2 />, container);
1633
+ Scheduler.unstable_flushAll();
1634
+
1635
+ buttonElement = buttonRef.current;
1636
+ dispatchClickEvent(buttonElement);
1637
+ expect(targetListener1).toHaveBeenCalledTimes(2);
1638
+ expect(targetListener2).toHaveBeenCalledTimes(2);
1639
+ expect(targetListener3).toHaveBeenCalledTimes(2);
1640
+ expect(targetListener4).toHaveBeenCalledTimes(2);
1641
+ });
1642
+
1643
+ // @gate experimental
1644
+ it('should correctly handle stopPropagation corrrectly for target events', () => {
1645
+ const buttonRef = React.createRef();
1646
+ const divRef = React.createRef();
1647
+ const clickEvent = jest.fn();
1648
+ const click1 = ReactDOM.unstable_createEventHandle('click', {
1649
+ bind: buttonRef,
1650
+ });
1651
+ const click2 = ReactDOM.unstable_createEventHandle('click');
1652
+
1653
+ function Test() {
1654
+ React.useEffect(() => {
1655
+ click1.setListener(buttonRef.current, clickEvent);
1656
+ click2.setListener(divRef.current, e => {
1657
+ e.stopPropagation();
1658
+ });
1659
+ });
1660
+
1661
+ return (
1662
+ <button ref={buttonRef}>
1663
+ <div ref={divRef}>Click me!</div>
1664
+ </button>
1665
+ );
1666
+ }
1667
+
1668
+ ReactDOM.render(<Test />, container);
1669
+ Scheduler.unstable_flushAll();
1670
+
1671
+ const divElement = divRef.current;
1672
+ dispatchClickEvent(divElement);
1673
+ expect(clickEvent).toHaveBeenCalledTimes(0);
1674
+ });
1675
+
1676
+ // @gate experimental
1677
+ it('should correctly handle stopPropagation corrrectly for many target events', () => {
1678
+ const buttonRef = React.createRef();
1679
+ const targetListerner1 = jest.fn(e => e.stopPropagation());
1680
+ const targetListerner2 = jest.fn(e => e.stopPropagation());
1681
+ const targetListerner3 = jest.fn(e => e.stopPropagation());
1682
+ const targetListerner4 = jest.fn(e => e.stopPropagation());
1683
+ const click1 = ReactDOM.unstable_createEventHandle('click');
1684
+ const click2 = ReactDOM.unstable_createEventHandle('click');
1685
+ const click3 = ReactDOM.unstable_createEventHandle('click');
1686
+ const click4 = ReactDOM.unstable_createEventHandle('click');
1687
+
1688
+ function Test() {
1689
+ React.useEffect(() => {
1690
+ click1.setListener(buttonRef.current, targetListerner1);
1691
+ click2.setListener(buttonRef.current, targetListerner2);
1692
+ click3.setListener(buttonRef.current, targetListerner3);
1693
+ click4.setListener(buttonRef.current, targetListerner4);
1694
+ });
1695
+
1696
+ return <button ref={buttonRef}>Click me!</button>;
1697
+ }
1698
+
1699
+ ReactDOM.render(<Test />, container);
1700
+ Scheduler.unstable_flushAll();
1701
+
1702
+ const buttonElement = buttonRef.current;
1703
+ dispatchClickEvent(buttonElement);
1704
+ expect(targetListerner1).toHaveBeenCalledTimes(1);
1705
+ expect(targetListerner2).toHaveBeenCalledTimes(1);
1706
+ expect(targetListerner3).toHaveBeenCalledTimes(1);
1707
+ expect(targetListerner4).toHaveBeenCalledTimes(1);
1708
+ });
1709
+
1710
+ // @gate experimental
1711
+ it('should correctly handle stopPropagation for mixed capture/bubbling target listeners', () => {
1712
+ const buttonRef = React.createRef();
1713
+ const targetListerner1 = jest.fn(e => e.stopPropagation());
1714
+ const targetListerner2 = jest.fn(e => e.stopPropagation());
1715
+ const targetListerner3 = jest.fn(e => e.stopPropagation());
1716
+ const targetListerner4 = jest.fn(e => e.stopPropagation());
1717
+ const click1 = ReactDOM.unstable_createEventHandle('click', {
1718
+ capture: true,
1719
+ });
1720
+ const click2 = ReactDOM.unstable_createEventHandle('click', {
1721
+ capture: true,
1722
+ });
1723
+ const click3 = ReactDOM.unstable_createEventHandle('click');
1724
+ const click4 = ReactDOM.unstable_createEventHandle('click');
1725
+
1726
+ function Test() {
1727
+ React.useEffect(() => {
1728
+ click1.setListener(buttonRef.current, targetListerner1);
1729
+ click2.setListener(buttonRef.current, targetListerner2);
1730
+ click3.setListener(buttonRef.current, targetListerner3);
1731
+ click4.setListener(buttonRef.current, targetListerner4);
1732
+ });
1733
+
1734
+ return <button ref={buttonRef}>Click me!</button>;
1735
+ }
1736
+
1737
+ ReactDOM.render(<Test />, container);
1738
+ Scheduler.unstable_flushAll();
1739
+
1740
+ const buttonElement = buttonRef.current;
1741
+ dispatchClickEvent(buttonElement);
1742
+ expect(targetListerner1).toHaveBeenCalledTimes(1);
1743
+ expect(targetListerner2).toHaveBeenCalledTimes(1);
1744
+ expect(targetListerner3).toHaveBeenCalledTimes(0);
1745
+ expect(targetListerner4).toHaveBeenCalledTimes(0);
1746
+ });
1747
+
1748
+ // @gate experimental
1749
+ it('should work with concurrent mode updates', async () => {
1750
+ const log = [];
1751
+ const ref = React.createRef();
1752
+ const click = ReactDOM.unstable_createEventHandle('click');
1753
+
1754
+ function Test({counter}) {
1755
+ React.useLayoutEffect(() => {
1756
+ click.setListener(ref.current, () => {
1757
+ log.push({counter});
1758
+ });
1759
+ });
1760
+
1761
+ Scheduler.unstable_yieldValue('Test');
1762
+ return <button ref={ref}>Press me</button>;
1763
+ }
1764
+
1765
+ const root = ReactDOM.createRoot(container);
1766
+ root.render(<Test counter={0} />);
1767
+
1768
+ expect(Scheduler).toFlushAndYield(['Test']);
1769
+
1770
+ // Click the button
1771
+ dispatchClickEvent(ref.current);
1772
+ expect(log).toEqual([{counter: 0}]);
1773
+
1774
+ // Clear log
1775
+ log.length = 0;
1776
+
1777
+ // Increase counter
1778
+ root.render(<Test counter={1} />);
1779
+ // Yield before committing
1780
+ expect(Scheduler).toFlushAndYieldThrough(['Test']);
1781
+
1782
+ // Click the button again
1783
+ dispatchClickEvent(ref.current);
1784
+ expect(log).toEqual([{counter: 0}]);
1785
+
1786
+ // Clear log
1787
+ log.length = 0;
1788
+
1789
+ // Commit
1790
+ expect(Scheduler).toFlushAndYield([]);
1791
+ dispatchClickEvent(ref.current);
1792
+ expect(log).toEqual([{counter: 1}]);
1793
+ });
1794
+
1795
+ // @gate experimental
1796
+ it('should correctly work for a basic "click" listener that upgrades', () => {
1797
+ const clickEvent = jest.fn();
1798
+ const buttonRef = React.createRef();
1799
+ const button2Ref = React.createRef();
1800
+ const click = ReactDOM.unstable_createEventHandle('click', {
1801
+ passive: false,
1802
+ });
1803
+ const click2 = ReactDOM.unstable_createEventHandle('click', {
1804
+ passive: true,
1805
+ });
1806
+
1807
+ function Test2() {
1808
+ React.useEffect(() => {
1809
+ click.setListener(button2Ref.current, clickEvent);
1810
+ });
1811
+
1812
+ return <button ref={button2Ref}>Click me!</button>;
1813
+ }
1814
+
1815
+ function Test({extra}) {
1816
+ React.useEffect(() => {
1817
+ click2.setListener(buttonRef.current, clickEvent);
1818
+ });
1819
+
1820
+ return (
1821
+ <>
1822
+ <button ref={buttonRef}>Click me!</button>
1823
+ {extra && <Test2 />}
1824
+ </>
1825
+ );
1826
+ }
1827
+
1828
+ ReactDOM.render(<Test />, container);
1829
+ Scheduler.unstable_flushAll();
1830
+
1831
+ let button = buttonRef.current;
1832
+ dispatchClickEvent(button);
1833
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1834
+
1835
+ ReactDOM.render(<Test extra={true} />, container);
1836
+ Scheduler.unstable_flushAll();
1837
+
1838
+ clickEvent.mockClear();
1839
+
1840
+ button = button2Ref.current;
1841
+ dispatchClickEvent(button);
1842
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1843
+ });
1844
+
1845
+ // @gate experimental
1846
+ it('should correctly work for a basic "click" listener that upgrades #2', () => {
1847
+ const clickEvent = jest.fn();
1848
+ const buttonRef = React.createRef();
1849
+ const button2Ref = React.createRef();
1850
+ const click = ReactDOM.unstable_createEventHandle('click', {
1851
+ passive: false,
1852
+ });
1853
+ const click2 = ReactDOM.unstable_createEventHandle('click', {
1854
+ passive: undefined,
1855
+ });
1856
+
1857
+ function Test2() {
1858
+ React.useEffect(() => {
1859
+ click.setListener(button2Ref.current, clickEvent);
1860
+ });
1861
+
1862
+ return <button ref={button2Ref}>Click me!</button>;
1863
+ }
1864
+
1865
+ function Test({extra}) {
1866
+ React.useEffect(() => {
1867
+ click2.setListener(buttonRef.current, clickEvent);
1868
+ });
1869
+
1870
+ return (
1871
+ <>
1872
+ <button ref={buttonRef}>Click me!</button>
1873
+ {extra && <Test2 />}
1874
+ </>
1875
+ );
1876
+ }
1877
+
1878
+ ReactDOM.render(<Test />, container);
1879
+ Scheduler.unstable_flushAll();
1880
+
1881
+ let button = buttonRef.current;
1882
+ dispatchClickEvent(button);
1883
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1884
+
1885
+ ReactDOM.render(<Test extra={true} />, container);
1886
+ Scheduler.unstable_flushAll();
1887
+
1888
+ clickEvent.mockClear();
1889
+
1890
+ button = button2Ref.current;
1891
+ dispatchClickEvent(button);
1892
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1893
+ });
1894
+
1895
+ // @gate experimental
1896
+ it('should correctly work for a basic "click" window listener', () => {
1897
+ const log = [];
1898
+ const clickEvent = jest.fn(event => {
1899
+ log.push({
1900
+ eventPhase: event.eventPhase,
1901
+ type: event.type,
1902
+ currentTarget: event.currentTarget,
1903
+ target: event.target,
1904
+ });
1905
+ });
1906
+ const click = ReactDOM.unstable_createEventHandle('click');
1907
+
1908
+ function Test() {
1909
+ React.useEffect(() => {
1910
+ click.setListener(window, clickEvent);
1911
+
1912
+ return () => {
1913
+ click.setListener(window, null);
1914
+ };
1915
+ });
1916
+
1917
+ return <button>Click anything!</button>;
1918
+ }
1919
+ ReactDOM.render(<Test />, container);
1920
+ Scheduler.unstable_flushAll();
1921
+
1922
+ expect(container.innerHTML).toBe(
1923
+ '<button>Click anything!</button>',
1924
+ );
1925
+
1926
+ // Clicking outside the button should trigger the event callback
1927
+ dispatchClickEvent(document.body);
1928
+ expect(log[0]).toEqual({
1929
+ eventPhase: 3,
1930
+ type: 'click',
1931
+ currentTarget: window,
1932
+ target: document.body,
1933
+ });
1934
+
1935
+ // Unmounting the container and clicking should not work
1936
+ ReactDOM.render(null, container);
1937
+ Scheduler.unstable_flushAll();
1938
+
1939
+ dispatchClickEvent(document.body);
1940
+ expect(clickEvent).toBeCalledTimes(1);
1941
+
1942
+ // Re-rendering and clicking the body should work again
1943
+ ReactDOM.render(<Test />, container);
1944
+ Scheduler.unstable_flushAll();
1945
+
1946
+ dispatchClickEvent(document.body);
1947
+ expect(clickEvent).toBeCalledTimes(2);
1948
+ });
1949
+
1950
+ // @gate experimental
1951
+ it('handle propagation of click events on the window', () => {
1952
+ const buttonRef = React.createRef();
1953
+ const divRef = React.createRef();
1954
+ const log = [];
1955
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
1956
+ const onClickCapture = jest.fn(e =>
1957
+ log.push(['capture', e.currentTarget]),
1958
+ );
1959
+ const click = ReactDOM.unstable_createEventHandle('click');
1960
+ const clickCapture = ReactDOM.unstable_createEventHandle('click', {
1961
+ capture: true,
1962
+ });
1963
+
1964
+ function Test() {
1965
+ React.useEffect(() => {
1966
+ click.setListener(window, onClick);
1967
+ clickCapture.setListener(window, onClickCapture);
1968
+ click.setListener(buttonRef.current, onClick);
1969
+ clickCapture.setListener(buttonRef.current, onClickCapture);
1970
+ click.setListener(divRef.current, onClick);
1971
+ clickCapture.setListener(divRef.current, onClickCapture);
1972
+
1973
+ return () => {
1974
+ click.setListener(window, null);
1975
+ clickCapture.setListener(window, null);
1976
+ };
1977
+ });
1978
+
1979
+ return (
1980
+ <button ref={buttonRef}>
1981
+ <div ref={divRef}>Click me!</div>
1982
+ </button>
1983
+ );
1984
+ }
1985
+
1986
+ ReactDOM.render(<Test />, container);
1987
+ Scheduler.unstable_flushAll();
1988
+
1989
+ const buttonElement = buttonRef.current;
1990
+ dispatchClickEvent(buttonElement);
1991
+ expect(onClick).toHaveBeenCalledTimes(2);
1992
+ expect(onClickCapture).toHaveBeenCalledTimes(2);
1993
+ expect(log[0]).toEqual(['capture', window]);
1994
+ expect(log[1]).toEqual(['capture', buttonElement]);
1995
+ expect(log[2]).toEqual(['bubble', buttonElement]);
1996
+ expect(log[3]).toEqual(['bubble', window]);
1997
+
1998
+ log.length = 0;
1999
+ onClick.mockClear();
2000
+ onClickCapture.mockClear();
2001
+
2002
+ const divElement = divRef.current;
2003
+ dispatchClickEvent(divElement);
2004
+ expect(onClick).toHaveBeenCalledTimes(3);
2005
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
2006
+ expect(log[0]).toEqual(['capture', window]);
2007
+ expect(log[1]).toEqual(['capture', buttonElement]);
2008
+ expect(log[2]).toEqual(['capture', divElement]);
2009
+ expect(log[3]).toEqual(['bubble', divElement]);
2010
+ expect(log[4]).toEqual(['bubble', buttonElement]);
2011
+ expect(log[5]).toEqual(['bubble', window]);
2012
+ });
2013
+
2014
+ // @gate experimental
2015
+ it('should correctly handle stopPropagation for mixed listeners', () => {
2016
+ const buttonRef = React.createRef();
2017
+ const rootListerner1 = jest.fn(e => e.stopPropagation());
2018
+ const rootListerner2 = jest.fn();
2019
+ const targetListerner1 = jest.fn();
2020
+ const targetListerner2 = jest.fn();
2021
+ const click1 = ReactDOM.unstable_createEventHandle('click', {
2022
+ capture: true,
2023
+ });
2024
+ const click2 = ReactDOM.unstable_createEventHandle('click', {
2025
+ capture: true,
2026
+ });
2027
+ const click3 = ReactDOM.unstable_createEventHandle('click');
2028
+ const click4 = ReactDOM.unstable_createEventHandle('click');
2029
+
2030
+ function Test() {
2031
+ React.useEffect(() => {
2032
+ click1.setListener(window, rootListerner1);
2033
+ click2.setListener(buttonRef.current, targetListerner1);
2034
+ click3.setListener(window, rootListerner2);
2035
+ click4.setListener(buttonRef.current, targetListerner2);
2036
+
2037
+ return () => {
2038
+ click1.setListener(window, null);
2039
+ click3.setListener(window, null);
2040
+ };
2041
+ });
2042
+
2043
+ return <button ref={buttonRef}>Click me!</button>;
2044
+ }
2045
+
2046
+ ReactDOM.render(<Test />, container);
2047
+ Scheduler.unstable_flushAll();
2048
+
2049
+ const buttonElement = buttonRef.current;
2050
+ dispatchClickEvent(buttonElement);
2051
+ expect(rootListerner1).toHaveBeenCalledTimes(1);
2052
+ expect(targetListerner1).toHaveBeenCalledTimes(0);
2053
+ expect(targetListerner2).toHaveBeenCalledTimes(0);
2054
+ expect(rootListerner2).toHaveBeenCalledTimes(0);
2055
+ });
2056
+
2057
+ // @gate experimental
2058
+ it('should correctly handle stopPropagation for delegated listeners', () => {
2059
+ const buttonRef = React.createRef();
2060
+ const rootListerner1 = jest.fn(e => e.stopPropagation());
2061
+ const rootListerner2 = jest.fn();
2062
+ const rootListerner3 = jest.fn(e => e.stopPropagation());
2063
+ const rootListerner4 = jest.fn();
2064
+ const click1 = ReactDOM.unstable_createEventHandle('click', {
2065
+ capture: true,
2066
+ });
2067
+ const click2 = ReactDOM.unstable_createEventHandle('click', {
2068
+ capture: true,
2069
+ });
2070
+ const click3 = ReactDOM.unstable_createEventHandle('click');
2071
+ const click4 = ReactDOM.unstable_createEventHandle('click');
2072
+
2073
+ function Test() {
2074
+ React.useEffect(() => {
2075
+ click1.setListener(window, rootListerner1);
2076
+ click2.setListener(window, rootListerner2);
2077
+ click3.setListener(window, rootListerner3);
2078
+ click4.setListener(window, rootListerner4);
2079
+
2080
+ return () => {
2081
+ click1.setListener(window, null);
2082
+ click2.setListener(window, null);
2083
+ click3.setListener(window, null);
2084
+ click4.setListener(window, null);
2085
+ };
2086
+ });
2087
+
2088
+ return <button ref={buttonRef}>Click me!</button>;
2089
+ }
2090
+
2091
+ ReactDOM.render(<Test />, container);
2092
+
2093
+ Scheduler.unstable_flushAll();
2094
+
2095
+ const buttonElement = buttonRef.current;
2096
+ dispatchClickEvent(buttonElement);
2097
+ expect(rootListerner1).toHaveBeenCalledTimes(1);
2098
+ expect(rootListerner2).toHaveBeenCalledTimes(1);
2099
+ expect(rootListerner3).toHaveBeenCalledTimes(0);
2100
+ expect(rootListerner4).toHaveBeenCalledTimes(0);
2101
+ });
2102
+
2103
+ // @gate experimental
2104
+ it('handle propagation of click events on the window and document', () => {
2105
+ const buttonRef = React.createRef();
2106
+ const divRef = React.createRef();
2107
+ const log = [];
2108
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
2109
+ const onClickCapture = jest.fn(e =>
2110
+ log.push(['capture', e.currentTarget]),
2111
+ );
2112
+ const click = ReactDOM.unstable_createEventHandle('click');
2113
+ const clickCapture = ReactDOM.unstable_createEventHandle('click', {
2114
+ capture: true,
2115
+ });
2116
+
2117
+ function Test() {
2118
+ React.useEffect(() => {
2119
+ click.setListener(window, onClick);
2120
+ clickCapture.setListener(window, onClickCapture);
2121
+ click.setListener(document, onClick);
2122
+ clickCapture.setListener(document, onClickCapture);
2123
+ click.setListener(buttonRef.current, onClick);
2124
+ clickCapture.setListener(buttonRef.current, onClickCapture);
2125
+ click.setListener(divRef.current, onClick);
2126
+ clickCapture.setListener(divRef.current, onClickCapture);
2127
+
2128
+ return () => {
2129
+ click.setListener(window, null);
2130
+ clickCapture.setListener(window, null);
2131
+ click.setListener(document, null);
2132
+ clickCapture.setListener(document, null);
2133
+ };
2134
+ });
2135
+
2136
+ return (
2137
+ <button ref={buttonRef}>
2138
+ <div ref={divRef}>Click me!</div>
2139
+ </button>
2140
+ );
2141
+ }
2142
+
2143
+ ReactDOM.render(<Test />, container);
2144
+ Scheduler.unstable_flushAll();
2145
+
2146
+ const buttonElement = buttonRef.current;
2147
+ dispatchClickEvent(buttonElement);
2148
+ expect(onClick).toHaveBeenCalledTimes(3);
2149
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
2150
+
2151
+ if (enableLegacyFBSupport) {
2152
+ expect(log[0]).toEqual(['capture', window]);
2153
+ expect(log[1]).toEqual(['capture', document]);
2154
+ expect(log[2]).toEqual(['bubble', document]);
2155
+ expect(log[3]).toEqual(['capture', buttonElement]);
2156
+ expect(log[4]).toEqual(['bubble', buttonElement]);
2157
+ expect(log[5]).toEqual(['bubble', window]);
2158
+ } else {
2159
+ expect(log[0]).toEqual(['capture', window]);
2160
+ expect(log[1]).toEqual(['capture', document]);
2161
+ expect(log[2]).toEqual(['capture', buttonElement]);
2162
+ expect(log[3]).toEqual(['bubble', buttonElement]);
2163
+ expect(log[4]).toEqual(['bubble', document]);
2164
+ expect(log[5]).toEqual(['bubble', window]);
2165
+ }
2166
+
2167
+ log.length = 0;
2168
+ onClick.mockClear();
2169
+ onClickCapture.mockClear();
2170
+
2171
+ const divElement = divRef.current;
2172
+ dispatchClickEvent(divElement);
2173
+ expect(onClick).toHaveBeenCalledTimes(4);
2174
+ expect(onClickCapture).toHaveBeenCalledTimes(4);
2175
+
2176
+ if (enableLegacyFBSupport) {
2177
+ expect(log[0]).toEqual(['capture', window]);
2178
+ expect(log[1]).toEqual(['capture', document]);
2179
+ expect(log[2]).toEqual(['bubble', document]);
2180
+ expect(log[3]).toEqual(['capture', buttonElement]);
2181
+ expect(log[4]).toEqual(['capture', divElement]);
2182
+ expect(log[5]).toEqual(['bubble', divElement]);
2183
+ expect(log[6]).toEqual(['bubble', buttonElement]);
2184
+ expect(log[7]).toEqual(['bubble', window]);
2185
+ } else {
2186
+ expect(log[0]).toEqual(['capture', window]);
2187
+ expect(log[1]).toEqual(['capture', document]);
2188
+ expect(log[2]).toEqual(['capture', buttonElement]);
2189
+ expect(log[3]).toEqual(['capture', divElement]);
2190
+ expect(log[4]).toEqual(['bubble', divElement]);
2191
+ expect(log[5]).toEqual(['bubble', buttonElement]);
2192
+ expect(log[6]).toEqual(['bubble', document]);
2193
+ expect(log[7]).toEqual(['bubble', window]);
2194
+ }
2195
+ });
2196
+
2197
+ // @gate experimental
2198
+ it('handles propagation of custom user events', () => {
2199
+ const buttonRef = React.createRef();
2200
+ const divRef = React.createRef();
2201
+ const log = [];
2202
+ const onCustomEvent = jest.fn(e =>
2203
+ log.push(['bubble', e.currentTarget]),
2204
+ );
2205
+ const onCustomEventCapture = jest.fn(e =>
2206
+ log.push(['capture', e.currentTarget]),
2207
+ );
2208
+
2209
+ let customEventHandle;
2210
+
2211
+ // Test that we get a warning when we don't provide an explicit priortiy
2212
+ expect(() => {
2213
+ customEventHandle = ReactDOM.unstable_createEventHandle(
2214
+ 'custom-event',
2215
+ );
2216
+ }).toWarnDev(
2217
+ 'Warning: The event "type" provided to createEventHandle() does not have a known priority type. ' +
2218
+ 'It is recommended to provide a "priority" option to specify a priority.',
2219
+ {withoutStack: true},
2220
+ );
2221
+
2222
+ customEventHandle = ReactDOM.unstable_createEventHandle(
2223
+ 'custom-event',
2224
+ {
2225
+ priority: 0, // Discrete
2226
+ },
2227
+ );
2228
+
2229
+ const customCaptureHandle = ReactDOM.unstable_createEventHandle(
2230
+ 'custom-event',
2231
+ {
2232
+ capture: true,
2233
+ priority: 0, // Discrete
2234
+ },
2235
+ );
2236
+
2237
+ function Test() {
2238
+ React.useEffect(() => {
2239
+ customEventHandle.setListener(buttonRef.current, onCustomEvent);
2240
+ customCaptureHandle.setListener(
2241
+ buttonRef.current,
2242
+ onCustomEventCapture,
2243
+ );
2244
+ customEventHandle.setListener(divRef.current, onCustomEvent);
2245
+ customCaptureHandle.setListener(
2246
+ divRef.current,
2247
+ onCustomEventCapture,
2248
+ );
2249
+ });
2250
+
2251
+ return (
2252
+ <button ref={buttonRef}>
2253
+ <div ref={divRef}>Click me!</div>
2254
+ </button>
2255
+ );
2256
+ }
2257
+
2258
+ ReactDOM.render(<Test />, container);
2259
+ Scheduler.unstable_flushAll();
2260
+
2261
+ const buttonElement = buttonRef.current;
2262
+ dispatchEvent(buttonElement, 'custom-event');
2263
+ expect(onCustomEvent).toHaveBeenCalledTimes(1);
2264
+ expect(onCustomEventCapture).toHaveBeenCalledTimes(1);
2265
+ expect(log[0]).toEqual(['capture', buttonElement]);
2266
+ expect(log[1]).toEqual(['bubble', buttonElement]);
2267
+
2268
+ const divElement = divRef.current;
2269
+ dispatchEvent(divElement, 'custom-event');
2270
+ expect(onCustomEvent).toHaveBeenCalledTimes(3);
2271
+ expect(onCustomEventCapture).toHaveBeenCalledTimes(3);
2272
+ expect(log[2]).toEqual(['capture', buttonElement]);
2273
+ expect(log[3]).toEqual(['capture', divElement]);
2274
+ expect(log[4]).toEqual(['bubble', divElement]);
2275
+ expect(log[5]).toEqual(['bubble', buttonElement]);
2276
+ });
2277
+
2278
+ // @gate experimental
2279
+ it('beforeblur and afterblur are called after a focused element is unmounted', () => {
2280
+ const log = [];
2281
+ // We have to persist here because we want to read relatedTarget later.
2282
+ const onAfterBlur = jest.fn(e => {
2283
+ e.persist();
2284
+ log.push(e.type);
2285
+ });
2286
+ const onBeforeBlur = jest.fn(e => log.push(e.type));
2287
+ const innerRef = React.createRef();
2288
+ const innerRef2 = React.createRef();
2289
+ const afterBlurHandle = ReactDOM.unstable_createEventHandle(
2290
+ 'afterblur',
2291
+ );
2292
+ const beforeBlurHandle = ReactDOM.unstable_createEventHandle(
2293
+ 'beforeblur',
2294
+ );
2295
+
2296
+ const Component = ({show}) => {
2297
+ const ref = React.useRef(null);
2298
+
2299
+ React.useEffect(() => {
2300
+ afterBlurHandle.setListener(document, onAfterBlur);
2301
+ beforeBlurHandle.setListener(ref.current, onBeforeBlur);
2302
+ });
2303
+
2304
+ return (
2305
+ <div ref={ref}>
2306
+ {show && <input ref={innerRef} />}
2307
+ <div ref={innerRef2} />
2308
+ </div>
2309
+ );
2310
+ };
2311
+
2312
+ ReactDOM.render(<Component show={true} />, container);
2313
+ Scheduler.unstable_flushAll();
2314
+
2315
+ const inner = innerRef.current;
2316
+ const target = createEventTarget(inner);
2317
+ target.focus();
2318
+ expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2319
+ expect(onAfterBlur).toHaveBeenCalledTimes(0);
2320
+
2321
+ ReactDOM.render(<Component show={false} />, container);
2322
+ Scheduler.unstable_flushAll();
2323
+
2324
+ expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2325
+ expect(onAfterBlur).toHaveBeenCalledTimes(1);
2326
+ expect(onAfterBlur).toHaveBeenCalledWith(
2327
+ expect.objectContaining({relatedTarget: inner}),
2328
+ );
2329
+ expect(log).toEqual(['beforeblur', 'afterblur']);
2330
+ });
2331
+
2332
+ // @gate experimental
2333
+ it('beforeblur and afterblur are called after a nested focused element is unmounted', () => {
2334
+ const log = [];
2335
+ // We have to persist here because we want to read relatedTarget later.
2336
+ const onAfterBlur = jest.fn(e => {
2337
+ e.persist();
2338
+ log.push(e.type);
2339
+ });
2340
+ const onBeforeBlur = jest.fn(e => log.push(e.type));
2341
+ const innerRef = React.createRef();
2342
+ const innerRef2 = React.createRef();
2343
+ const afterBlurHandle = ReactDOM.unstable_createEventHandle(
2344
+ 'afterblur',
2345
+ );
2346
+ const beforeBlurHandle = ReactDOM.unstable_createEventHandle(
2347
+ 'beforeblur',
2348
+ );
2349
+
2350
+ const Component = ({show}) => {
2351
+ const ref = React.useRef(null);
2352
+
2353
+ React.useEffect(() => {
2354
+ afterBlurHandle.setListener(document, onAfterBlur);
2355
+ beforeBlurHandle.setListener(ref.current, onBeforeBlur);
2356
+ });
2357
+
2358
+ return (
2359
+ <div ref={ref}>
2360
+ {show && (
2361
+ <div>
2362
+ <input ref={innerRef} />
2363
+ </div>
2364
+ )}
2365
+ <div ref={innerRef2} />
2366
+ </div>
2367
+ );
2368
+ };
2369
+
2370
+ ReactDOM.render(<Component show={true} />, container);
2371
+ Scheduler.unstable_flushAll();
2372
+
2373
+ const inner = innerRef.current;
2374
+ const target = createEventTarget(inner);
2375
+ target.focus();
2376
+ expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2377
+ expect(onAfterBlur).toHaveBeenCalledTimes(0);
2378
+
2379
+ ReactDOM.render(<Component show={false} />, container);
2380
+ Scheduler.unstable_flushAll();
2381
+
2382
+ expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2383
+ expect(onAfterBlur).toHaveBeenCalledTimes(1);
2384
+ expect(onAfterBlur).toHaveBeenCalledWith(
2385
+ expect.objectContaining({relatedTarget: inner}),
2386
+ );
2387
+ expect(log).toEqual(['beforeblur', 'afterblur']);
2388
+ });
2389
+
2390
+ // @gate experimental
2391
+ it('beforeblur and afterblur are called after a focused element is suspended', () => {
2392
+ const log = [];
2393
+ // We have to persist here because we want to read relatedTarget later.
2394
+ const onAfterBlur = jest.fn(e => {
2395
+ e.persist();
2396
+ log.push(e.type);
2397
+ });
2398
+ const onBeforeBlur = jest.fn(e => log.push(e.type));
2399
+ const innerRef = React.createRef();
2400
+ const Suspense = React.Suspense;
2401
+ let suspend = false;
2402
+ let resolve;
2403
+ const promise = new Promise(
2404
+ resolvePromise => (resolve = resolvePromise),
2405
+ );
2406
+ const afterBlurHandle = ReactDOM.unstable_createEventHandle(
2407
+ 'afterblur',
2408
+ );
2409
+ const beforeBlurHandle = ReactDOM.unstable_createEventHandle(
2410
+ 'beforeblur',
2411
+ );
2412
+
2413
+ function Child() {
2414
+ if (suspend) {
2415
+ throw promise;
2416
+ } else {
2417
+ return <input ref={innerRef} />;
2418
+ }
2419
+ }
2420
+
2421
+ const Component = () => {
2422
+ const ref = React.useRef(null);
2423
+
2424
+ React.useEffect(() => {
2425
+ afterBlurHandle.setListener(document, onAfterBlur);
2426
+ beforeBlurHandle.setListener(ref.current, onBeforeBlur);
2427
+ });
2428
+
2429
+ return (
2430
+ <div ref={ref}>
2431
+ <Suspense fallback="Loading...">
2432
+ <Child />
2433
+ </Suspense>
2434
+ </div>
2435
+ );
2436
+ };
2437
+
2438
+ const container2 = document.createElement('div');
2439
+ document.body.appendChild(container2);
2440
+
2441
+ const root = ReactDOM.createRoot(container2);
2442
+
2443
+ ReactTestUtils.act(() => {
2444
+ root.render(<Component />);
2445
+ });
2446
+ jest.runAllTimers();
2447
+
2448
+ const inner = innerRef.current;
2449
+ const target = createEventTarget(inner);
2450
+ target.focus();
2451
+ expect(onBeforeBlur).toHaveBeenCalledTimes(0);
2452
+ expect(onAfterBlur).toHaveBeenCalledTimes(0);
2453
+
2454
+ suspend = true;
2455
+ ReactTestUtils.act(() => {
2456
+ root.render(<Component />);
2457
+ });
2458
+ jest.runAllTimers();
2459
+
2460
+ expect(onBeforeBlur).toHaveBeenCalledTimes(1);
2461
+ expect(onAfterBlur).toHaveBeenCalledTimes(1);
2462
+ expect(onAfterBlur).toHaveBeenCalledWith(
2463
+ expect.objectContaining({relatedTarget: inner}),
2464
+ );
2465
+ resolve();
2466
+ expect(log).toEqual(['beforeblur', 'afterblur']);
2467
+
2468
+ document.body.removeChild(container2);
2469
+ });
2470
+
2471
+ describe('Compatibility with Scopes API', () => {
2472
+ beforeEach(() => {
2473
+ jest.resetModules();
2474
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
2475
+ ReactFeatureFlags.enableModernEventSystem = true;
2476
+ ReactFeatureFlags.enableCreateEventHandleAPI = true;
2477
+ ReactFeatureFlags.enableScopeAPI = true;
2478
+
2479
+ React = require('react');
2480
+ ReactDOM = require('react-dom');
2481
+ Scheduler = require('scheduler');
2482
+ ReactDOMServer = require('react-dom/server');
2483
+ });
2484
+
2485
+ // @gate experimental
2486
+ it('handle propagation of click events on a scope', () => {
2487
+ const buttonRef = React.createRef();
2488
+ const log = [];
2489
+ const onClick = jest.fn(e =>
2490
+ log.push(['bubble', e.currentTarget]),
2491
+ );
2492
+ const onClickCapture = jest.fn(e =>
2493
+ log.push(['capture', e.currentTarget]),
2494
+ );
2495
+ const TestScope = React.unstable_createScope();
2496
+ const click = ReactDOM.unstable_createEventHandle('click');
2497
+ const clickCapture = ReactDOM.unstable_createEventHandle(
2498
+ 'click',
2499
+ {
2500
+ capture: true,
2501
+ },
2502
+ );
2503
+
2504
+ function Test() {
2505
+ const scopeRef = React.useRef(null);
2506
+
2507
+ React.useEffect(() => {
2508
+ click.setListener(scopeRef.current, onClick);
2509
+ clickCapture.setListener(scopeRef.current, onClickCapture);
2510
+ });
2511
+
2512
+ return (
2513
+ <TestScope ref={scopeRef}>
2514
+ <button ref={buttonRef} />
2515
+ </TestScope>
2516
+ );
2517
+ }
2518
+
2519
+ ReactDOM.render(<Test />, container);
2520
+ Scheduler.unstable_flushAll();
2521
+
2522
+ const buttonElement = buttonRef.current;
2523
+ dispatchClickEvent(buttonElement);
2524
+
2525
+ expect(onClick).toHaveBeenCalledTimes(1);
2526
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
2527
+ expect(log).toEqual([
2528
+ ['capture', buttonElement],
2529
+ ['bubble', buttonElement],
2530
+ ]);
2531
+ });
2532
+
2533
+ // @gate experimental
2534
+ it('handle mixed propagation of click events on a scope', () => {
2535
+ const buttonRef = React.createRef();
2536
+ const divRef = React.createRef();
2537
+ const log = [];
2538
+ const onClick = jest.fn(e =>
2539
+ log.push(['bubble', e.currentTarget]),
2540
+ );
2541
+ const onClickCapture = jest.fn(e =>
2542
+ log.push(['capture', e.currentTarget]),
2543
+ );
2544
+ const TestScope = React.unstable_createScope();
2545
+ const click = ReactDOM.unstable_createEventHandle('click');
2546
+ const clickCapture = ReactDOM.unstable_createEventHandle(
2547
+ 'click',
2548
+ {
2549
+ capture: true,
2550
+ },
2551
+ );
2552
+
2553
+ function Test() {
2554
+ const scopeRef = React.useRef(null);
2555
+
2556
+ React.useEffect(() => {
2557
+ click.setListener(scopeRef.current, onClick);
2558
+ clickCapture.setListener(scopeRef.current, onClickCapture);
2559
+ click.setListener(buttonRef.current, onClick);
2560
+ clickCapture.setListener(buttonRef.current, onClickCapture);
2561
+ });
2562
+
2563
+ return (
2564
+ <TestScope ref={scopeRef}>
2565
+ <button ref={buttonRef}>
2566
+ <div
2567
+ ref={divRef}
2568
+ onClick={onClick}
2569
+ onClickCapture={onClickCapture}>
2570
+ Click me!
2571
+ </div>
2572
+ </button>
2573
+ </TestScope>
2574
+ );
2575
+ }
2576
+
2577
+ ReactDOM.render(<Test />, container);
2578
+ Scheduler.unstable_flushAll();
2579
+
2580
+ const buttonElement = buttonRef.current;
2581
+ dispatchClickEvent(buttonElement);
2582
+
2583
+ expect(onClick).toHaveBeenCalledTimes(2);
2584
+ expect(onClickCapture).toHaveBeenCalledTimes(2);
2585
+ expect(log).toEqual([
2586
+ ['capture', buttonElement],
2587
+ ['capture', buttonElement],
2588
+ ['bubble', buttonElement],
2589
+ ['bubble', buttonElement],
2590
+ ]);
2591
+
2592
+ log.length = 0;
2593
+ onClick.mockClear();
2594
+ onClickCapture.mockClear();
2595
+
2596
+ const divElement = divRef.current;
2597
+ dispatchClickEvent(divElement);
2598
+
2599
+ expect(onClick).toHaveBeenCalledTimes(3);
2600
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
2601
+ expect(log).toEqual([
2602
+ ['capture', buttonElement],
2603
+ ['capture', buttonElement],
2604
+ ['capture', divElement],
2605
+ ['bubble', divElement],
2606
+ ['bubble', buttonElement],
2607
+ ['bubble', buttonElement],
2608
+ ]);
2609
+ });
2610
+
2611
+ // @gate experimental
2612
+ it('should not handle the target being a dangling text node within a scope', () => {
2613
+ const clickEvent = jest.fn();
2614
+ const buttonRef = React.createRef();
2615
+ const TestScope = React.unstable_createScope();
2616
+ const click = ReactDOM.unstable_createEventHandle('click');
2617
+
2618
+ function Test() {
2619
+ const scopeRef = React.useRef(null);
2620
+
2621
+ React.useEffect(() => {
2622
+ click.setListener(scopeRef.current, clickEvent);
2623
+ });
2624
+
2625
+ return (
2626
+ <button ref={buttonRef}>
2627
+ <TestScope ref={scopeRef}>Click me!</TestScope>
2628
+ </button>
2629
+ );
2630
+ }
2631
+
2632
+ ReactDOM.render(<Test />, container);
2633
+ Scheduler.unstable_flushAll();
2634
+
2635
+ const textNode = buttonRef.current.firstChild;
2636
+ dispatchClickEvent(textNode);
2637
+ // This should not work, as the target instance will be the
2638
+ // <button>, which is actually outside the scope.
2639
+ expect(clickEvent).toBeCalledTimes(0);
2640
+ });
2641
+
2642
+ // @gate experimental
2643
+ it('handle stopPropagation (inner) correctly between scopes', () => {
2644
+ const buttonRef = React.createRef();
2645
+ const outerOnClick = jest.fn();
2646
+ const innerOnClick = jest.fn(e => e.stopPropagation());
2647
+ const TestScope = React.unstable_createScope();
2648
+ const TestScope2 = React.unstable_createScope();
2649
+ const click = ReactDOM.unstable_createEventHandle('click');
2650
+
2651
+ function Test() {
2652
+ const scopeRef = React.useRef(null);
2653
+ const scope2Ref = React.useRef(null);
2654
+
2655
+ React.useEffect(() => {
2656
+ click.setListener(scopeRef.current, outerOnClick);
2657
+ click.setListener(scope2Ref.current, innerOnClick);
2658
+ });
2659
+
2660
+ return (
2661
+ <TestScope ref={scopeRef}>
2662
+ <TestScope2 ref={scope2Ref}>
2663
+ <button ref={buttonRef} />
2664
+ </TestScope2>
2665
+ </TestScope>
2666
+ );
2667
+ }
2668
+
2669
+ ReactDOM.render(<Test />, container);
2670
+ Scheduler.unstable_flushAll();
2671
+
2672
+ const buttonElement = buttonRef.current;
2673
+ dispatchClickEvent(buttonElement);
2674
+
2675
+ expect(innerOnClick).toHaveBeenCalledTimes(1);
2676
+ expect(outerOnClick).toHaveBeenCalledTimes(0);
2677
+ });
2678
+
2679
+ // @gate experimental
2680
+ it('handle stopPropagation (outer) correctly between scopes', () => {
2681
+ const buttonRef = React.createRef();
2682
+ const outerOnClick = jest.fn(e => e.stopPropagation());
2683
+ const innerOnClick = jest.fn();
2684
+ const TestScope = React.unstable_createScope();
2685
+ const TestScope2 = React.unstable_createScope();
2686
+ const click = ReactDOM.unstable_createEventHandle('click');
2687
+
2688
+ function Test() {
2689
+ const scopeRef = React.useRef(null);
2690
+ const scope2Ref = React.useRef(null);
2691
+
2692
+ React.useEffect(() => {
2693
+ click.setListener(scopeRef.current, outerOnClick);
2694
+ click.setListener(scope2Ref.current, innerOnClick);
2695
+ });
2696
+
2697
+ return (
2698
+ <TestScope ref={scopeRef}>
2699
+ <TestScope2 ref={scope2Ref}>
2700
+ <button ref={buttonRef} />
2701
+ </TestScope2>
2702
+ </TestScope>
2703
+ );
2704
+ }
2705
+
2706
+ ReactDOM.render(<Test />, container);
2707
+ Scheduler.unstable_flushAll();
2708
+
2709
+ const buttonElement = buttonRef.current;
2710
+ dispatchClickEvent(buttonElement);
2711
+
2712
+ expect(innerOnClick).toHaveBeenCalledTimes(1);
2713
+ expect(outerOnClick).toHaveBeenCalledTimes(1);
2714
+ });
2715
+
2716
+ // @gate experimental
2717
+ it('handle stopPropagation (inner and outer) correctly between scopes', () => {
2718
+ const buttonRef = React.createRef();
2719
+ const onClick = jest.fn(e => e.stopPropagation());
2720
+ const TestScope = React.unstable_createScope();
2721
+ const TestScope2 = React.unstable_createScope();
2722
+ const click = ReactDOM.unstable_createEventHandle('click');
2723
+
2724
+ function Test() {
2725
+ const scopeRef = React.useRef(null);
2726
+ const scope2Ref = React.useRef(null);
2727
+
2728
+ React.useEffect(() => {
2729
+ click.setListener(scopeRef.current, onClick);
2730
+ click.setListener(scope2Ref.current, onClick);
2731
+ });
2732
+
2733
+ return (
2734
+ <TestScope ref={scopeRef}>
2735
+ <TestScope2 ref={scope2Ref}>
2736
+ <button ref={buttonRef} />
2737
+ </TestScope2>
2738
+ </TestScope>
2739
+ );
2740
+ }
2741
+
2742
+ ReactDOM.render(<Test />, container);
2743
+ Scheduler.unstable_flushAll();
2744
+
2745
+ const buttonElement = buttonRef.current;
2746
+ dispatchClickEvent(buttonElement);
2747
+
2748
+ expect(onClick).toHaveBeenCalledTimes(1);
2749
+ });
2750
+ });
2751
+ });
2752
},
2753
);
2754
}
packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js
+7
-3
@@ -25,7 +25,10 @@ import {
25
topLevelEventsToDispatchConfig,
26
simpleEventPluginEventTypes,
27
} from '../DOMEventProperties';
28
-import {accumulateTwoPhaseListeners} from '../DOMModernPluginEventSystem';
28
+import {
29
+ accumulateTwoPhaseListeners,
30
+ accumulateEventTargetListeners,
31
+} from '../DOMModernPluginEventSystem';
32
import {IS_TARGET_PHASE_ONLY} from '../EventSystemFlags';
33
34
import SyntheticAnimationEvent from '../SyntheticAnimationEvent';
@@ -210,10 +213,11 @@ const SimpleEventPlugin: ModernPluginModule<MouseEvent> = {
213
eventSystemFlags & IS_TARGET_PHASE_ONLY &&
214
targetContainer != null
215
) {
213
- // TODO: accumulateEventTargetListeners
216
+ accumulateEventTargetListeners(dispatchQueue, event, targetContainer);
217
} else {
215
- accumulateTwoPhaseListeners(targetInst, dispatchQueue, event);
218
+ accumulateTwoPhaseListeners(targetInst, dispatchQueue, event, true);
219
}
220
+ return event;
221
},
222
};
223
packages/react-interactions/events/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/create-event-handle/Focus';
packages/react-interactions/events/src/dom/create-event-handle/Focus.js
new
+439
@@ -0,0 +1,439 @@
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 useEvent from './useEvent';
12
+
13
+const {useEffect, useRef} = React;
14
+
15
+type UseFocusOptions = {|
16
+ disabled?: boolean,
17
+ onBlur?: (SyntheticEvent<EventTarget>) => void,
18
+ onFocus?: (SyntheticEvent<EventTarget>) => void,
19
+ onFocusChange?: boolean => void,
20
+ onFocusVisibleChange?: boolean => void,
21
+|};
22
+
23
+type UseFocusWithinOptions = {|
24
+ disabled?: boolean,
25
+ onAfterBlurWithin?: (SyntheticEvent<EventTarget>) => void,
26
+ onBeforeBlurWithin?: (SyntheticEvent<EventTarget>) => void,
27
+ onBlurWithin?: (SyntheticEvent<EventTarget>) => void,
28
+ onFocusWithin?: (SyntheticEvent<EventTarget>) => void,
29
+ onFocusWithinChange?: boolean => void,
30
+ onFocusWithinVisibleChange?: boolean => void,
31
+|};
32
+
33
+const isMac =
34
+ typeof window !== 'undefined' && window.navigator != null
35
+ ? /^Mac/.test(window.navigator.platform)
36
+ : false;
37
+
38
+const canUseDOM: boolean = !!(
39
+ typeof window !== 'undefined' &&
40
+ typeof window.document !== 'undefined' &&
41
+ typeof window.document.createElement !== 'undefined'
42
+);
43
+
44
+let passiveBrowserEventsSupported = false;
45
+
46
+// Check if browser support events with passive listeners
47
+// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support
48
+if (canUseDOM) {
49
+ try {
50
+ const options = {};
51
+ // $FlowFixMe: Ignore Flow complaining about needing a value
52
+ Object.defineProperty(options, 'passive', {
53
+ get: function() {
54
+ passiveBrowserEventsSupported = true;
55
+ },
56
+ });
57
+ window.addEventListener('test', options, options);
58
+ window.removeEventListener('test', options, options);
59
+ } catch (e) {
60
+ passiveBrowserEventsSupported = false;
61
+ }
62
+}
63
+
64
+const hasPointerEvents =
65
+ typeof window !== 'undefined' && window.PointerEvent != null;
66
+
67
+const globalFocusVisibleEvents = hasPointerEvents
68
+ ? ['keydown', 'pointermove', 'pointerdown', 'pointerup']
69
+ : ['keydown', 'mousedown', 'touchmove', 'touchstart', 'touchend'];
70
+
71
+// Global state for tracking focus visible and emulation of mouse
72
+let isGlobalFocusVisible = true;
73
+let hasTrackedGlobalFocusVisible = false;
74
+let isEmulatingMouseEvents = false;
75
+
76
+function trackGlobalFocusVisible() {
77
+ globalFocusVisibleEvents.forEach(type => {
78
+ window.addEventListener(
79
+ type,
80
+ handleGlobalFocusVisibleEvent,
81
+ passiveBrowserEventsSupported ? {capture: true, passive: true} : true,
82
+ );
83
+ });
84
+}
85
+
86
+function handleGlobalFocusVisibleEvent(
87
+ nativeEvent: MouseEvent | TouchEvent | KeyboardEvent,
88
+): void {
89
+ const {type} = nativeEvent;
90
+
91
+ switch (type) {
92
+ case 'pointermove':
93
+ case 'pointerdown':
94
+ case 'pointerup': {
95
+ isGlobalFocusVisible = false;
96
+ break;
97
+ }
98
+
99
+ case 'keydown': {
100
+ const {metaKey, altKey, ctrlKey} = nativeEvent;
101
+ const validKey = !(metaKey || (!isMac && altKey) || ctrlKey);
102
+
103
+ if (validKey) {
104
+ isGlobalFocusVisible = true;
105
+ }
106
+ break;
107
+ }
108
+
109
+ // fallbacks for no PointerEvent support
110
+ case 'touchmove':
111
+ case 'touchstart':
112
+ case 'touchend': {
113
+ isEmulatingMouseEvents = true;
114
+ isGlobalFocusVisible = false;
115
+ break;
116
+ }
117
+ case 'mousedown': {
118
+ if (!isEmulatingMouseEvents) {
119
+ isGlobalFocusVisible = false;
120
+ } else {
121
+ isEmulatingMouseEvents = false;
122
+ }
123
+ break;
124
+ }
125
+ }
126
+}
127
+
128
+const passiveObject = {passive: true};
129
+
130
+function handleFocusVisibleTargetEvent(
131
+ type: string,
132
+ focusTarget: EventTarget,
133
+ callback: boolean => void,
134
+): void {
135
+ isGlobalFocusVisible = false;
136
+
137
+ // Focus should stop being visible if a pointer is used on the element
138
+ // after it was focused using a keyboard.
139
+ if (
140
+ focusTarget !== null &&
141
+ (type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
142
+ ) {
143
+ callback(false);
144
+ }
145
+}
146
+
147
+function handleFocusVisibleTargetEvents(
148
+ event: SyntheticEvent<EventTarget>,
149
+ focusTarget,
150
+ callback,
151
+): void {
152
+ const {type} = event;
153
+
154
+ switch (type) {
155
+ case 'pointermove':
156
+ case 'pointerdown':
157
+ case 'pointerup': {
158
+ handleFocusVisibleTargetEvent(type, focusTarget, callback);
159
+ break;
160
+ }
161
+
162
+ case 'keydown':
163
+ case 'keyup': {
164
+ const {metaKey, altKey, ctrlKey} = (event: any);
165
+ const validKey = !(metaKey || (!isMac && altKey) || ctrlKey);
166
+
167
+ if (validKey) {
168
+ if (focusTarget !== null) {
169
+ callback(true);
170
+ }
171
+ }
172
+ break;
173
+ }
174
+
175
+ // fallbacks for no PointerEvent support
176
+ case 'touchmove':
177
+ case 'touchstart':
178
+ case 'touchend': {
179
+ handleFocusVisibleTargetEvent(type, focusTarget, callback);
180
+ break;
181
+ }
182
+ case 'mousedown': {
183
+ if (!isEmulatingMouseEvents) {
184
+ handleFocusVisibleTargetEvent(type, focusTarget, callback);
185
+ }
186
+ break;
187
+ }
188
+ }
189
+}
190
+
191
+function isRelatedTargetWithin(
192
+ focusWithinTarget: Node,
193
+ relatedTarget: null | Node,
194
+): boolean {
195
+ if (relatedTarget == null) {
196
+ return false;
197
+ }
198
+ // To support experimental scopes, which can be the target:
199
+ const containsNode = (focusWithinTarget: any).containsNode;
200
+ if (typeof containsNode === 'function') {
201
+ return containsNode(relatedTarget);
202
+ }
203
+ return focusWithinTarget.contains(relatedTarget);
204
+}
205
+
206
+function setFocusVisibleListeners(focusVisibleHandles, focusTarget, callback) {
207
+ focusVisibleHandles.forEach(focusVisibleHandle => {
208
+ focusVisibleHandle.setListener(focusTarget, event =>
209
+ handleFocusVisibleTargetEvents(event, focusTarget, callback),
210
+ );
211
+ });
212
+}
213
+
214
+function useFocusVisibleInputHandles() {
215
+ return [
216
+ useEvent('mousedown', passiveObject),
217
+ useEvent(hasPointerEvents ? 'pointerdown' : 'touchstart', passiveObject),
218
+ useEvent(hasPointerEvents ? 'pointermove' : 'touchmove', passiveObject),
219
+ useEvent(hasPointerEvents ? 'pointerup' : 'touchend', passiveObject),
220
+ useEvent('keydown', passiveObject),
221
+ ];
222
+}
223
+
224
+function useFocusLifecycles(stateRef) {
225
+ useEffect(() => {
226
+ if (!hasTrackedGlobalFocusVisible) {
227
+ hasTrackedGlobalFocusVisible = true;
228
+ trackGlobalFocusVisible();
229
+ }
230
+ }, []);
231
+}
232
+
233
+export function useFocus(
234
+ focusTargetRef: {current: null | Node},
235
+ {
236
+ disabled,
237
+ onBlur,
238
+ onFocus,
239
+ onFocusChange,
240
+ onFocusVisibleChange,
241
+ }: UseFocusOptions,
242
+): void {
243
+ // Setup controlled state for this useFocus hook
244
+ const stateRef = useRef({isFocused: false, isFocusVisible: false});
245
+ const focusHandle = useEvent('focus', passiveObject);
246
+ const blurHandle = useEvent('blur', passiveObject);
247
+ const focusVisibleHandles = useFocusVisibleInputHandles();
248
+
249
+ useEffect(() => {
250
+ const focusTarget = focusTargetRef.current;
251
+ const state = stateRef.current;
252
+
253
+ if (focusTarget !== null && state !== null) {
254
+ // Handle focus visible
255
+ setFocusVisibleListeners(
256
+ focusVisibleHandles,
257
+ focusTarget,
258
+ isFocusVisible => {
259
+ if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
260
+ state.isFocusVisible = isFocusVisible;
261
+ if (onFocusVisibleChange) {
262
+ onFocusVisibleChange(isFocusVisible);
263
+ }
264
+ }
265
+ },
266
+ );
267
+
268
+ // Handle focus
269
+ focusHandle.setListener(focusTarget, event => {
270
+ if (disabled) {
271
+ return;
272
+ }
273
+ // Limit focus events to the direct child of the event component.
274
+ // Browser focus is not expected to bubble.
275
+ if (!state.isFocused && focusTarget === event.target) {
276
+ state.isFocused = true;
277
+ state.isFocusVisible = isGlobalFocusVisible;
278
+ if (onFocus) {
279
+ onFocus(event);
280
+ }
281
+ if (onFocusChange) {
282
+ onFocusChange(true);
283
+ }
284
+ if (state.isFocusVisible && onFocusVisibleChange) {
285
+ onFocusVisibleChange(true);
286
+ }
287
+ isEmulatingMouseEvents = false;
288
+ }
289
+ });
290
+
291
+ // Handle blur
292
+ blurHandle.setListener(focusTarget, event => {
293
+ if (disabled) {
294
+ return;
295
+ }
296
+ if (state.isFocused) {
297
+ state.isFocused = false;
298
+ state.isFocusVisible = isGlobalFocusVisible;
299
+ if (onBlur) {
300
+ onBlur(event);
301
+ }
302
+ if (onFocusChange) {
303
+ onFocusChange(false);
304
+ }
305
+ if (state.isFocusVisible && onFocusVisibleChange) {
306
+ onFocusVisibleChange(false);
307
+ }
308
+ }
309
+ isEmulatingMouseEvents = false;
310
+ });
311
+ }
312
+ }, [disabled, onBlur, onFocus, onFocusChange, onFocusVisibleChange]);
313
+
314
+ // Mount/Unmount logic
315
+ useFocusLifecycles(stateRef);
316
+}
317
+
318
+export function useFocusWithin(
319
+ focusWithinTargetRef: {current: null | Node},
320
+ {
321
+ disabled,
322
+ onAfterBlurWithin,
323
+ onBeforeBlurWithin,
324
+ onBlurWithin,
325
+ onFocusWithin,
326
+ onFocusWithinChange,
327
+ onFocusWithinVisibleChange,
328
+ }: UseFocusWithinOptions,
329
+) {
330
+ // Setup controlled state for this useFocus hook
331
+ const stateRef = useRef({isFocused: false, isFocusVisible: false});
332
+ const focusHandle = useEvent('focus', passiveObject);
333
+ const blurHandle = useEvent('blur', passiveObject);
334
+ const afterBlurHandle = useEvent('afterblur', passiveObject);
335
+ const beforeBlurHandle = useEvent('beforeblur', passiveObject);
336
+ const focusVisibleHandles = useFocusVisibleInputHandles();
337
+
338
+ useEffect(() => {
339
+ const focusWithinTarget = focusWithinTargetRef.current;
340
+ const state = stateRef.current;
341
+
342
+ if (focusWithinTarget !== null && state !== null) {
343
+ // Handle focus visible
344
+ setFocusVisibleListeners(
345
+ focusVisibleHandles,
346
+ focusWithinTarget,
347
+ isFocusVisible => {
348
+ if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
349
+ state.isFocusVisible = isFocusVisible;
350
+ if (onFocusWithinVisibleChange) {
351
+ onFocusWithinVisibleChange(isFocusVisible);
352
+ }
353
+ }
354
+ },
355
+ );
356
+
357
+ // Handle focus
358
+ focusHandle.setListener(focusWithinTarget, event => {
359
+ if (disabled) {
360
+ return;
361
+ }
362
+ if (!state.isFocused) {
363
+ state.isFocused = true;
364
+ state.isFocusVisible = isGlobalFocusVisible;
365
+ if (onFocusWithinChange) {
366
+ onFocusWithinChange(true);
367
+ }
368
+ if (state.isFocusVisible && onFocusWithinVisibleChange) {
369
+ onFocusWithinVisibleChange(true);
370
+ }
371
+ }
372
+ if (!state.isFocusVisible && isGlobalFocusVisible) {
373
+ state.isFocusVisible = isGlobalFocusVisible;
374
+ if (onFocusWithinVisibleChange) {
375
+ onFocusWithinVisibleChange(true);
376
+ }
377
+ }
378
+ if (onFocusWithin) {
379
+ onFocusWithin(event);
380
+ }
381
+ isEmulatingMouseEvents = false;
382
+ });
383
+
384
+ // Handle blur
385
+ blurHandle.setListener(focusWithinTarget, event => {
386
+ if (disabled) {
387
+ return;
388
+ }
389
+ const {relatedTarget} = (event: any);
390
+
391
+ if (
392
+ state.isFocused &&
393
+ !isRelatedTargetWithin(focusWithinTarget, relatedTarget)
394
+ ) {
395
+ state.isFocused = false;
396
+ if (onFocusWithinChange) {
397
+ onFocusWithinChange(false);
398
+ }
399
+ if (state.isFocusVisible && onFocusWithinVisibleChange) {
400
+ onFocusWithinVisibleChange(false);
401
+ }
402
+ if (onBlurWithin) {
403
+ onBlurWithin(event);
404
+ }
405
+ }
406
+ isEmulatingMouseEvents = false;
407
+ });
408
+
409
+ // Handle before blur. This is a special
410
+ // React provided event.
411
+ beforeBlurHandle.setListener(focusWithinTarget, event => {
412
+ if (disabled) {
413
+ return;
414
+ }
415
+ if (onBeforeBlurWithin) {
416
+ onBeforeBlurWithin(event);
417
+ // Add an "afterblur" listener on document. This is a special
418
+ // React provided event.
419
+ afterBlurHandle.setListener(document, afterBlurEvent => {
420
+ if (onAfterBlurWithin) {
421
+ onAfterBlurWithin(afterBlurEvent);
422
+ }
423
+ // Clear listener on document
424
+ afterBlurHandle.setListener(document, null);
425
+ });
426
+ }
427
+ });
428
+ }
429
+ }, [
430
+ disabled,
431
+ onBlurWithin,
432
+ onFocusWithin,
433
+ onFocusWithinChange,
434
+ onFocusWithinVisibleChange,
435
+ ]);
436
+
437
+ // Mount/Unmount logic
438
+ useFocusLifecycles(stateRef);
439
+}
packages/react-interactions/events/src/dom/create-event-handle/__tests__/useFocus-test.internal.js
new
+327
@@ -0,0 +1,327 @@
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
+let Scheduler;
19
+
20
+function initializeModules(hasPointerEvents) {
21
+ setPointerEvent(hasPointerEvents);
22
+ jest.resetModules();
23
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
24
+ ReactFeatureFlags.enableModernEventSystem = true;
25
+ ReactFeatureFlags.enableCreateEventHandleAPI = true;
26
+ React = require('react');
27
+ ReactDOM = require('react-dom');
28
+ Scheduler = require('scheduler');
29
+
30
+ // TODO: This import throws outside of experimental mode. Figure out better
31
+ // strategy for gated imports.
32
+ if (__EXPERIMENTAL__) {
33
+ useFocus = require('react-interactions/events/focus').useFocus;
34
+ }
35
+}
36
+
37
+const forcePointerEvents = true;
38
+const table = [[forcePointerEvents], [!forcePointerEvents]];
39
+
40
+describe.each(table)(`useFocus`, hasPointerEvents => {
41
+ let container;
42
+
43
+ beforeEach(() => {
44
+ initializeModules(hasPointerEvents);
45
+ container = document.createElement('div');
46
+ document.body.appendChild(container);
47
+ });
48
+
49
+ afterEach(() => {
50
+ ReactDOM.render(null, container);
51
+ document.body.removeChild(container);
52
+ container = null;
53
+ });
54
+
55
+ describe('disabled', () => {
56
+ let onBlur, onFocus, ref;
57
+
58
+ const componentInit = () => {
59
+ onBlur = jest.fn();
60
+ onFocus = jest.fn();
61
+ ref = React.createRef();
62
+ const Component = () => {
63
+ useFocus(ref, {
64
+ disabled: true,
65
+ onBlur,
66
+ onFocus,
67
+ });
68
+ return <div ref={ref} />;
69
+ };
70
+ ReactDOM.render(<Component />, container);
71
+ Scheduler.unstable_flushAll();
72
+ };
73
+
74
+ // @gate experimental
75
+ it('does not call callbacks', () => {
76
+ componentInit();
77
+ const target = createEventTarget(ref.current);
78
+ target.focus();
79
+ target.blur();
80
+ expect(onFocus).not.toBeCalled();
81
+ expect(onBlur).not.toBeCalled();
82
+ });
83
+ });
84
+
85
+ describe('onBlur', () => {
86
+ let onBlur, ref;
87
+
88
+ const componentInit = () => {
89
+ onBlur = jest.fn();
90
+ ref = React.createRef();
91
+ const Component = () => {
92
+ useFocus(ref, {
93
+ onBlur,
94
+ });
95
+ return <div ref={ref} />;
96
+ };
97
+ ReactDOM.render(<Component />, container);
98
+ Scheduler.unstable_flushAll();
99
+ };
100
+
101
+ // @gate experimental
102
+ it('is called after "blur" event', () => {
103
+ componentInit();
104
+ const target = createEventTarget(ref.current);
105
+ target.focus();
106
+ target.blur();
107
+ expect(onBlur).toHaveBeenCalledTimes(1);
108
+ });
109
+ });
110
+
111
+ describe('onFocus', () => {
112
+ let onFocus, ref, innerRef;
113
+
114
+ const componentInit = () => {
115
+ onFocus = jest.fn();
116
+ ref = React.createRef();
117
+ innerRef = React.createRef();
118
+ const Component = () => {
119
+ useFocus(ref, {
120
+ onFocus,
121
+ });
122
+ return (
123
+ <div ref={ref}>
124
+ <a ref={innerRef} />
125
+ </div>
126
+ );
127
+ };
128
+ ReactDOM.render(<Component />, container);
129
+ Scheduler.unstable_flushAll();
130
+ };
131
+
132
+ // @gate experimental
133
+ it('is called after "focus" event', () => {
134
+ componentInit();
135
+ const target = createEventTarget(ref.current);
136
+ target.focus();
137
+ expect(onFocus).toHaveBeenCalledTimes(1);
138
+ });
139
+
140
+ // @gate experimental
141
+ it('is not called if descendants of target receive focus', () => {
142
+ componentInit();
143
+ const target = createEventTarget(innerRef.current);
144
+ target.focus();
145
+ expect(onFocus).not.toBeCalled();
146
+ });
147
+ });
148
+
149
+ describe('onFocusChange', () => {
150
+ let onFocusChange, ref, innerRef;
151
+
152
+ const componentInit = () => {
153
+ onFocusChange = jest.fn();
154
+ ref = React.createRef();
155
+ innerRef = React.createRef();
156
+ const Component = () => {
157
+ useFocus(ref, {
158
+ onFocusChange,
159
+ });
160
+ return (
161
+ <div ref={ref}>
162
+ <div ref={innerRef} />
163
+ </div>
164
+ );
165
+ };
166
+ ReactDOM.render(<Component />, container);
167
+ Scheduler.unstable_flushAll();
168
+ };
169
+
170
+ // @gate experimental
171
+ it('is called after "blur" and "focus" events', () => {
172
+ componentInit();
173
+ const target = createEventTarget(ref.current);
174
+ target.focus();
175
+ expect(onFocusChange).toHaveBeenCalledTimes(1);
176
+ expect(onFocusChange).toHaveBeenCalledWith(true);
177
+ target.blur();
178
+ expect(onFocusChange).toHaveBeenCalledTimes(2);
179
+ expect(onFocusChange).toHaveBeenCalledWith(false);
180
+ });
181
+
182
+ // @gate experimental
183
+ it('is not called after "blur" and "focus" events on descendants', () => {
184
+ componentInit();
185
+ const target = createEventTarget(innerRef.current);
186
+ target.focus();
187
+ expect(onFocusChange).toHaveBeenCalledTimes(0);
188
+ target.blur();
189
+ expect(onFocusChange).toHaveBeenCalledTimes(0);
190
+ });
191
+ });
192
+
193
+ describe('onFocusVisibleChange', () => {
194
+ let onFocusVisibleChange, ref, innerRef;
195
+
196
+ const componentInit = () => {
197
+ onFocusVisibleChange = jest.fn();
198
+ ref = React.createRef();
199
+ innerRef = React.createRef();
200
+ const Component = () => {
201
+ useFocus(ref, {
202
+ onFocusVisibleChange,
203
+ });
204
+ return (
205
+ <div ref={ref}>
206
+ <div ref={innerRef} />
207
+ </div>
208
+ );
209
+ };
210
+ ReactDOM.render(<Component />, container);
211
+ Scheduler.unstable_flushAll();
212
+ };
213
+
214
+ // @gate experimental
215
+ it('is called after "focus" and "blur" if keyboard navigation is active', () => {
216
+ componentInit();
217
+ const target = createEventTarget(ref.current);
218
+ const containerTarget = createEventTarget(container);
219
+ // use keyboard first
220
+ containerTarget.keydown({key: 'Tab'});
221
+ target.focus();
222
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
223
+ expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
224
+ target.blur({relatedTarget: container});
225
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
226
+ expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
227
+ });
228
+
229
+ // @gate experimental
230
+ it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
231
+ componentInit();
232
+ const target = createEventTarget(ref.current);
233
+ const containerTarget = createEventTarget(container);
234
+ // use keyboard first
235
+ containerTarget.keydown({key: 'Tab'});
236
+ target.focus();
237
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
238
+ expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
239
+ // then use pointer on the target, focus should no longer be visible
240
+ target.pointerdown();
241
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
242
+ expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
243
+ // onFocusVisibleChange should not be called again
244
+ target.blur({relatedTarget: container});
245
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
246
+ });
247
+
248
+ // @gate experimental
249
+ it('is not called after "focus" and "blur" events without keyboard', () => {
250
+ componentInit();
251
+ const target = createEventTarget(ref.current);
252
+ const containerTarget = createEventTarget(container);
253
+ target.pointerdown();
254
+ target.pointerup();
255
+ containerTarget.pointerdown();
256
+ target.blur({relatedTarget: container});
257
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
258
+ });
259
+
260
+ // @gate experimental
261
+ it('is not called after "blur" and "focus" events on descendants', () => {
262
+ componentInit();
263
+ const innerTarget = createEventTarget(innerRef.current);
264
+ const containerTarget = createEventTarget(container);
265
+ containerTarget.keydown({key: 'Tab'});
266
+ innerTarget.focus();
267
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
268
+ innerTarget.blur({relatedTarget: container});
269
+ expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
270
+ });
271
+ });
272
+
273
+ describe('nested Focus components', () => {
274
+ // @gate experimental
275
+ it('propagates events in the correct order', () => {
276
+ const events = [];
277
+ const innerRef = React.createRef();
278
+ const outerRef = React.createRef();
279
+ const createEventHandler = msg => () => {
280
+ events.push(msg);
281
+ };
282
+
283
+ const Inner = () => {
284
+ useFocus(innerRef, {
285
+ onBlur: createEventHandler('inner: onBlur'),
286
+ onFocus: createEventHandler('inner: onFocus'),
287
+ onFocusChange: createEventHandler('inner: onFocusChange'),
288
+ });
289
+ return <div ref={innerRef} />;
290
+ };
291
+
292
+ const Outer = () => {
293
+ useFocus(outerRef, {
294
+ onBlur: createEventHandler('outer: onBlur'),
295
+ onFocus: createEventHandler('outer: onFocus'),
296
+ onFocusChange: createEventHandler('outer: onFocusChange'),
297
+ });
298
+ return (
299
+ <div ref={outerRef}>
300
+ <Inner />
301
+ </div>
302
+ );
303
+ };
304
+
305
+ ReactDOM.render(<Outer />, container);
306
+ Scheduler.unstable_flushAll();
307
+
308
+ const innerTarget = createEventTarget(innerRef.current);
309
+ const outerTarget = createEventTarget(outerRef.current);
310
+
311
+ outerTarget.focus();
312
+ outerTarget.blur();
313
+ innerTarget.focus();
314
+ innerTarget.blur();
315
+ expect(events).toEqual([
316
+ 'outer: onFocus',
317
+ 'outer: onFocusChange',
318
+ 'outer: onBlur',
319
+ 'outer: onFocusChange',
320
+ 'inner: onFocus',
321
+ 'inner: onFocusChange',
322
+ 'inner: onBlur',
323
+ 'inner: onFocusChange',
324
+ ]);
325
+ });
326
+ });
327
+});
packages/react-interactions/events/src/dom/create-event-handle/__tests__/useFocusWithin-test.internal.js
new
+579
@@ -0,0 +1,579 @@
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
+let Scheduler;
21
+
22
+function initializeModules(hasPointerEvents) {
23
+ setPointerEvent(hasPointerEvents);
24
+ jest.resetModules();
25
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
26
+ ReactFeatureFlags.enableScopeAPI = true;
27
+ ReactFeatureFlags.enableModernEventSystem = true;
28
+ ReactFeatureFlags.enableCreateEventHandleAPI = true;
29
+ React = require('react');
30
+ ReactDOM = require('react-dom');
31
+ ReactTestRenderer = require('react-test-renderer');
32
+ Scheduler = require('scheduler');
33
+ act = ReactTestRenderer.act;
34
+
35
+ // TODO: This import throws outside of experimental mode. Figure out better
36
+ // strategy for gated imports.
37
+ if (__EXPERIMENTAL__) {
38
+ useFocusWithin = require('react-interactions/events/focus').useFocusWithin;
39
+ }
40
+}
41
+
42
+const forcePointerEvents = true;
43
+const table = [[forcePointerEvents], [!forcePointerEvents]];
44
+
45
+describe.each(table)(`useFocus`, hasPointerEvents => {
46
+ let container;
47
+ let container2;
48
+
49
+ beforeEach(() => {
50
+ initializeModules(hasPointerEvents);
51
+ container = document.createElement('div');
52
+ document.body.appendChild(container);
53
+ container2 = document.createElement('div');
54
+ document.body.appendChild(container2);
55
+ });
56
+
57
+ afterEach(() => {
58
+ ReactDOM.render(null, container);
59
+ document.body.removeChild(container);
60
+ document.body.removeChild(container2);
61
+ container = null;
62
+ container2 = null;
63
+ });
64
+
65
+ describe('disabled', () => {
66
+ let onFocusWithinChange, onFocusWithinVisibleChange, ref;
67
+
68
+ const componentInit = () => {
69
+ onFocusWithinChange = jest.fn();
70
+ onFocusWithinVisibleChange = jest.fn();
71
+ ref = React.createRef();
72
+ const Component = () => {
73
+ useFocusWithin(ref, {
74
+ disabled: true,
75
+ onFocusWithinChange,
76
+ onFocusWithinVisibleChange,
77
+ });
78
+ return <div ref={ref} />;
79
+ };
80
+ ReactDOM.render(<Component />, container);
81
+ Scheduler.unstable_flushAll();
82
+ };
83
+
84
+ // @gate experimental
85
+ it('prevents custom events being dispatched', () => {
86
+ componentInit();
87
+ const target = createEventTarget(ref.current);
88
+ target.focus();
89
+ target.blur();
90
+ expect(onFocusWithinChange).not.toBeCalled();
91
+ expect(onFocusWithinVisibleChange).not.toBeCalled();
92
+ });
93
+ });
94
+
95
+ describe('onFocusWithinChange', () => {
96
+ let onFocusWithinChange, ref, innerRef, innerRef2;
97
+
98
+ const Component = ({show}) => {
99
+ useFocusWithin(ref, {
100
+ onFocusWithinChange,
101
+ });
102
+ return (
103
+ <div ref={ref}>
104
+ {show && <input ref={innerRef} />}
105
+ <div ref={innerRef2} />
106
+ </div>
107
+ );
108
+ };
109
+
110
+ const componentInit = () => {
111
+ onFocusWithinChange = jest.fn();
112
+ ref = React.createRef();
113
+ innerRef = React.createRef();
114
+ innerRef2 = React.createRef();
115
+ ReactDOM.render(<Component show={true} />, container);
116
+ Scheduler.unstable_flushAll();
117
+ };
118
+
119
+ // @gate experimental
120
+ it('is called after "blur" and "focus" events on focus target', () => {
121
+ componentInit();
122
+ const target = createEventTarget(ref.current);
123
+ target.focus();
124
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
125
+ expect(onFocusWithinChange).toHaveBeenCalledWith(true);
126
+ target.blur({relatedTarget: container});
127
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
128
+ expect(onFocusWithinChange).toHaveBeenCalledWith(false);
129
+ });
130
+
131
+ // @gate experimental
132
+ it('is called after "blur" and "focus" events on descendants', () => {
133
+ componentInit();
134
+ const target = createEventTarget(innerRef.current);
135
+ target.focus();
136
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
137
+ expect(onFocusWithinChange).toHaveBeenCalledWith(true);
138
+ target.blur({relatedTarget: container});
139
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
140
+ expect(onFocusWithinChange).toHaveBeenCalledWith(false);
141
+ });
142
+
143
+ // @gate experimental
144
+ it('is only called once when focus moves within and outside the subtree', () => {
145
+ componentInit();
146
+ const node = ref.current;
147
+ const innerNode1 = innerRef.current;
148
+ const innerNode2 = innerRef.current;
149
+ const target = createEventTarget(node);
150
+ const innerTarget1 = createEventTarget(innerNode1);
151
+ const innerTarget2 = createEventTarget(innerNode2);
152
+
153
+ // focus shifts into subtree
154
+ innerTarget1.focus();
155
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
156
+ expect(onFocusWithinChange).toHaveBeenCalledWith(true);
157
+ // focus moves around subtree
158
+ innerTarget1.blur({relatedTarget: innerNode2});
159
+ innerTarget2.focus();
160
+ innerTarget2.blur({relatedTarget: node});
161
+ target.focus();
162
+ target.blur({relatedTarget: innerNode1});
163
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
164
+ // focus shifts outside subtree
165
+ innerTarget1.blur({relatedTarget: container});
166
+ expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
167
+ expect(onFocusWithinChange).toHaveBeenCalledWith(false);
168
+ });
169
+ });
170
+
171
+ describe('onFocusWithinVisibleChange', () => {
172
+ let onFocusWithinVisibleChange, ref, innerRef, innerRef2;
173
+
174
+ const Component = ({show}) => {
175
+ useFocusWithin(ref, {
176
+ onFocusWithinVisibleChange,
177
+ });
178
+ return (
179
+ <div ref={ref}>
180
+ {show && <input ref={innerRef} />}
181
+ <div ref={innerRef2} />
182
+ </div>
183
+ );
184
+ };
185
+
186
+ const componentInit = () => {
187
+ onFocusWithinVisibleChange = jest.fn();
188
+ ref = React.createRef();
189
+ innerRef = React.createRef();
190
+ innerRef2 = React.createRef();
191
+ ReactDOM.render(<Component show={true} />, container);
192
+ Scheduler.unstable_flushAll();
193
+ };
194
+
195
+ // @gate experimental
196
+ it('is called after "focus" and "blur" on focus target if keyboard was used', () => {
197
+ componentInit();
198
+ const target = createEventTarget(ref.current);
199
+ const containerTarget = createEventTarget(container);
200
+ // use keyboard first
201
+ containerTarget.keydown({key: 'Tab'});
202
+ target.focus();
203
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
204
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
205
+ target.blur({relatedTarget: container});
206
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
207
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
208
+ });
209
+
210
+ // @gate experimental
211
+ it('is called after "focus" and "blur" on descendants if keyboard was used', () => {
212
+ componentInit();
213
+ const innerTarget = createEventTarget(innerRef.current);
214
+ const containerTarget = createEventTarget(container);
215
+ // use keyboard first
216
+ containerTarget.keydown({key: 'Tab'});
217
+ innerTarget.focus();
218
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
219
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
220
+ innerTarget.blur({relatedTarget: container});
221
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
222
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
223
+ });
224
+
225
+ // @gate experimental
226
+ it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
227
+ componentInit();
228
+ const node = ref.current;
229
+ const innerNode1 = innerRef.current;
230
+ const innerNode2 = innerRef2.current;
231
+
232
+ const target = createEventTarget(node);
233
+ const innerTarget1 = createEventTarget(innerNode1);
234
+ const innerTarget2 = createEventTarget(innerNode2);
235
+ // use keyboard first
236
+ target.focus();
237
+ target.keydown({key: 'Tab'});
238
+ target.blur({relatedTarget: innerNode1});
239
+ innerTarget1.focus();
240
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
241
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
242
+ // then use pointer on the next target, focus should no longer be visible
243
+ innerTarget2.pointerdown();
244
+ innerTarget1.blur({relatedTarget: innerNode2});
245
+ innerTarget2.focus();
246
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
247
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
248
+ // then use keyboard again
249
+ innerTarget2.keydown({key: 'Tab', shiftKey: true});
250
+ innerTarget2.blur({relatedTarget: innerNode1});
251
+ innerTarget1.focus();
252
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(3);
253
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
254
+ // then use pointer on the target, focus should no longer be visible
255
+ innerTarget1.pointerdown();
256
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
257
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
258
+ // onFocusVisibleChange should not be called again
259
+ innerTarget1.blur({relatedTarget: container});
260
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
261
+ });
262
+
263
+ // @gate experimental
264
+ it('is not called after "focus" and "blur" events without keyboard', () => {
265
+ componentInit();
266
+ const innerTarget = createEventTarget(innerRef.current);
267
+ innerTarget.pointerdown();
268
+ innerTarget.pointerup();
269
+ innerTarget.blur({relatedTarget: container});
270
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(0);
271
+ });
272
+
273
+ // @gate experimental
274
+ it('is only called once when focus moves within and outside the subtree', () => {
275
+ componentInit();
276
+ const node = ref.current;
277
+ const innerNode1 = innerRef.current;
278
+ const innerNode2 = innerRef2.current;
279
+ const target = createEventTarget(node);
280
+ const innerTarget1 = createEventTarget(innerNode1);
281
+ const innerTarget2 = createEventTarget(innerNode2);
282
+
283
+ // focus shifts into subtree
284
+ innerTarget1.focus();
285
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
286
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
287
+ // focus moves around subtree
288
+ innerTarget1.blur({relatedTarget: innerNode2});
289
+ innerTarget2.focus();
290
+ innerTarget2.blur({relatedTarget: node});
291
+ target.focus();
292
+ target.blur({relatedTarget: innerNode1});
293
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
294
+ // focus shifts outside subtree
295
+ innerTarget1.blur({relatedTarget: container});
296
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
297
+ expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
298
+ });
299
+ });
300
+
301
+ describe('onBeforeBlurWithin', () => {
302
+ let onBeforeBlurWithin, onAfterBlurWithin, ref, innerRef, innerRef2;
303
+
304
+ beforeEach(() => {
305
+ onBeforeBlurWithin = jest.fn();
306
+ onAfterBlurWithin = jest.fn(e => {
307
+ e.persist();
308
+ });
309
+ ref = React.createRef();
310
+ innerRef = React.createRef();
311
+ innerRef2 = React.createRef();
312
+ });
313
+
314
+ // @gate experimental
315
+ it('is called after a focused element is unmounted', () => {
316
+ const Component = ({show}) => {
317
+ useFocusWithin(ref, {
318
+ onBeforeBlurWithin,
319
+ onAfterBlurWithin,
320
+ });
321
+ return (
322
+ <div ref={ref}>
323
+ {show && <input ref={innerRef} />}
324
+ <div ref={innerRef2} />
325
+ </div>
326
+ );
327
+ };
328
+
329
+ ReactDOM.render(<Component show={true} />, container);
330
+ Scheduler.unstable_flushAll();
331
+
332
+ const inner = innerRef.current;
333
+ const target = createEventTarget(inner);
334
+ target.keydown({key: 'Tab'});
335
+ target.focus();
336
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
337
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
338
+ ReactDOM.render(<Component show={false} />, container);
339
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
340
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
341
+ expect(onAfterBlurWithin).toHaveBeenCalledWith(
342
+ expect.objectContaining({relatedTarget: inner}),
343
+ );
344
+ });
345
+
346
+ // @gate experimental
347
+ it('is called after a nested focused element is unmounted', () => {
348
+ const Component = ({show}) => {
349
+ useFocusWithin(ref, {
350
+ onBeforeBlurWithin,
351
+ onAfterBlurWithin,
352
+ });
353
+ return (
354
+ <div ref={ref}>
355
+ {show && (
356
+ <div>
357
+ <input ref={innerRef} />
358
+ </div>
359
+ )}
360
+ <div ref={innerRef2} />
361
+ </div>
362
+ );
363
+ };
364
+
365
+ ReactDOM.render(<Component show={true} />, container);
366
+ Scheduler.unstable_flushAll();
367
+
368
+ const inner = innerRef.current;
369
+ const target = createEventTarget(inner);
370
+ target.keydown({key: 'Tab'});
371
+ target.focus();
372
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
373
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
374
+ ReactDOM.render(<Component show={false} />, container);
375
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
376
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
377
+ expect(onAfterBlurWithin).toHaveBeenCalledWith(
378
+ expect.objectContaining({relatedTarget: inner}),
379
+ );
380
+ });
381
+
382
+ // @gate experimental
383
+ it('is called after many elements are unmounted', () => {
384
+ const buttonRef = React.createRef();
385
+ const inputRef = React.createRef();
386
+
387
+ const Component = ({show}) => {
388
+ useFocusWithin(ref, {
389
+ onBeforeBlurWithin,
390
+ onAfterBlurWithin,
391
+ });
392
+ return (
393
+ <div ref={ref}>
394
+ {show && <button>Press me!</button>}
395
+ {show && <button>Press me!</button>}
396
+ {show && <input ref={inputRef} />}
397
+ {show && <button>Press me!</button>}
398
+ {!show && <button ref={buttonRef}>Press me!</button>}
399
+ {show && <button>Press me!</button>}
400
+ <button>Press me!</button>
401
+ <button>Press me!</button>
402
+ </div>
403
+ );
404
+ };
405
+
406
+ ReactDOM.render(<Component show={true} />, container);
407
+ Scheduler.unstable_flushAll();
408
+
409
+ inputRef.current.focus();
410
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
411
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
412
+ ReactDOM.render(<Component show={false} />, container);
413
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
414
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
415
+ });
416
+
417
+ // @gate experimental
418
+ it('is called after a nested focused element is unmounted (with scope query)', () => {
419
+ const TestScope = React.unstable_createScope();
420
+ const testScopeQuery = (type, props) => true;
421
+ let targetNodes;
422
+ let targetNode;
423
+
424
+ const Component = ({show}) => {
425
+ const scopeRef = React.useRef(null);
426
+ useFocusWithin(scopeRef, {
427
+ onBeforeBlurWithin(event) {
428
+ const scope = scopeRef.current;
429
+ targetNode = innerRef.current;
430
+ targetNodes = scope.DO_NOT_USE_queryAllNodes(testScopeQuery);
431
+ },
432
+ });
433
+
434
+ return (
435
+ <TestScope ref={scopeRef}>
436
+ {show && <input ref={innerRef} />}
437
+ </TestScope>
438
+ );
439
+ };
440
+
441
+ ReactDOM.render(<Component show={true} />, container);
442
+ Scheduler.unstable_flushAll();
443
+
444
+ const inner = innerRef.current;
445
+ const target = createEventTarget(inner);
446
+ target.keydown({key: 'Tab'});
447
+ target.focus();
448
+ ReactDOM.render(<Component show={false} />, container);
449
+ Scheduler.unstable_flushAll();
450
+ expect(targetNodes).toEqual([targetNode]);
451
+ });
452
+
453
+ // @gate experimental
454
+ it('is called after a focused suspended element is hidden', () => {
455
+ const Suspense = React.Suspense;
456
+ let suspend = false;
457
+ let resolve;
458
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
459
+
460
+ function Child() {
461
+ if (suspend) {
462
+ throw promise;
463
+ } else {
464
+ return <input ref={innerRef} />;
465
+ }
466
+ }
467
+
468
+ const Component = ({show}) => {
469
+ useFocusWithin(ref, {
470
+ onBeforeBlurWithin,
471
+ onAfterBlurWithin,
472
+ });
473
+
474
+ return (
475
+ <div ref={ref}>
476
+ <Suspense fallback="Loading...">
477
+ <Child />
478
+ </Suspense>
479
+ </div>
480
+ );
481
+ };
482
+
483
+ const root = ReactDOM.createRoot(container2);
484
+
485
+ act(() => {
486
+ root.render(<Component />);
487
+ });
488
+ jest.runAllTimers();
489
+ expect(container2.innerHTML).toBe('<div><input></div>');
490
+
491
+ const inner = innerRef.current;
492
+ const target = createEventTarget(inner);
493
+ target.keydown({key: 'Tab'});
494
+ target.focus();
495
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
496
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
497
+
498
+ suspend = true;
499
+ act(() => {
500
+ root.render(<Component />);
501
+ });
502
+ jest.runAllTimers();
503
+ expect(container2.innerHTML).toBe(
504
+ '<div><input style="display: none;">Loading...</div>',
505
+ );
506
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
507
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
508
+ resolve();
509
+ });
510
+
511
+ // @gate experimental
512
+ it('is called after a focused suspended element is hidden then shown', () => {
513
+ const Suspense = React.Suspense;
514
+ let suspend = false;
515
+ let resolve;
516
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
517
+ const buttonRef = React.createRef();
518
+
519
+ function Child() {
520
+ if (suspend) {
521
+ throw promise;
522
+ } else {
523
+ return <input ref={innerRef} />;
524
+ }
525
+ }
526
+
527
+ const Component = ({show}) => {
528
+ useFocusWithin(ref, {
529
+ onBeforeBlurWithin,
530
+ onAfterBlurWithin,
531
+ });
532
+
533
+ return (
534
+ <div ref={ref}>
535
+ <Suspense fallback={<button ref={buttonRef}>Loading...</button>}>
536
+ <Child />
537
+ </Suspense>
538
+ </div>
539
+ );
540
+ };
541
+
542
+ const root = ReactDOM.createRoot(container2);
543
+
544
+ act(() => {
545
+ root.render(<Component />);
546
+ });
547
+ jest.runAllTimers();
548
+
549
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
550
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
551
+
552
+ suspend = true;
553
+ act(() => {
554
+ root.render(<Component />);
555
+ });
556
+ jest.runAllTimers();
557
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
558
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
559
+
560
+ act(() => {
561
+ root.render(<Component />);
562
+ });
563
+ jest.runAllTimers();
564
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
565
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
566
+
567
+ buttonRef.current.focus();
568
+ suspend = false;
569
+ act(() => {
570
+ root.render(<Component />);
571
+ });
572
+ jest.runAllTimers();
573
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
574
+ expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
575
+
576
+ resolve();
577
+ });
578
+ });
579
+});
packages/react-interactions/events/src/dom/create-event-handle/useEvent.js
new
+49
@@ -0,0 +1,49 @@
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 {useEffect, useRef} = React;
14
+const {unstable_createEventHandle: createEventHandle} = ReactDOM;
15
+
16
+type UseEventHandle = {|
17
+ setListener: (
18
+ target: EventTarget,
19
+ null | ((SyntheticEvent<EventTarget>) => void),
20
+ ) => void,
21
+ clear: () => void,
22
+|};
23
+
24
+export default function useEvent(
25
+ event: string,
26
+ options?: {|
27
+ capture?: boolean,
28
+ passive?: boolean,
29
+ priority?: 0 | 1 | 2,
30
+ |},
31
+): UseEventHandle {
32
+ const handleRef = useRef(null);
33
+
34
+ if (handleRef.current == null) {
35
+ handleRef.current = createEventHandle(event, options);
36
+ }
37
+
38
+ useEffect(() => {
39
+ const handle = handleRef.current;
40
+ return () => {
41
+ if (handle !== null) {
42
+ handle.clear();
43
+ }
44
+ handleRef.current = null;
45
+ };
46
+ }, []);
47
+
48
+ return ((handleRef.current: any): UseEventHandle);
49
+}
packages/shared/forks/ReactFeatureFlags.www.js
+1
-1
@@ -58,7 +58,7 @@ export const disableModulePatternComponents = true;
58
59
export const enableDeprecatedFlareAPI = true;
60
61
-export const enableCreateEventHandleAPI = false;
61
+export const enableCreateEventHandleAPI = true;
62
63
export const enableFundamentalAPI = false;
64
scripts/error-codes/codes.json
+4
-1
@@ -363,5 +363,8 @@
363
"362": "Could not find React container within specified host subtree.",
364
"363": "Test selector API is not supported by this renderer.",
365
"364": "Invalid host root specified. Should be either a React container or a node with a testname attribute.",
366
- "365": "Invalid selector type %s specified."
366
+ "365": "Invalid selector type %s specified.",
367
+ "366": "ReactDOM.createEventHandle: setListener called on an target that did not have a corresponding root. This is likely a bug in React.",
368
+ "367": "ReactDOM.createEventHandle: setListener called on an element target that is not managed by React. Ensure React rendered the DOM element.",
369
+ "368": "ReactDOM.createEventHandle: setListener called on an invalid target. Provide a vaid EventTarget or an element managed by React."
370
}