@samitouri / QOS-React-2 / commits / 61f2a560e0

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 }