Skip to content

Commit a16b349

Browse files
authored
ReactDOM.useEvent: Add support for experimental scopes API (facebook#18375)
* ReactDOM.useEvent: Add support for experimental scopes API
1 parent dbb060d commit a16b349

File tree

7 files changed

+307
-45
lines changed

7 files changed

+307
-45
lines changed

packages/react-debug-tools/src/ReactDebugHooks.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
ReactProviderType,
1616
ReactEventResponder,
1717
ReactEventResponderListener,
18+
ReactScopeMethods,
1819
} from 'shared/ReactTypes';
1920
import type {Fiber} from 'react-reconciler/src/ReactFiber';
2021
import type {Hook, TimeoutConfig} from 'react-reconciler/src/ReactFiberHooks';
@@ -44,7 +45,10 @@ type HookLogEntry = {
4445

4546
type ReactDebugListenerMap = {|
4647
clear: () => void,
47-
setListener: (target: EventTarget, callback: ?(Event) => void) => void,
48+
setListener: (
49+
target: EventTarget | ReactScopeMethods,
50+
callback: ?(Event) => void,
51+
) => void,
4852
|};
4953

5054
let hookLog: Array<HookLogEntry> = [];

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99

1010
import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
1111
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';
1221

1322
import {
1423
precacheFiberNode,
@@ -49,14 +58,6 @@ import {
4958
} from '../shared/HTMLNodeType';
5059
import dangerousStyleValue from '../shared/dangerousStyleValue';
5160

52-
import type {
53-
ReactDOMEventResponder,
54-
ReactDOMEventResponderInstance,
55-
ReactDOMFundamentalComponentInstance,
56-
ReactDOMListener,
57-
ReactDOMListenerEvent,
58-
ReactDOMListenerMap,
59-
} from '../shared/ReactDOMTypes';
6061
import {
6162
mountEventResponder,
6263
unmountEventResponder,
@@ -69,6 +70,7 @@ import {
6970
enableDeprecatedFlareAPI,
7071
enableFundamentalAPI,
7172
enableUseEventAPI,
73+
enableScopeAPI,
7274
} from 'shared/ReactFeatureFlags';
7375
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
7476
import {
@@ -79,10 +81,13 @@ import {
7981
isManagedDOMElement,
8082
isValidEventTarget,
8183
listenToTopLevelEvent,
84+
attachListenerToManagedDOMElement,
8285
detachListenerFromManagedDOMElement,
83-
attachListenerFromManagedDOMElement,
84-
detachTargetEventListener,
8586
attachTargetEventListener,
87+
detachTargetEventListener,
88+
isReactScope,
89+
attachListenerToReactScope,
90+
detachListenerFromReactScope,
8691
} from '../events/DOMModernPluginEventSystem';
8792
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
8893
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
@@ -1159,7 +1164,9 @@ export function mountEventListener(listener: ReactDOMListener): void {
11591164
if (enableUseEventAPI) {
11601165
const {target} = listener;
11611166
if (isManagedDOMElement(target)) {
1162-
attachListenerFromManagedDOMElement(listener);
1167+
attachListenerToManagedDOMElement(listener);
1168+
} else if (enableScopeAPI && isReactScope(target)) {
1169+
attachListenerToReactScope(listener);
11631170
} else {
11641171
attachTargetEventListener(listener);
11651172
}
@@ -1171,20 +1178,24 @@ export function unmountEventListener(listener: ReactDOMListener): void {
11711178
const {target} = listener;
11721179
if (isManagedDOMElement(target)) {
11731180
detachListenerFromManagedDOMElement(listener);
1181+
} else if (enableScopeAPI && isReactScope(target)) {
1182+
detachListenerFromReactScope(listener);
11741183
} else {
11751184
detachTargetEventListener(listener);
11761185
}
11771186
}
11781187
}
11791188

11801189
export function validateEventListenerTarget(
1181-
target: EventTarget,
1190+
target: EventTarget | ReactScopeMethods,
11821191
listener: ?(Event) => void,
11831192
): boolean {
11841193
if (enableUseEventAPI) {
11851194
if (
11861195
target != null &&
1187-
(isManagedDOMElement(target) || isValidEventTarget(target))
1196+
(isManagedDOMElement(target) ||
1197+
isValidEventTarget(target) ||
1198+
isReactScope(target))
11881199
) {
11891200
if (listener == null || typeof listener === 'function') {
11901201
return true;

packages/react-dom/src/events/DOMModernPluginEventSystem.js

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
ElementListenerMapEntry,
1515
} from '../events/DOMEventListenerMap';
1616
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
17-
import type {EventPriority} from 'shared/ReactTypes';
17+
import type {EventPriority, ReactScopeMethods} from 'shared/ReactTypes';
1818
import type {Fiber} from 'react-reconciler/src/ReactFiber';
1919
import type {PluginModule} from 'legacy-events/PluginModuleType';
2020
import type {
@@ -142,8 +142,11 @@ const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
142142

143143
const isArray = Array.isArray;
144144

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+
147150
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
148151
export const eventTargetEventListenerStore: WeakMap<
149152
EventTarget,
@@ -153,6 +156,15 @@ export const eventTargetEventListenerStore: WeakMap<
153156
>,
154157
> = new PossiblyWeakMap();
155158

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+
156168
function dispatchEventsForPlugins(
157169
topLevelType: DOMTopLevelEventType,
158170
eventSystemFlags: EventSystemFlags,
@@ -306,12 +318,20 @@ function isMatchingRootContainer(
306318
);
307319
}
308320

309-
export function isManagedDOMElement(target: EventTarget): boolean {
321+
export function isManagedDOMElement(
322+
target: EventTarget | ReactScopeMethods,
323+
): boolean {
310324
return getClosestInstanceFromNode(((target: any): Node)) !== null;
311325
}
312326

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: any).addEventListener === 'function';
331+
}
332+
333+
export function isReactScope(target: EventTarget | ReactScopeMethods): boolean {
334+
return typeof (target: any).getChildContextValues === 'function';
315335
}
316336

317337
export function dispatchEventForPluginEventSystem(
@@ -446,18 +466,16 @@ function addEventTypeToDispatchConfig(type: DOMTopLevelEventType): void {
446466
}
447467
}
448468

449-
export function attachListenerFromManagedDOMElement(
469+
export function attachListenerToManagedDOMElement(
450470
listener: ReactDOMListener,
451471
): void {
452472
const {event, target} = listener;
453473
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+
);
461479
const listenerMap = getListenerMapForElement(containerEventTarget);
462480
// Add the event listener to the target container (falling back to
463481
// the target if we didn't find one).
@@ -469,11 +487,11 @@ export function attachListenerFromManagedDOMElement(
469487
priority,
470488
);
471489
// Get the internal listeners Set from the target instance.
472-
let listeners = getListenersFromTarget(target);
490+
let listeners = getListenersFromTarget(managedTargetElement);
473491
// If we don't have any listeners, then we need to init them.
474492
if (listeners === null) {
475493
listeners = new Set();
476-
initListenersSet(target, listeners);
494+
initListenersSet(managedTargetElement, listeners);
477495
}
478496
// Add our listener to the listeners Set.
479497
listeners.add(listener);
@@ -485,8 +503,9 @@ export function detachListenerFromManagedDOMElement(
485503
listener: ReactDOMListener,
486504
): void {
487505
const {target} = listener;
506+
const managedTargetElement = ((target: any): Element);
488507
// Get the internal listeners Set from the target instance.
489-
const listeners = getListenersFromTarget(target);
508+
const listeners = getListenersFromTarget(managedTargetElement);
490509
if (listeners !== null) {
491510
// Remove out listener from the listeners Set.
492511
listeners.delete(listener);
@@ -496,13 +515,21 @@ export function detachListenerFromManagedDOMElement(
496515
export function attachTargetEventListener(listener: ReactDOMListener): void {
497516
const {event, target} = listener;
498517
const {capture, passive, priority, type} = event;
499-
const listenerMap = getListenerMapForElement(target);
518+
const eventTarget = ((target: any): EventTarget);
519+
const listenerMap = getListenerMapForElement(eventTarget);
500520
// 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);
503530
if (eventTypeMap === undefined) {
504531
eventTypeMap = new Map();
505-
eventTargetEventListenerStore.set(target, eventTypeMap);
532+
eventTargetEventListenerStore.set(eventTarget, eventTypeMap);
506533
}
507534
// Get the listeners by the event type
508535
let listeners = eventTypeMap.get(type);
@@ -523,7 +550,51 @@ export function attachTargetEventListener(listener: ReactDOMListener): void {
523550
export function detachTargetEventListener(listener: ReactDOMListener): void {
524551
const {event, target} = listener;
525552
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);
527598
if (eventTypeMap !== undefined) {
528599
const listeners = eventTypeMap.get(type);
529600
if (listeners !== undefined) {

0 commit comments

Comments
 (0)