Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ReactDOM.useEvent: Add support for experimental scopes API #18375

Merged
merged 2 commits into from
Mar 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
ReactProviderType,
ReactEventResponder,
ReactEventResponderListener,
ReactScopeMethods,
} from 'shared/ReactTypes';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {Hook, TimeoutConfig} from 'react-reconciler/src/ReactFiberHooks';
Expand Down Expand Up @@ -44,7 +45,10 @@ type HookLogEntry = {

type ReactDebugListenerMap = {|
clear: () => void,
setListener: (target: EventTarget, callback: ?(Event) => void) => void,
setListener: (
target: EventTarget | ReactScopeMethods,
callback: ?(Event) => void,
) => void,
|};

let hookLog: Array<HookLogEntry> = [];
Expand Down
37 changes: 24 additions & 13 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@

import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
import type {RootType} from './ReactDOMRoot';
import type {
ReactDOMEventResponder,
ReactDOMEventResponderInstance,
ReactDOMFundamentalComponentInstance,
ReactDOMListener,
ReactDOMListenerEvent,
ReactDOMListenerMap,
} from '../shared/ReactDOMTypes';
import type {ReactScopeMethods} from 'shared/ReactTypes';

import {
precacheFiberNode,
Expand Down Expand Up @@ -49,14 +58,6 @@ import {
} from '../shared/HTMLNodeType';
import dangerousStyleValue from '../shared/dangerousStyleValue';

import type {
ReactDOMEventResponder,
ReactDOMEventResponderInstance,
ReactDOMFundamentalComponentInstance,
ReactDOMListener,
ReactDOMListenerEvent,
ReactDOMListenerMap,
} from '../shared/ReactDOMTypes';
import {
mountEventResponder,
unmountEventResponder,
Expand All @@ -69,6 +70,7 @@ import {
enableDeprecatedFlareAPI,
enableFundamentalAPI,
enableUseEventAPI,
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
import {
Expand All @@ -79,10 +81,13 @@ import {
isManagedDOMElement,
isValidEventTarget,
listenToTopLevelEvent,
attachListenerToManagedDOMElement,
detachListenerFromManagedDOMElement,
attachListenerFromManagedDOMElement,
detachTargetEventListener,
attachTargetEventListener,
detachTargetEventListener,
isReactScope,
attachListenerToReactScope,
detachListenerFromReactScope,
} from '../events/DOMModernPluginEventSystem';
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
Expand Down Expand Up @@ -1159,7 +1164,9 @@ export function mountEventListener(listener: ReactDOMListener): void {
if (enableUseEventAPI) {
const {target} = listener;
if (isManagedDOMElement(target)) {
attachListenerFromManagedDOMElement(listener);
attachListenerToManagedDOMElement(listener);
} else if (enableScopeAPI && isReactScope(target)) {
attachListenerToReactScope(listener);
} else {
attachTargetEventListener(listener);
}
Expand All @@ -1171,20 +1178,24 @@ export function unmountEventListener(listener: ReactDOMListener): void {
const {target} = listener;
if (isManagedDOMElement(target)) {
detachListenerFromManagedDOMElement(listener);
} else if (enableScopeAPI && isReactScope(target)) {
detachListenerFromReactScope(listener);
} else {
detachTargetEventListener(listener);
}
}
}

export function validateEventListenerTarget(
target: EventTarget,
target: EventTarget | ReactScopeMethods,
listener: ?(Event) => void,
): boolean {
if (enableUseEventAPI) {
if (
target != null &&
(isManagedDOMElement(target) || isValidEventTarget(target))
(isManagedDOMElement(target) ||
isValidEventTarget(target) ||
isReactScope(target))
) {
if (listener == null || typeof listener === 'function') {
return true;
Expand Down
115 changes: 93 additions & 22 deletions packages/react-dom/src/events/DOMModernPluginEventSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
ElementListenerMapEntry,
} from '../events/DOMEventListenerMap';
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
import type {EventPriority} from 'shared/ReactTypes';
import type {EventPriority, ReactScopeMethods} from 'shared/ReactTypes';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {PluginModule} from 'legacy-events/PluginModuleType';
import type {
Expand Down Expand Up @@ -142,8 +142,11 @@ const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {

const isArray = Array.isArray;

// $FlowFixMe: Flow struggles with this pattern
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
// TODO: we should remove the FlowFixMes and the casting to figure out how to make
// these patterns work properly.
// $FlowFixMe: Flow struggles with this pattern, so we also have to cast it.
const PossiblyWeakMap = ((typeof WeakMap === 'function' ? WeakMap : Map): any);

// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
export const eventTargetEventListenerStore: WeakMap<
EventTarget,
Expand All @@ -153,6 +156,15 @@ export const eventTargetEventListenerStore: WeakMap<
>,
> = new PossiblyWeakMap();

// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
export const reactScopeListenerStore: WeakMap<
ReactScopeMethods,
Map<
DOMTopLevelEventType,
{bubbled: Set<ReactDOMListener>, captured: Set<ReactDOMListener>},
>,
> = new PossiblyWeakMap();

function dispatchEventsForPlugins(
topLevelType: DOMTopLevelEventType,
eventSystemFlags: EventSystemFlags,
Expand Down Expand Up @@ -306,12 +318,20 @@ function isMatchingRootContainer(
);
}

export function isManagedDOMElement(target: EventTarget): boolean {
export function isManagedDOMElement(
target: EventTarget | ReactScopeMethods,
): boolean {
return getClosestInstanceFromNode(((target: any): Node)) !== null;
}

export function isValidEventTarget(target: EventTarget): boolean {
return typeof target.addEventListener === 'function';
export function isValidEventTarget(
target: EventTarget | ReactScopeMethods,
): boolean {
return typeof (target: any).addEventListener === 'function';
}

export function isReactScope(target: EventTarget | ReactScopeMethods): boolean {
return typeof (target: any).getChildContextValues === 'function';
}

export function dispatchEventForPluginEventSystem(
Expand Down Expand Up @@ -446,18 +466,16 @@ function addEventTypeToDispatchConfig(type: DOMTopLevelEventType): void {
}
}

export function attachListenerFromManagedDOMElement(
export function attachListenerToManagedDOMElement(
listener: ReactDOMListener,
): void {
const {event, target} = listener;
const {passive, priority, type} = event;
const possibleManagedTarget = ((target: any): Element);
let containerEventTarget = target;
if (getClosestInstanceFromNode(possibleManagedTarget)) {
containerEventTarget = getNearestRootOrPortalContainer(
possibleManagedTarget,
);
}

const managedTargetElement = ((target: any): Element);
const containerEventTarget = getNearestRootOrPortalContainer(
managedTargetElement,
);
const listenerMap = getListenerMapForElement(containerEventTarget);
// Add the event listener to the target container (falling back to
// the target if we didn't find one).
Expand All @@ -469,11 +487,11 @@ export function attachListenerFromManagedDOMElement(
priority,
);
// Get the internal listeners Set from the target instance.
let listeners = getListenersFromTarget(target);
let listeners = getListenersFromTarget(managedTargetElement);
// If we don't have any listeners, then we need to init them.
if (listeners === null) {
listeners = new Set();
initListenersSet(target, listeners);
initListenersSet(managedTargetElement, listeners);
}
// Add our listener to the listeners Set.
listeners.add(listener);
Expand All @@ -485,8 +503,9 @@ export function detachListenerFromManagedDOMElement(
listener: ReactDOMListener,
): void {
const {target} = listener;
const managedTargetElement = ((target: any): Element);
// Get the internal listeners Set from the target instance.
const listeners = getListenersFromTarget(target);
const listeners = getListenersFromTarget(managedTargetElement);
if (listeners !== null) {
// Remove out listener from the listeners Set.
listeners.delete(listener);
Expand All @@ -496,13 +515,21 @@ export function detachListenerFromManagedDOMElement(
export function attachTargetEventListener(listener: ReactDOMListener): void {
const {event, target} = listener;
const {capture, passive, priority, type} = event;
const listenerMap = getListenerMapForElement(target);
const eventTarget = ((target: any): EventTarget);
const listenerMap = getListenerMapForElement(eventTarget);
// Add the event listener to the TargetEvent object.
listenToTopLevelEvent(type, target, listenerMap, passive, priority, capture);
let eventTypeMap = eventTargetEventListenerStore.get(target);
listenToTopLevelEvent(
type,
eventTarget,
listenerMap,
passive,
priority,
capture,
);
let eventTypeMap = eventTargetEventListenerStore.get(eventTarget);
if (eventTypeMap === undefined) {
eventTypeMap = new Map();
eventTargetEventListenerStore.set(target, eventTypeMap);
eventTargetEventListenerStore.set(eventTarget, eventTypeMap);
}
// Get the listeners by the event type
let listeners = eventTypeMap.get(type);
Expand All @@ -523,7 +550,51 @@ export function attachTargetEventListener(listener: ReactDOMListener): void {
export function detachTargetEventListener(listener: ReactDOMListener): void {
const {event, target} = listener;
const {capture, type} = event;
const eventTypeMap = eventTargetEventListenerStore.get(target);
const validEventTarget = ((target: any): EventTarget);
const eventTypeMap = eventTargetEventListenerStore.get(validEventTarget);
if (eventTypeMap !== undefined) {
const listeners = eventTypeMap.get(type);
if (listeners !== undefined) {
// Remove out listener from the listeners Set.
if (capture) {
listeners.captured.delete(listener);
} else {
listeners.bubbled.delete(listener);
}
}
}
}

export function attachListenerToReactScope(listener: ReactDOMListener): void {
const {event, target} = listener;
const {capture, type} = event;
const reactScope = ((target: any): ReactScopeMethods);
let eventTypeMap = reactScopeListenerStore.get(reactScope);
if (eventTypeMap === undefined) {
eventTypeMap = new Map();
reactScopeListenerStore.set(reactScope, eventTypeMap);
}
// Get the listeners by the event type
let listeners = eventTypeMap.get(type);
if (listeners === undefined) {
listeners = {captured: new Set(), bubbled: new Set()};
eventTypeMap.set(type, listeners);
}
// Add our listener to the listeners Set.
if (capture) {
listeners.captured.add(listener);
} else {
listeners.bubbled.add(listener);
}
// Finally, add the event to our known event types list.
addEventTypeToDispatchConfig(type);
}

export function detachListenerFromReactScope(listener: ReactDOMListener): void {
const {event, target} = listener;
const {capture, type} = event;
const reactScope = ((target: any): ReactScopeMethods);
const eventTypeMap = reactScopeListenerStore.get(reactScope);
if (eventTypeMap !== undefined) {
const listeners = eventTypeMap.get(type);
if (listeners !== undefined) {
Expand Down
Loading