ReactDOM.useEvent: revert and add guard for null stateNode (#18441)
Dominic Gannaway committed
Apr 1, 2020 at 12:45 UTC
dc3c6c9565ba1710c89c2072b6d809cee8abd40c
12 files changed
+429
-51
packages/react-debug-tools/src/ReactDebugHooks.js
+5
-1
@@ -15,6 +15,7 @@ import type {
15
ReactProviderType,
16
ReactEventResponder,
17
ReactEventResponderListener,
18
+ ReactScopeMethods,
19
} from 'shared/ReactTypes';
20
import type {Fiber} from 'react-reconciler/src/ReactFiber';
21
import type {Hook, TimeoutConfig} from 'react-reconciler/src/ReactFiberHooks';
@@ -44,7 +45,10 @@ type HookLogEntry = {
45
46
type ReactDebugListenerMap = {|
47
clear: () => void,
47
- setListener: (target: EventTarget, callback: ?(Event) => void) => void,
48
+ setListener: (
49
+ target: EventTarget | ReactScopeMethods,
50
+ callback: ?(Event) => void,
51
+ ) => void,
52
|};
53
54
let hookLog: Array<HookLogEntry> = [];
packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js
+43
@@ -11,6 +11,7 @@
11
'use strict';
12
13
let React;
14
+let ReactDOM;
15
let ReactDebugTools;
16
17
describe('ReactHooksInspection', () => {
@@ -18,7 +19,9 @@ describe('ReactHooksInspection', () => {
19
jest.resetModules();
20
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
21
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
22
+ ReactFeatureFlags.enableUseEventAPI = true;
23
React = require('react');
24
+ ReactDOM = require('react-dom');
25
ReactDebugTools = require('react-debug-tools');
26
});
27
@@ -47,4 +50,44 @@ describe('ReactHooksInspection', () => {
50
},
51
]);
52
});
53
+
54
+ it('should inspect a simple ReactDOM.useEvent hook', () => {
55
+ let clickHandle;
56
+ let ref;
57
+
58
+ const effect = () => {
59
+ clickHandle.setListener(ref.current, () => {});
60
+ };
61
+
62
+ function Foo(props) {
63
+ ref = React.useRef(null);
64
+ clickHandle = ReactDOM.unstable_useEvent('click');
65
+ React.useEffect(effect);
66
+ return <div ref={ref}>Hello world</div>;
67
+ }
68
+ let tree = ReactDebugTools.inspectHooks(Foo, {});
69
+ expect(tree).toEqual([
70
+ {
71
+ isStateEditable: false,
72
+ id: 0,
73
+ name: 'Ref',
74
+ subHooks: [],
75
+ value: null,
76
+ },
77
+ {
78
+ isStateEditable: false,
79
+ id: 1,
80
+ name: 'Event',
81
+ value: {capture: false, passive: undefined, priority: 0, type: 'click'},
82
+ subHooks: [],
83
+ },
84
+ {
85
+ isStateEditable: false,
86
+ id: 2,
87
+ name: 'Effect',
88
+ value: effect,
89
+ subHooks: [],
90
+ },
91
+ ]);
92
+ });
93
});
packages/react-dom/src/client/ReactDOMComponent.js
+4
@@ -1348,6 +1348,10 @@ export function listenToEventResponderEventTypes(
1348
const targetEventType = isPassive
1349
? eventType
1350
: eventType.substring(0, eventType.length - 7);
1351
+ // We don't listen to this as we actually emulate it in the host config
1352
+ if (targetEventType === 'beforeblur') {
1353
+ continue;
1354
+ }
1355
if (!listenerMap.has(eventKey)) {
1356
if (isPassive) {
1357
const activeKey = targetEventType + '_active';
packages/react-dom/src/client/ReactDOMHostConfig.js
+24
-13
@@ -9,6 +9,15 @@
9
10
import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
11
import type {RootType} from './ReactDOMRoot';
12
+import type {
13
+ ReactDOMEventResponder,
14
+ ReactDOMEventResponderInstance,
15
+ ReactDOMFundamentalComponentInstance,
16
+ ReactDOMListener,
17
+ ReactDOMListenerEvent,
18
+ ReactDOMListenerMap,
19
+} from '../shared/ReactDOMTypes';
20
+import type {ReactScopeMethods} from 'shared/ReactTypes';
21
22
import {
23
precacheFiberNode,
@@ -49,14 +58,6 @@ import {
58
} from '../shared/HTMLNodeType';
59
import dangerousStyleValue from '../shared/dangerousStyleValue';
60
52
-import type {
53
- ReactDOMEventResponder,
54
- ReactDOMEventResponderInstance,
55
- ReactDOMFundamentalComponentInstance,
56
- ReactDOMListener,
57
- ReactDOMListenerEvent,
58
- ReactDOMListenerMap,
59
-} from '../shared/ReactDOMTypes';
61
import {
62
mountEventResponder,
63
unmountEventResponder,
@@ -69,6 +70,7 @@ import {
70
enableDeprecatedFlareAPI,
71
enableFundamentalAPI,
72
enableUseEventAPI,
73
+ enableScopeAPI,
74
} from 'shared/ReactFeatureFlags';
75
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
76
import {
@@ -79,10 +81,13 @@ import {
81
isManagedDOMElement,
82
isValidEventTarget,
83
listenToTopLevelEvent,
84
+ attachListenerToManagedDOMElement,
85
detachListenerFromManagedDOMElement,
83
- attachListenerFromManagedDOMElement,
84
- detachTargetEventListener,
86
attachTargetEventListener,
87
+ detachTargetEventListener,
88
+ isReactScope,
89
+ attachListenerToReactScope,
90
+ detachListenerFromReactScope,
91
} from '../events/DOMModernPluginEventSystem';
92
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
93
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
@@ -1160,7 +1165,9 @@ export function mountEventListener(listener: ReactDOMListener): void {
1165
if (enableUseEventAPI) {
1166
const {target} = listener;
1167
if (isManagedDOMElement(target)) {
1163
- attachListenerFromManagedDOMElement(listener);
1168
+ attachListenerToManagedDOMElement(listener);
1169
+ } else if (enableScopeAPI && isReactScope(target)) {
1170
+ attachListenerToReactScope(listener);
1171
} else {
1172
attachTargetEventListener(listener);
1173
}
@@ -1172,6 +1179,8 @@ export function unmountEventListener(listener: ReactDOMListener): void {
1179
const {target} = listener;
1180
if (isManagedDOMElement(target)) {
1181
detachListenerFromManagedDOMElement(listener);
1182
+ } else if (enableScopeAPI && isReactScope(target)) {
1183
+ detachListenerFromReactScope(listener);
1184
} else {
1185
detachTargetEventListener(listener);
1186
}
@@ -1179,13 +1188,15 @@ export function unmountEventListener(listener: ReactDOMListener): void {
1188
}
1189
1190
export function validateEventListenerTarget(
1182
- target: EventTarget,
1191
+ target: EventTarget | ReactScopeMethods,
1192
listener: ?(Event) => void,
1193
): boolean {
1194
if (enableUseEventAPI) {
1195
if (
1196
target != null &&
1188
- (isManagedDOMElement(target) || isValidEventTarget(target))
1197
+ (isManagedDOMElement(target) ||
1198
+ isValidEventTarget(target) ||
1199
+ isReactScope(target))
1200
) {
1201
if (listener == null || typeof listener === 'function') {
1202
return true;
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+93
-22
@@ -14,7 +14,7 @@ import type {
14
ElementListenerMapEntry,
15
} from '../events/DOMEventListenerMap';
16
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
17
-import type {EventPriority} from 'shared/ReactTypes';
17
+import type {EventPriority, ReactScopeMethods} from 'shared/ReactTypes';
18
import type {Fiber} from 'react-reconciler/src/ReactFiber';
19
import type {PluginModule} from 'legacy-events/PluginModuleType';
20
import type {
@@ -142,8 +142,11 @@ const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
142
143
const isArray = Array.isArray;
144
145
-// $FlowFixMe: Flow struggles with this pattern
146
-const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
145
+// TODO: we should remove the FlowFixMes and the casting to figure out how to make
146
+// these patterns work properly.
147
+// $FlowFixMe: Flow struggles with this pattern, so we also have to cast it.
148
+const PossiblyWeakMap = ((typeof WeakMap === 'function' ? WeakMap : Map): any);
149
+
150
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
151
export const eventTargetEventListenerStore: WeakMap<
152
EventTarget,
@@ -153,6 +156,15 @@ export const eventTargetEventListenerStore: WeakMap<
156
>,
157
> = new PossiblyWeakMap();
158
159
+// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
160
+export const reactScopeListenerStore: WeakMap<
161
+ ReactScopeMethods,
162
+ Map<
163
+ DOMTopLevelEventType,
164
+ {bubbled: Set<ReactDOMListener>, captured: Set<ReactDOMListener>},
165
+ >,
166
+> = new PossiblyWeakMap();
167
+
168
function dispatchEventsForPlugins(
169
topLevelType: DOMTopLevelEventType,
170
eventSystemFlags: EventSystemFlags,
@@ -306,12 +318,20 @@ function isMatchingRootContainer(
318
);
319
}
320
309
-export function isManagedDOMElement(target: EventTarget): boolean {
321
+export function isManagedDOMElement(
322
+ target: EventTarget | ReactScopeMethods,
323
+): boolean {
324
return getClosestInstanceFromNode(((target: any): Node)) !== null;
325
}
326
313
-export function isValidEventTarget(target: EventTarget): boolean {
314
- return typeof target.addEventListener === 'function';
327
+export function isValidEventTarget(
328
+ target: EventTarget | ReactScopeMethods,
329
+): boolean {
330
+ return typeof (target: Object).addEventListener === 'function';
331
+}
332
+
333
+export function isReactScope(target: EventTarget | ReactScopeMethods): boolean {
334
+ return typeof (target: Object).getChildContextValues === 'function';
335
}
336
337
export function dispatchEventForPluginEventSystem(
@@ -446,18 +466,16 @@ function addEventTypeToDispatchConfig(type: DOMTopLevelEventType): void {
466
}
467
}
468
449
-export function attachListenerFromManagedDOMElement(
469
+export function attachListenerToManagedDOMElement(
470
listener: ReactDOMListener,
471
): void {
472
const {event, target} = listener;
473
const {passive, priority, type} = event;
454
- const possibleManagedTarget = ((target: any): Element);
455
- let containerEventTarget = target;
456
- if (getClosestInstanceFromNode(possibleManagedTarget)) {
457
- containerEventTarget = getNearestRootOrPortalContainer(
458
- possibleManagedTarget,
459
- );
460
- }
474
+
475
+ const managedTargetElement = ((target: any): Element);
476
+ const containerEventTarget = getNearestRootOrPortalContainer(
477
+ managedTargetElement,
478
+ );
479
const listenerMap = getListenerMapForElement(containerEventTarget);
480
// Add the event listener to the target container (falling back to
481
// the target if we didn't find one).
@@ -469,11 +487,11 @@ export function attachListenerFromManagedDOMElement(
487
priority,
488
);
489
// Get the internal listeners Set from the target instance.
472
- let listeners = getListenersFromTarget(target);
490
+ let listeners = getListenersFromTarget(managedTargetElement);
491
// If we don't have any listeners, then we need to init them.
492
if (listeners === null) {
493
listeners = new Set();
476
- initListenersSet(target, listeners);
494
+ initListenersSet(managedTargetElement, listeners);
495
}
496
// Add our listener to the listeners Set.
497
listeners.add(listener);
@@ -485,8 +503,9 @@ export function detachListenerFromManagedDOMElement(
503
listener: ReactDOMListener,
504
): void {
505
const {target} = listener;
506
+ const managedTargetElement = ((target: any): Element);
507
// Get the internal listeners Set from the target instance.
489
- const listeners = getListenersFromTarget(target);
508
+ const listeners = getListenersFromTarget(managedTargetElement);
509
if (listeners !== null) {
510
// Remove out listener from the listeners Set.
511
listeners.delete(listener);
@@ -496,13 +515,21 @@ export function detachListenerFromManagedDOMElement(
515
export function attachTargetEventListener(listener: ReactDOMListener): void {
516
const {event, target} = listener;
517
const {capture, passive, priority, type} = event;
499
- const listenerMap = getListenerMapForElement(target);
518
+ const eventTarget = ((target: any): EventTarget);
519
+ const listenerMap = getListenerMapForElement(eventTarget);
520
// Add the event listener to the TargetEvent object.
501
- listenToTopLevelEvent(type, target, listenerMap, passive, priority, capture);
502
- let eventTypeMap = eventTargetEventListenerStore.get(target);
521
+ listenToTopLevelEvent(
522
+ type,
523
+ eventTarget,
524
+ listenerMap,
525
+ passive,
526
+ priority,
527
+ capture,
528
+ );
529
+ let eventTypeMap = eventTargetEventListenerStore.get(eventTarget);
530
if (eventTypeMap === undefined) {
531
eventTypeMap = new Map();
505
- eventTargetEventListenerStore.set(target, eventTypeMap);
532
+ eventTargetEventListenerStore.set(eventTarget, eventTypeMap);
533
}
534
// Get the listeners by the event type
535
let listeners = eventTypeMap.get(type);
@@ -523,7 +550,51 @@ export function attachTargetEventListener(listener: ReactDOMListener): void {
550
export function detachTargetEventListener(listener: ReactDOMListener): void {
551
const {event, target} = listener;
552
const {capture, type} = event;
526
- const eventTypeMap = eventTargetEventListenerStore.get(target);
553
+ const validEventTarget = ((target: any): EventTarget);
554
+ const eventTypeMap = eventTargetEventListenerStore.get(validEventTarget);
555
+ if (eventTypeMap !== undefined) {
556
+ const listeners = eventTypeMap.get(type);
557
+ if (listeners !== undefined) {
558
+ // Remove out listener from the listeners Set.
559
+ if (capture) {
560
+ listeners.captured.delete(listener);
561
+ } else {
562
+ listeners.bubbled.delete(listener);
563
+ }
564
+ }
565
+ }
566
+}
567
+
568
+export function attachListenerToReactScope(listener: ReactDOMListener): void {
569
+ const {event, target} = listener;
570
+ const {capture, type} = event;
571
+ const reactScope = ((target: any): ReactScopeMethods);
572
+ let eventTypeMap = reactScopeListenerStore.get(reactScope);
573
+ if (eventTypeMap === undefined) {
574
+ eventTypeMap = new Map();
575
+ reactScopeListenerStore.set(reactScope, eventTypeMap);
576
+ }
577
+ // Get the listeners by the event type
578
+ let listeners = eventTypeMap.get(type);
579
+ if (listeners === undefined) {
580
+ listeners = {captured: new Set(), bubbled: new Set()};
581
+ eventTypeMap.set(type, listeners);
582
+ }
583
+ // Add our listener to the listeners Set.
584
+ if (capture) {
585
+ listeners.captured.add(listener);
586
+ } else {
587
+ listeners.bubbled.add(listener);
588
+ }
589
+ // Finally, add the event to our known event types list.
590
+ addEventTypeToDispatchConfig(type);
591
+}
592
+
593
+export function detachListenerFromReactScope(listener: ReactDOMListener): void {
594
+ const {event, target} = listener;
595
+ const {capture, type} = event;
596
+ const reactScope = ((target: any): ReactScopeMethods);
597
+ const eventTypeMap = reactScopeListenerStore.get(reactScope);
598
if (eventTypeMap !== undefined) {
599
const listeners = eventTypeMap.get(type);
600
if (listeners !== undefined) {
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+186
@@ -1317,6 +1317,28 @@ describe('DOMModernPluginEventSystem', () => {
1317
expect(clickEvent).toBeCalledTimes(0);
1318
});
1319
1320
+ it('should handle the target being a text node', () => {
1321
+ const clickEvent = jest.fn();
1322
+ const buttonRef = React.createRef();
1323
+
1324
+ function Test() {
1325
+ const click = ReactDOM.unstable_useEvent('click');
1326
+
1327
+ React.useEffect(() => {
1328
+ click.setListener(buttonRef.current, clickEvent);
1329
+ });
1330
+
1331
+ return <button ref={buttonRef}>Click me!</button>;
1332
+ }
1333
+
1334
+ ReactDOM.render(<Test />, container);
1335
+ Scheduler.unstable_flushAll();
1336
+
1337
+ let textNode = buttonRef.current.firstChild;
1338
+ dispatchClickEvent(textNode);
1339
+ expect(clickEvent).toBeCalledTimes(1);
1340
+ });
1341
+
1342
it('handle propagation of click events', () => {
1343
const buttonRef = React.createRef();
1344
const divRef = React.createRef();
@@ -2327,6 +2349,170 @@ describe('DOMModernPluginEventSystem', () => {
2349
document.body.removeChild(container2);
2350
},
2351
);
2352
+
2353
+ describe('Compatibility with Scopes API', () => {
2354
+ beforeEach(() => {
2355
+ jest.resetModules();
2356
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
2357
+ ReactFeatureFlags.enableModernEventSystem = true;
2358
+ ReactFeatureFlags.enableUseEventAPI = true;
2359
+ ReactFeatureFlags.enableScopeAPI = true;
2360
+
2361
+ React = require('react');
2362
+ ReactDOM = require('react-dom');
2363
+ Scheduler = require('scheduler');
2364
+ ReactDOMServer = require('react-dom/server');
2365
+ });
2366
+
2367
+ it('handle propagation of click events on a scope', () => {
2368
+ const buttonRef = React.createRef();
2369
+ const log = [];
2370
+ const onClick = jest.fn(e =>
2371
+ log.push(['bubble', e.currentTarget]),
2372
+ );
2373
+ const onClickCapture = jest.fn(e =>
2374
+ log.push(['capture', e.currentTarget]),
2375
+ );
2376
+ const TestScope = React.unstable_createScope();
2377
+
2378
+ function Test() {
2379
+ const click = ReactDOM.unstable_useEvent('click');
2380
+ const clickCapture = ReactDOM.unstable_useEvent('click', {
2381
+ capture: true,
2382
+ });
2383
+ const scopeRef = React.useRef(null);
2384
+
2385
+ React.useEffect(() => {
2386
+ click.setListener(scopeRef.current, onClick);
2387
+ clickCapture.setListener(scopeRef.current, onClickCapture);
2388
+ });
2389
+
2390
+ return (
2391
+ <TestScope ref={scopeRef}>
2392
+ <button ref={buttonRef} />
2393
+ </TestScope>
2394
+ );
2395
+ }
2396
+
2397
+ ReactDOM.render(<Test />, container);
2398
+ Scheduler.unstable_flushAll();
2399
+
2400
+ const buttonElement = buttonRef.current;
2401
+ dispatchClickEvent(buttonElement);
2402
+
2403
+ expect(onClick).toHaveBeenCalledTimes(1);
2404
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
2405
+ expect(log).toEqual([
2406
+ ['capture', buttonElement],
2407
+ ['bubble', buttonElement],
2408
+ ]);
2409
+ });
2410
+
2411
+ it('handle mixed propagation of click events on a scope', () => {
2412
+ const buttonRef = React.createRef();
2413
+ const divRef = React.createRef();
2414
+ const log = [];
2415
+ const onClick = jest.fn(e =>
2416
+ log.push(['bubble', e.currentTarget]),
2417
+ );
2418
+ const onClickCapture = jest.fn(e =>
2419
+ log.push(['capture', e.currentTarget]),
2420
+ );
2421
+ const TestScope = React.unstable_createScope();
2422
+
2423
+ function Test() {
2424
+ const click = ReactDOM.unstable_useEvent('click');
2425
+ const clickCapture = ReactDOM.unstable_useEvent('click', {
2426
+ capture: true,
2427
+ });
2428
+ const scopeRef = React.useRef(null);
2429
+
2430
+ React.useEffect(() => {
2431
+ click.setListener(scopeRef.current, onClick);
2432
+ clickCapture.setListener(scopeRef.current, onClickCapture);
2433
+ click.setListener(buttonRef.current, onClick);
2434
+ clickCapture.setListener(buttonRef.current, onClickCapture);
2435
+ });
2436
+
2437
+ return (
2438
+ <TestScope ref={scopeRef}>
2439
+ <button ref={buttonRef}>
2440
+ <div
2441
+ ref={divRef}
2442
+ onClick={onClick}
2443
+ onClickCapture={onClickCapture}>
2444
+ Click me!
2445
+ </div>
2446
+ </button>
2447
+ </TestScope>
2448
+ );
2449
+ }
2450
+
2451
+ ReactDOM.render(<Test />, container);
2452
+ Scheduler.unstable_flushAll();
2453
+
2454
+ const buttonElement = buttonRef.current;
2455
+ dispatchClickEvent(buttonElement);
2456
+
2457
+ expect(onClick).toHaveBeenCalledTimes(2);
2458
+ expect(onClickCapture).toHaveBeenCalledTimes(2);
2459
+ expect(log).toEqual([
2460
+ ['capture', buttonElement],
2461
+ ['capture', buttonElement],
2462
+ ['bubble', buttonElement],
2463
+ ['bubble', buttonElement],
2464
+ ]);
2465
+
2466
+ log.length = 0;
2467
+ onClick.mockClear();
2468
+ onClickCapture.mockClear();
2469
+
2470
+ const divElement = divRef.current;
2471
+ dispatchClickEvent(divElement);
2472
+
2473
+ expect(onClick).toHaveBeenCalledTimes(3);
2474
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
2475
+ expect(log).toEqual([
2476
+ ['capture', buttonElement],
2477
+ ['capture', buttonElement],
2478
+ ['capture', divElement],
2479
+ ['bubble', divElement],
2480
+ ['bubble', buttonElement],
2481
+ ['bubble', buttonElement],
2482
+ ]);
2483
+ });
2484
+
2485
+ it('should not handle the target being a dangling text node within a scope', () => {
2486
+ const clickEvent = jest.fn();
2487
+ const buttonRef = React.createRef();
2488
+
2489
+ const TestScope = React.unstable_createScope();
2490
+
2491
+ function Test() {
2492
+ const click = ReactDOM.unstable_useEvent('click');
2493
+ const scopeRef = React.useRef(null);
2494
+
2495
+ React.useEffect(() => {
2496
+ click.setListener(scopeRef.current, clickEvent);
2497
+ });
2498
+
2499
+ return (
2500
+ <button ref={buttonRef}>
2501
+ <TestScope ref={scopeRef}>Click me!</TestScope>
2502
+ </button>
2503
+ );
2504
+ }
2505
+
2506
+ ReactDOM.render(<Test />, container);
2507
+ Scheduler.unstable_flushAll();
2508
+
2509
+ let textNode = buttonRef.current.firstChild;
2510
+ dispatchClickEvent(textNode);
2511
+ // This should not work, as the target instance will be the
2512
+ // <button>, which is actually outside the scope.
2513
+ expect(clickEvent).toBeCalledTimes(0);
2514
+ });
2515
+ });
2516
});
2517
},
2518
);
packages/react-dom/src/events/accumulateTwoPhaseListeners.js
+55
-7
@@ -11,13 +11,23 @@ import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
11
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
12
import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
13
14
-import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
15
-import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
14
+import {
15
+ HostComponent,
16
+ ScopeComponent,
17
+} from 'react-reconciler/src/ReactWorkTags';
18
+import {
19
+ enableUseEventAPI,
20
+ enableScopeAPI,
21
+ enableModernEventSystem,
22
+} from 'shared/ReactFeatureFlags';
23
24
import getListener from 'legacy-events/getListener';
25
import {getListenersFromTarget} from '../client/ReactDOMComponentTree';
26
import {IS_TARGET_EVENT_ONLY} from 'legacy-events/EventSystemFlags';
20
-import {eventTargetEventListenerStore} from './DOMModernPluginEventSystem';
27
+import {
28
+ eventTargetEventListenerStore,
29
+ reactScopeListenerStore,
30
+} from './DOMModernPluginEventSystem';
31
32
export default function accumulateTwoPhaseListeners(
33
event: ReactSyntheticEvent,
@@ -76,15 +86,21 @@ export default function accumulateTwoPhaseListeners(
86
// usual two phase accumulation using the React fiber tree to pick up
87
// all relevant useEvent and on* prop events.
88
let node = event._targetInst;
89
+ let lastHostComponent = null;
90
91
// Accumulate all instances and listeners via the target -> root path.
92
while (node !== null) {
82
- // We only care for listeners that are on HostComponents (i.e. <div>)
83
- if (node.tag === HostComponent) {
93
+ const {stateNode: instance, tag} = node;
94
+ // Handle listeners that are on HostComponents (i.e. <div>)
95
+ if (instance !== null && tag === HostComponent) {
96
+ lastHostComponent = instance;
97
// For useEvent listenrs
85
- if (enableUseEventAPI && accumulateUseEventListeners) {
98
+ if (
99
+ enableModernEventSystem &&
100
+ enableUseEventAPI &&
101
+ accumulateUseEventListeners
102
+ ) {
103
// useEvent event listeners
87
- const instance = node.stateNode;
104
const targetType = event.type;
105
const listeners = getListenersFromTarget(instance);
106
@@ -128,6 +144,38 @@ export default function accumulateTwoPhaseListeners(
144
}
145
}
146
}
147
+ if (
148
+ enableModernEventSystem &&
149
+ enableUseEventAPI &&
150
+ enableScopeAPI &&
151
+ accumulateUseEventListeners &&
152
+ tag === ScopeComponent &&
153
+ lastHostComponent !== null
154
+ ) {
155
+ const reactScope = instance.methods;
156
+ const eventTypeMap = reactScopeListenerStore.get(reactScope);
157
+ if (eventTypeMap !== undefined) {
158
+ const type = ((event.type: any): DOMTopLevelEventType);
159
+ const listeners = eventTypeMap.get(type);
160
+ if (listeners !== undefined) {
161
+ const captureListeners = Array.from(listeners.captured);
162
+ const bubbleListeners = Array.from(listeners.bubbled);
163
+
164
+ for (let i = 0; i < captureListeners.length; i++) {
165
+ const listener = captureListeners[i];
166
+ const {callback} = listener;
167
+ dispatchListeners.unshift(callback);
168
+ dispatchInstances.unshift(((lastHostComponent: any): Element));
169
+ }
170
+ for (let i = 0; i < bubbleListeners.length; i++) {
171
+ const listener = bubbleListeners[i];
172
+ const {callback} = listener;
173
+ dispatchListeners.push(callback);
174
+ dispatchInstances.push(((lastHostComponent: any): Element));
175
+ }
176
+ }
177
+ }
178
+ }
179
node = node.return;
180
}
181
}
packages/react-dom/src/shared/ReactDOMTypes.js
+6
-2
@@ -12,6 +12,7 @@ import type {
12
ReactEventResponder,
13
ReactEventResponderInstance,
14
EventPriority,
15
+ ReactScopeMethods,
16
} from 'shared/ReactTypes';
17
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
18
@@ -86,12 +87,15 @@ export type ReactDOMListenerEvent = {|
87
88
export type ReactDOMListenerMap = {|
89
clear: () => void,
89
- setListener: (target: EventTarget, callback: ?(Event) => void) => void,
90
+ setListener: (
91
+ target: EventTarget | ReactScopeMethods,
92
+ callback: ?(Event) => void,
93
+ ) => void,
94
|};
95
96
export type ReactDOMListener = {|
97
callback: Event => void,
98
destroy: Node => void,
99
event: ReactDOMListenerEvent,
96
- target: EventTarget,
100
+ target: EventTarget | ReactScopeMethods,
101
|};
packages/react-reconciler/src/ReactFiberHooks.js
+10
-3
@@ -14,6 +14,7 @@ import type {
14
ReactEventResponder,
15
ReactContext,
16
ReactEventResponderListener,
17
+ ReactScopeMethods,
18
} from 'shared/ReactTypes';
19
import type {Fiber} from './ReactFiber';
20
import type {ExpirationTime} from './ReactFiberExpirationTime';
@@ -1651,7 +1652,7 @@ function validateNotInFunctionRender(): boolean {
1652
function createReactListener(
1653
event: ReactListenerEvent,
1654
callback: Event => void,
1654
- target: EventTarget,
1655
+ target: EventTarget | ReactScopeMethods,
1656
destroy: Node => void,
1657
): ReactListener {
1658
return {
@@ -1665,7 +1666,10 @@ function createReactListener(
1666
function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
1667
if (enableUseEventAPI) {
1668
const hook = mountWorkInProgressHook();
1668
- const listenerMap: Map<EventTarget, ReactListener> = new Map();
1669
+ const listenerMap: Map<
1670
+ EventTarget | ReactScopeMethods,
1671
+ ReactListener,
1672
+ > = new Map();
1673
const rootContainerInstance = getRootHostContainer();
1674
1675
// Register the event to the current root to ensure event
@@ -1700,7 +1704,10 @@ function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
1704
1705
const reactListenerMap: ReactListenerMap = {
1706
clear,
1703
- setListener(target: EventTarget, callback: ?(Event) => void): void {
1707
+ setListener(
1708
+ target: EventTarget | ReactScopeMethods,
1709
+ callback: ?(Event) => void,
1710
+ ): void {
1711
if (
1712
validateNotInFunctionRender() &&
1713
validateEventListenerTarget(target, callback)
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
-1
@@ -26,7 +26,7 @@ export const disableInputAttributeSyncing = false;
26
export const enableDeprecatedFlareAPI = true;
27
export const enableFundamentalAPI = false;
28
export const enableScopeAPI = true;
29
-export const enableUseEventAPI = false;
29
+export const enableUseEventAPI = true;
30
export const warnAboutUnmockedScheduler = true;
31
export const flushSuspenseFallbacksInTests = true;
32
export const enableSuspenseCallback = true;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
-1
@@ -26,7 +26,7 @@ export const enableSchedulerDebugging = false;
26
export const enableDeprecatedFlareAPI = true;
27
export const enableFundamentalAPI = false;
28
export const enableScopeAPI = true;
29
-export const enableUseEventAPI = false;
29
+export const enableUseEventAPI = true;
30
export const warnAboutUnmockedScheduler = true;
31
export const flushSuspenseFallbacksInTests = true;
32
export const enableSuspenseCallback = true;
packages/shared/forks/ReactFeatureFlags.www.js
+1
-1
@@ -55,7 +55,7 @@ export const enableFundamentalAPI = false;
55
56
export const enableScopeAPI = true;
57
58
-export const enableUseEventAPI = false;
58
+export const enableUseEventAPI = true;
59
60
export const warnAboutUnmockedScheduler = true;
61