Skip to content

Commit

Permalink
More event system cleanup and scaffolding (#18179)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm committed Mar 2, 2020
1 parent 8ccfce4 commit 62861bb
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 134 deletions.
2 changes: 1 addition & 1 deletion packages/legacy-events/PluginModuleType.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export type PluginModule<NativeEvent> = {
nativeTarget: NativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
container?: Document | Element | Node,
container?: Document | Element,
) => ?ReactSyntheticEvent,
tapMoveThreshold?: number,
};
140 changes: 103 additions & 37 deletions packages/react-dom/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import {registrationNameModules} from 'legacy-events/EventPluginRegistry';
import {canUseDOM} from 'shared/ExecutionEnvironment';
import endsWith from 'shared/endsWith';
import invariant from 'shared/invariant';
import {setListenToResponderEventTypes} from '../events/DeprecatedDOMEventResponderSystem';

import {
Expand Down Expand Up @@ -59,7 +60,6 @@ import {getListenerMapForElement} from '../events/DOMEventListenerMap';
import {
addResponderEventSystemEvent,
removeActiveResponderEventSystemEvent,
trapBubbledEvent,
} from '../events/ReactDOMEventListener.js';
import {mediaEventTypes} from '../events/DOMTopLevelEventTypes';
import {
Expand All @@ -74,7 +74,12 @@ import {
shouldRemoveAttribute,
} from '../shared/DOMProperty';
import assertValidProps from '../shared/assertValidProps';
import {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/HTMLNodeType';
import {
DOCUMENT_NODE,
DOCUMENT_FRAGMENT_NODE,
ELEMENT_NODE,
COMMENT_NODE,
} from '../shared/HTMLNodeType';
import isCustomComponent from '../shared/isCustomComponent';
import possibleStandardNames from '../shared/possibleStandardNames';
import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
Expand All @@ -84,8 +89,13 @@ import {validateProperties as validateUnknownProperties} from '../shared/ReactDO
import {
enableDeprecatedFlareAPI,
enableTrustedTypesIntegration,
enableModernEventSystem,
} from 'shared/ReactFeatureFlags';
import {legacyListenToEvent} from '../events/DOMLegacyEventPluginSystem';
import {
legacyListenToEvent,
legacyTrapBubbledEvent,
} from '../events/DOMLegacyEventPluginSystem';
import {listenToEvent} from '../events/DOMModernPluginEventSystem';

let didWarnInvalidHydration = false;
let didWarnScriptTags = false;
Expand Down Expand Up @@ -260,16 +270,36 @@ if (__DEV__) {
}

function ensureListeningTo(
rootContainerElement: Element | Node,
rootContainerInstance: Element | Node,
registrationName: string,
): void {
const isDocumentOrFragment =
rootContainerElement.nodeType === DOCUMENT_NODE ||
rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
const doc = isDocumentOrFragment
? rootContainerElement
: rootContainerElement.ownerDocument;
legacyListenToEvent(registrationName, doc);
if (enableModernEventSystem) {
// If we have a comment node, then use the parent node,
// which should be an element.
const rootContainerElement =
rootContainerInstance.nodeType === COMMENT_NODE
? rootContainerInstance.parentNode
: rootContainerInstance;
// Containers can only ever be element nodes. We do not
// want to register events to document fragments or documents
// with the modern plugin event system.
invariant(
rootContainerElement != null &&
rootContainerElement.nodeType === ELEMENT_NODE,
'ensureListeningTo(): received a container that was not an element node. ' +
'This is likely a bug in React.',
);
listenToEvent(registrationName, ((rootContainerElement: any): Element));
} else {
// Legacy plugin event system path
const isDocumentOrFragment =
rootContainerInstance.nodeType === DOCUMENT_NODE ||
rootContainerInstance.nodeType === DOCUMENT_FRAGMENT_NODE;
const doc = isDocumentOrFragment
? rootContainerInstance
: rootContainerInstance.ownerDocument;
legacyListenToEvent(registrationName, ((doc: any): Document));
}
}

function getOwnerDocumentFromRootContainer(
Expand Down Expand Up @@ -514,41 +544,55 @@ export function setInitialProperties(
case 'iframe':
case 'object':
case 'embed':
trapBubbledEvent(TOP_LOAD, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
props = rawProps;
break;
case 'video':
case 'audio':
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
trapBubbledEvent(mediaEventTypes[i], domElement);
if (!enableModernEventSystem) {
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
legacyTrapBubbledEvent(mediaEventTypes[i], domElement);
}
}
props = rawProps;
break;
case 'source':
trapBubbledEvent(TOP_ERROR, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
}
props = rawProps;
break;
case 'img':
case 'image':
case 'link':
trapBubbledEvent(TOP_ERROR, domElement);
trapBubbledEvent(TOP_LOAD, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
props = rawProps;
break;
case 'form':
trapBubbledEvent(TOP_RESET, domElement);
trapBubbledEvent(TOP_SUBMIT, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_RESET, domElement);
legacyTrapBubbledEvent(TOP_SUBMIT, domElement);
}
props = rawProps;
break;
case 'details':
trapBubbledEvent(TOP_TOGGLE, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_TOGGLE, domElement);
}
props = rawProps;
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
props = ReactDOMInputGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
Expand All @@ -560,15 +604,19 @@ export function setInitialProperties(
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
props = ReactDOMSelectGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
Expand Down Expand Up @@ -898,34 +946,48 @@ export function diffHydratedProperties(
case 'iframe':
case 'object':
case 'embed':
trapBubbledEvent(TOP_LOAD, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
break;
case 'video':
case 'audio':
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
trapBubbledEvent(mediaEventTypes[i], domElement);
if (!enableModernEventSystem) {
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
legacyTrapBubbledEvent(mediaEventTypes[i], domElement);
}
}
break;
case 'source':
trapBubbledEvent(TOP_ERROR, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
}
break;
case 'img':
case 'image':
case 'link':
trapBubbledEvent(TOP_ERROR, domElement);
trapBubbledEvent(TOP_LOAD, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
break;
case 'form':
trapBubbledEvent(TOP_RESET, domElement);
trapBubbledEvent(TOP_SUBMIT, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_RESET, domElement);
legacyTrapBubbledEvent(TOP_SUBMIT, domElement);
}
break;
case 'details':
trapBubbledEvent(TOP_TOGGLE, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_TOGGLE, domElement);
}
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
Expand All @@ -935,14 +997,18 @@ export function diffHydratedProperties(
break;
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
trapBubbledEvent(TOP_INVALID, domElement);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
Expand Down
18 changes: 18 additions & 0 deletions packages/react-dom/src/events/DOMEventListenerMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';

import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';

const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
// prettier-ignore
const elementListenerMap:
Expand All @@ -29,3 +31,19 @@ export function getListenerMapForElement(
}
return listenerMap;
}

export function isListeningToAllDependencies(
registrationName: string,
mountAt: Document | Element,
): boolean {
const listenerMap = getListenerMapForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];

for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!listenerMap.has(dependency)) {
return false;
}
}
return true;
}
40 changes: 19 additions & 21 deletions packages/react-dom/src/events/DOMLegacyEventPluginSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';

import getEventTarget from './getEventTarget';
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
import {trapCapturedEvent, trapBubbledEvent} from './ReactDOMEventListener';
import {getListenerMapForElement} from './DOMEventListenerMap';
import isEventSupported from './isEventSupported';
import {
Expand All @@ -40,6 +39,7 @@ import {
getRawEventName,
mediaEventTypes,
} from './DOMTopLevelEventTypes';
import {trapEventForPluginEventSystem} from './ReactDOMEventListener';

/**
* Summary of `DOMEventPluginSystem` event handling:
Expand Down Expand Up @@ -309,7 +309,7 @@ export function dispatchEventForLegacyPluginEventSystem(
*/
export function legacyListenToEvent(
registrationName: string,
mountAt: Document | Element | Node,
mountAt: Document | Element,
): void {
const listenerMap = getListenerMapForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];
Expand All @@ -322,18 +322,18 @@ export function legacyListenToEvent(

export function legacyListenToTopLevelEvent(
topLevelType: DOMTopLevelEventType,
mountAt: Document | Element | Node,
mountAt: Document | Element,
listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>,
): void {
if (!listenerMap.has(topLevelType)) {
switch (topLevelType) {
case TOP_SCROLL:
trapCapturedEvent(TOP_SCROLL, mountAt);
legacyTrapCapturedEvent(TOP_SCROLL, mountAt);
break;
case TOP_FOCUS:
case TOP_BLUR:
trapCapturedEvent(TOP_FOCUS, mountAt);
trapCapturedEvent(TOP_BLUR, mountAt);
legacyTrapCapturedEvent(TOP_FOCUS, mountAt);
legacyTrapCapturedEvent(TOP_BLUR, mountAt);
// We set the flag for a single dependency later in this function,
// but this ensures we mark both as attached rather than just one.
listenerMap.set(TOP_BLUR, null);
Expand All @@ -342,7 +342,7 @@ export function legacyListenToTopLevelEvent(
case TOP_CANCEL:
case TOP_CLOSE:
if (isEventSupported(getRawEventName(topLevelType))) {
trapCapturedEvent(topLevelType, mountAt);
legacyTrapCapturedEvent(topLevelType, mountAt);
}
break;
case TOP_INVALID:
Expand All @@ -356,26 +356,24 @@ export function legacyListenToTopLevelEvent(
// Media events don't bubble so adding the listener wouldn't do anything.
const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(topLevelType, mountAt);
legacyTrapBubbledEvent(topLevelType, mountAt);
}
break;
}
listenerMap.set(topLevelType, null);
}
}

export function isListeningToAllDependencies(
registrationName: string,
mountAt: Document | Element,
): boolean {
const listenerMap = getListenerMapForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];
export function legacyTrapBubbledEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element,
): void {
trapEventForPluginEventSystem(element, topLevelType, false);
}

for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!listenerMap.has(dependency)) {
return false;
}
}
return true;
export function legacyTrapCapturedEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element,
): void {
trapEventForPluginEventSystem(element, topLevelType, true);
}

0 comments on commit 62861bb

Please sign in to comment.