@samitouri / QOS-React-2 / commits / dc3c6c9565

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