react/packages/legacy-events/EventPluginHub.js
Sebastian Markbåge 0a527707cd
Event Replaying (#16725)
* Add Event Replaying Infra

* Wire up Roots and Suspense boundaries, to retry events, after they commit

* Replay discrete events in order in a separate scheduler callback

* Add continuous events

These events only replay their last target if the target is not yet
hydrated. That way we don't have to wait for a previously hovered
boundary before invoking the current target.

* Enable tests from before

These tests were written with replaying in mind and now we can properly
enable them.

* Unify replaying and dispatching

* Mark system flags as a replay and pass to legacy events

That way we can check if this is a replay and therefore needs a special
case. One such special case is "mouseover" where we check the
relatedTarget.

* Eagerly listen to all replayable events

To minimize breakages in a minor, I only do this for the new root APIs
since replaying only matters there anyway. Only if hydrating.

For Flare, I have to attach all active listeners since the current
system has one DOM listener for each. In a follow up I plan on optimizing
that by only attaching one if there's at least one active listener
which would allow us to start with only passive and then upgrade.

* Desperate attempt to save bytese

* Add test for mouseover replaying

We need to check if the "relatedTarget" is mounted due to how the old
event system dispatches from the "out" event.

* Fix for nested boundaries and suspense in root container

This is a follow up to #16673 which didn't have a test because it wasn't
observable yet. This shows that it had a bug.

* Rename RESPONDER_EVENT_SYSTEM to PLUGIN_EVENT_SYSTEM
2019-09-23 11:21:10 -07:00

176 lines
4.9 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @flow
*/
import invariant from 'shared/invariant';
import {
injectEventPluginOrder,
injectEventPluginsByName,
plugins,
} from './EventPluginRegistry';
import {getFiberCurrentPropsFromNode} from './EventPluginUtils';
import accumulateInto from './accumulateInto';
import {runEventsInBatch} from './EventBatching';
import type {PluginModule} from './PluginModuleType';
import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {AnyNativeEvent} from './PluginModuleType';
import type {TopLevelType} from './TopLevelEventTypes';
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
function isInteractive(tag) {
return (
tag === 'button' ||
tag === 'input' ||
tag === 'select' ||
tag === 'textarea'
);
}
function shouldPreventMouseEvent(name, type, props) {
switch (name) {
case 'onClick':
case 'onClickCapture':
case 'onDoubleClick':
case 'onDoubleClickCapture':
case 'onMouseDown':
case 'onMouseDownCapture':
case 'onMouseMove':
case 'onMouseMoveCapture':
case 'onMouseUp':
case 'onMouseUpCapture':
return !!(props.disabled && isInteractive(type));
default:
return false;
}
}
/**
* This is a unified interface for event plugins to be installed and configured.
*
* Event plugins can implement the following properties:
*
* `extractEvents` {function(string, DOMEventTarget, string, object): *}
* Required. When a top-level event is fired, this method is expected to
* extract synthetic events that will in turn be queued and dispatched.
*
* `eventTypes` {object}
* Optional, plugins that fire events must publish a mapping of registration
* names that are used to register listeners. Values of this mapping must
* be objects that contain `registrationName` or `phasedRegistrationNames`.
*
* `executeDispatch` {function(object, function, string)}
* Optional, allows plugins to override how an event gets dispatched. By
* default, the listener is simply invoked.
*
* Each plugin that is injected into `EventsPluginHub` is immediately operable.
*
* @public
*/
/**
* Methods for injecting dependencies.
*/
export const injection = {
/**
* @param {array} InjectedEventPluginOrder
* @public
*/
injectEventPluginOrder,
/**
* @param {object} injectedNamesToPlugins Map from names to plugin modules.
*/
injectEventPluginsByName,
};
/**
* @param {object} inst The instance, which is the source of events.
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @return {?function} The stored callback.
*/
export function getListener(inst: Fiber, registrationName: string) {
let listener;
// TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
// live here; needs to be moved to a better place soon
const stateNode = inst.stateNode;
if (!stateNode) {
// Work in progress (ex: onload events in incremental mode).
return null;
}
const props = getFiberCurrentPropsFromNode(stateNode);
if (!props) {
// Work in progress.
return null;
}
listener = props[registrationName];
if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
return null;
}
invariant(
!listener || typeof listener === 'function',
'Expected `%s` listener to be a function, instead got a value of `%s` type.',
registrationName,
typeof listener,
);
return listener;
}
/**
* Allows registered plugins an opportunity to extract events from top-level
* native browser events.
*
* @return {*} An accumulation of synthetic events.
* @internal
*/
function extractPluginEvents(
topLevelType: TopLevelType,
eventSystemFlags: EventSystemFlags,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
let events = null;
for (let i = 0; i < plugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
if (possiblePlugin) {
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
eventSystemFlags,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
}
export function runExtractedPluginEventsInBatch(
topLevelType: TopLevelType,
eventSystemFlags: EventSystemFlags,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
) {
const events = extractPluginEvents(
topLevelType,
eventSystemFlags,
targetInst,
nativeEvent,
nativeEventTarget,
);
runEventsInBatch(events);
}