Add part of the event responder system for experimental event API (#15179)
* Add part of the event responder system
This commit is contained in:
parent
d03ac4b231
commit
80f8b0d512
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* 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 {rethrowCaughtError} from 'shared/ReactErrorUtils';
|
||||
|
||||
import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
|
||||
import accumulateInto from './accumulateInto';
|
||||
import forEachAccumulated from './forEachAccumulated';
|
||||
import {executeDispatchesInOrder} from './EventPluginUtils';
|
||||
|
||||
/**
|
||||
* Internal queue of events that have accumulated their dispatches and are
|
||||
* waiting to have their dispatches executed.
|
||||
*/
|
||||
let eventQueue: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent) = null;
|
||||
|
||||
/**
|
||||
* Dispatches an event and releases it back into the pool, unless persistent.
|
||||
*
|
||||
* @param {?object} event Synthetic event to be dispatched.
|
||||
* @private
|
||||
*/
|
||||
const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
|
||||
if (event) {
|
||||
executeDispatchesInOrder(event);
|
||||
|
||||
if (!event.isPersistent()) {
|
||||
event.constructor.release(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
const executeDispatchesAndReleaseTopLevel = function(e) {
|
||||
return executeDispatchesAndRelease(e);
|
||||
};
|
||||
|
||||
export function runEventsInBatch(
|
||||
events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
|
||||
) {
|
||||
if (events !== null) {
|
||||
eventQueue = accumulateInto(eventQueue, events);
|
||||
}
|
||||
|
||||
// Set `eventQueue` to null before processing it so that we can tell if more
|
||||
// events get enqueued while processing.
|
||||
const processingEventQueue = eventQueue;
|
||||
eventQueue = null;
|
||||
|
||||
if (!processingEventQueue) {
|
||||
return;
|
||||
}
|
||||
|
||||
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
|
||||
invariant(
|
||||
!eventQueue,
|
||||
'processEventQueue(): Additional events were enqueued while processing ' +
|
||||
'an event queue. Support for this has not yet been implemented.',
|
||||
);
|
||||
// This would be a good time to rethrow if any of the event handlers threw.
|
||||
rethrowCaughtError();
|
||||
}
|
|
@ -6,7 +6,6 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import {rethrowCaughtError} from 'shared/ReactErrorUtils';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
import {
|
||||
|
@ -14,12 +13,9 @@ import {
|
|||
injectEventPluginsByName,
|
||||
plugins,
|
||||
} from './EventPluginRegistry';
|
||||
import {
|
||||
executeDispatchesInOrder,
|
||||
getFiberCurrentPropsFromNode,
|
||||
} from './EventPluginUtils';
|
||||
import {getFiberCurrentPropsFromNode} from './EventPluginUtils';
|
||||
import accumulateInto from './accumulateInto';
|
||||
import forEachAccumulated from './forEachAccumulated';
|
||||
import {runEventsInBatch} from './EventBatching';
|
||||
|
||||
import type {PluginModule} from './PluginModuleType';
|
||||
import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
|
||||
|
@ -27,31 +23,6 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber';
|
|||
import type {AnyNativeEvent} from './PluginModuleType';
|
||||
import type {TopLevelType} from './TopLevelEventTypes';
|
||||
|
||||
/**
|
||||
* Internal queue of events that have accumulated their dispatches and are
|
||||
* waiting to have their dispatches executed.
|
||||
*/
|
||||
let eventQueue: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent) = null;
|
||||
|
||||
/**
|
||||
* Dispatches an event and releases it back into the pool, unless persistent.
|
||||
*
|
||||
* @param {?object} event Synthetic event to be dispatched.
|
||||
* @private
|
||||
*/
|
||||
const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
|
||||
if (event) {
|
||||
executeDispatchesInOrder(event);
|
||||
|
||||
if (!event.isPersistent()) {
|
||||
event.constructor.release(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
const executeDispatchesAndReleaseTopLevel = function(e) {
|
||||
return executeDispatchesAndRelease(e);
|
||||
};
|
||||
|
||||
function isInteractive(tag) {
|
||||
return (
|
||||
tag === 'button' ||
|
||||
|
@ -158,7 +129,7 @@ export function getListener(inst: Fiber, registrationName: string) {
|
|||
* @return {*} An accumulation of synthetic events.
|
||||
* @internal
|
||||
*/
|
||||
function extractEvents(
|
||||
function extractPluginEvents(
|
||||
topLevelType: TopLevelType,
|
||||
targetInst: null | Fiber,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
|
@ -183,39 +154,13 @@ function extractEvents(
|
|||
return events;
|
||||
}
|
||||
|
||||
export function runEventsInBatch(
|
||||
events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
|
||||
) {
|
||||
if (events !== null) {
|
||||
eventQueue = accumulateInto(eventQueue, events);
|
||||
}
|
||||
|
||||
// Set `eventQueue` to null before processing it so that we can tell if more
|
||||
// events get enqueued while processing.
|
||||
const processingEventQueue = eventQueue;
|
||||
eventQueue = null;
|
||||
|
||||
if (!processingEventQueue) {
|
||||
return;
|
||||
}
|
||||
|
||||
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
|
||||
invariant(
|
||||
!eventQueue,
|
||||
'processEventQueue(): Additional events were enqueued while processing ' +
|
||||
'an event queue. Support for this has not yet been implemented.',
|
||||
);
|
||||
// This would be a good time to rethrow if any of the event handlers threw.
|
||||
rethrowCaughtError();
|
||||
}
|
||||
|
||||
export function runExtractedEventsInBatch(
|
||||
export function runExtractedPluginEventsInBatch(
|
||||
topLevelType: TopLevelType,
|
||||
targetInst: null | Fiber,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
nativeEventTarget: EventTarget,
|
||||
) {
|
||||
const events = extractEvents(
|
||||
const events = extractPluginEvents(
|
||||
topLevelType,
|
||||
targetInst,
|
||||
nativeEvent,
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
const {HostComponent} = require('shared/ReactWorkTags');
|
||||
|
||||
let EventPluginHub;
|
||||
let EventBatching;
|
||||
let EventPluginUtils;
|
||||
let ResponderEventPlugin;
|
||||
|
||||
|
@ -321,7 +321,7 @@ const run = function(config, hierarchyConfig, nativeEventConfig) {
|
|||
// At this point the negotiation events have been dispatched as part of the
|
||||
// extraction process, but not the side effectful events. Below, we dispatch
|
||||
// side effectful events.
|
||||
EventPluginHub.runEventsInBatch(extractedEvents);
|
||||
EventBatching.runEventsInBatch(extractedEvents);
|
||||
|
||||
// Ensure that every event that declared an `order`, was actually dispatched.
|
||||
expect('number of events dispatched:' + runData.dispatchCount).toBe(
|
||||
|
@ -403,7 +403,7 @@ describe('ResponderEventPlugin', () => {
|
|||
jest.resetModules();
|
||||
|
||||
const ReactDOMUnstableNativeDependencies = require('react-dom/unstable-native-dependencies');
|
||||
EventPluginHub = require('events/EventPluginHub');
|
||||
EventBatching = require('events/EventBatching');
|
||||
EventPluginUtils = require('events/EventPluginUtils');
|
||||
ResponderEventPlugin =
|
||||
ReactDOMUnstableNativeDependencies.ResponderEventPlugin;
|
||||
|
|
|
@ -44,10 +44,8 @@ import {
|
|||
enqueueStateRestore,
|
||||
restoreStateIfNeeded,
|
||||
} from 'events/ReactControlledComponent';
|
||||
import {
|
||||
injection as EventPluginHubInjection,
|
||||
runEventsInBatch,
|
||||
} from 'events/EventPluginHub';
|
||||
import {injection as EventPluginHubInjection} from 'events/EventPluginHub';
|
||||
import {runEventsInBatch} from 'events/EventBatching';
|
||||
import {eventNameDispatchConfigs} from 'events/EventPluginRegistry';
|
||||
import {
|
||||
accumulateTwoPhaseDispatches,
|
||||
|
|
|
@ -862,8 +862,10 @@ export function handleEventComponent(
|
|||
rootContainerInstance: Container,
|
||||
internalInstanceHandle: Object,
|
||||
): void {
|
||||
const rootElement = rootContainerInstance.ownerDocument;
|
||||
listenToEventResponderEvents(eventResponder, rootElement);
|
||||
if (enableEventAPI) {
|
||||
const rootElement = rootContainerInstance.ownerDocument;
|
||||
listenToEventResponderEvents(eventResponder, rootElement);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleEventTarget(
|
||||
|
@ -871,20 +873,22 @@ export function handleEventTarget(
|
|||
props: Props,
|
||||
internalInstanceHandle: Object,
|
||||
): void {
|
||||
// Touch target hit slop handling
|
||||
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
|
||||
// Validates that there is a single element
|
||||
const element = getElementFromTouchHitTarget(internalInstanceHandle);
|
||||
if (element !== null) {
|
||||
// We update the event target state node to be that of the element.
|
||||
// We can then diff this entry to determine if we need to add the
|
||||
// hit slop element, or change the dimensions of the hit slop.
|
||||
const lastElement = internalInstanceHandle.stateNode;
|
||||
if (lastElement !== element) {
|
||||
internalInstanceHandle.stateNode = element;
|
||||
// TODO: Create the hit slop element and attach it to the element
|
||||
} else {
|
||||
// TODO: Diff the left, top, right, bottom props
|
||||
if (enableEventAPI) {
|
||||
// Touch target hit slop handling
|
||||
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
|
||||
// Validates that there is a single element
|
||||
const element = getElementFromTouchHitTarget(internalInstanceHandle);
|
||||
if (element !== null) {
|
||||
// We update the event target state node to be that of the element.
|
||||
// We can then diff this entry to determine if we need to add the
|
||||
// hit slop element, or change the dimensions of the hit slop.
|
||||
const lastElement = internalInstanceHandle.stateNode;
|
||||
if (lastElement !== element) {
|
||||
internalInstanceHandle.stateNode = element;
|
||||
// TODO: Create the hit slop element and attach it to the element
|
||||
} else {
|
||||
// TODO: Diff the left, top, right, bottom props
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {runEventsInBatch} from 'events/EventPluginHub';
|
||||
import {runEventsInBatch} from 'events/EventBatching';
|
||||
import {accumulateTwoPhaseDispatches} from 'events/EventPropagators';
|
||||
import {enqueueStateRestore} from 'events/ReactControlledComponent';
|
||||
import {batchedUpdates} from 'events/ReactGenericBatching';
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
/**
|
||||
* 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 {
|
||||
type EventSystemFlags,
|
||||
IS_PASSIVE,
|
||||
PASSIVE_NOT_SUPPORTED,
|
||||
} from 'events/EventSystemFlags';
|
||||
import type {AnyNativeEvent} from 'events/PluginModuleType';
|
||||
import {EventComponent} from 'shared/ReactWorkTags';
|
||||
import type {ReactEventResponder} from 'shared/ReactTypes';
|
||||
import warning from 'shared/warning';
|
||||
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
|
||||
import SyntheticEvent from 'events/SyntheticEvent';
|
||||
import {runEventsInBatch} from 'events/EventBatching';
|
||||
import {interactiveUpdates} from 'events/ReactGenericBatching';
|
||||
import type {Fiber} from 'react-reconciler/src/ReactFiber';
|
||||
|
||||
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
|
||||
|
||||
// Event responders provide us an array of target event types.
|
||||
// To ensure we fire the right responders for given events, we check
|
||||
// if the incoming event type is actually relevant for an event
|
||||
// responder. Instead of doing an O(n) lookup on the event responder
|
||||
// target event types array each time, we instead create a Set for
|
||||
// faster O(1) lookups.
|
||||
export const eventResponderValidEventTypes: Map<
|
||||
ReactEventResponder,
|
||||
Set<DOMTopLevelEventType>,
|
||||
> = new Map();
|
||||
|
||||
type EventListener = (event: SyntheticEvent) => void;
|
||||
|
||||
// TODO add context methods for dispatching events
|
||||
function DOMEventResponderContext(
|
||||
topLevelType: DOMTopLevelEventType,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
nativeEventTarget: EventTarget,
|
||||
eventSystemFlags: EventSystemFlags,
|
||||
) {
|
||||
this.event = nativeEvent;
|
||||
this.eventType = topLevelType;
|
||||
this.eventTarget = nativeEventTarget;
|
||||
this._flags = eventSystemFlags;
|
||||
this._fiber = null;
|
||||
this._responder = null;
|
||||
this._discreteEvents = null;
|
||||
this._nonDiscreteEvents = null;
|
||||
}
|
||||
|
||||
DOMEventResponderContext.prototype.isPassive = function(): boolean {
|
||||
return (this._flags & IS_PASSIVE) !== 0;
|
||||
};
|
||||
|
||||
DOMEventResponderContext.prototype.isPassiveSupported = function(): boolean {
|
||||
return (this._flags & PASSIVE_NOT_SUPPORTED) === 0;
|
||||
};
|
||||
|
||||
function copyEventProperties(eventData, syntheticEvent) {
|
||||
for (let propName in eventData) {
|
||||
syntheticEvent[propName] = eventData[propName];
|
||||
}
|
||||
}
|
||||
|
||||
DOMEventResponderContext.prototype.dispatchEvent = function(
|
||||
eventName: string,
|
||||
eventListener: EventListener,
|
||||
eventTarget: AnyNativeEvent,
|
||||
discrete: boolean,
|
||||
extraProperties?: Object,
|
||||
): void {
|
||||
const eventTargetFiber = getClosestInstanceFromNode(eventTarget);
|
||||
const syntheticEvent = SyntheticEvent.getPooled(
|
||||
null,
|
||||
eventTargetFiber,
|
||||
this.event,
|
||||
eventTarget,
|
||||
);
|
||||
if (extraProperties !== undefined) {
|
||||
copyEventProperties(extraProperties, syntheticEvent);
|
||||
}
|
||||
syntheticEvent.type = eventName;
|
||||
syntheticEvent._dispatchInstances = [eventTargetFiber];
|
||||
syntheticEvent._dispatchListeners = [eventListener];
|
||||
|
||||
let events;
|
||||
if (discrete) {
|
||||
events = this._discreteEvents;
|
||||
if (events === null) {
|
||||
events = this._discreteEvents = [];
|
||||
}
|
||||
} else {
|
||||
events = this._nonDiscreteEvents;
|
||||
if (events === null) {
|
||||
events = this._nonDiscreteEvents = [];
|
||||
}
|
||||
}
|
||||
events.push(syntheticEvent);
|
||||
};
|
||||
|
||||
DOMEventResponderContext.prototype._runEventsInBatch = function(): void {
|
||||
if (this._discreteEvents !== null) {
|
||||
interactiveUpdates(() => {
|
||||
runEventsInBatch(this._discreteEvents);
|
||||
});
|
||||
}
|
||||
if (this._nonDiscreteEvents !== null) {
|
||||
runEventsInBatch(this._nonDiscreteEvents);
|
||||
}
|
||||
};
|
||||
|
||||
function createValidEventTypeSet(targetEventTypes): Set<DOMTopLevelEventType> {
|
||||
const eventTypeSet = new Set();
|
||||
// Go through each target event type of the event responder
|
||||
for (let i = 0, length = targetEventTypes.length; i < length; ++i) {
|
||||
const targetEventType = targetEventTypes[i];
|
||||
|
||||
if (typeof targetEventType === 'string') {
|
||||
eventTypeSet.add(((targetEventType: any): DOMTopLevelEventType));
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
warning(
|
||||
typeof targetEventType === 'object' && targetEventType !== null,
|
||||
'Event Responder: invalid entry in targetEventTypes array. ' +
|
||||
'Entry must be string or an object. Instead, got %s.',
|
||||
targetEventType,
|
||||
);
|
||||
}
|
||||
const targetEventConfigObject = ((targetEventType: any): {
|
||||
name: DOMTopLevelEventType,
|
||||
passive?: boolean,
|
||||
capture?: boolean,
|
||||
});
|
||||
eventTypeSet.add(targetEventConfigObject.name);
|
||||
}
|
||||
}
|
||||
return eventTypeSet;
|
||||
}
|
||||
|
||||
function handleTopLevelType(
|
||||
topLevelType: DOMTopLevelEventType,
|
||||
fiber: Fiber,
|
||||
context: Object,
|
||||
): void {
|
||||
const responder: ReactEventResponder = fiber.type.responder;
|
||||
let {props, state} = fiber.stateNode;
|
||||
let validEventTypesForResponder = eventResponderValidEventTypes.get(
|
||||
responder,
|
||||
);
|
||||
|
||||
if (validEventTypesForResponder === undefined) {
|
||||
validEventTypesForResponder = createValidEventTypeSet(
|
||||
responder.targetEventTypes,
|
||||
);
|
||||
eventResponderValidEventTypes.set(responder, validEventTypesForResponder);
|
||||
}
|
||||
if (!validEventTypesForResponder.has(topLevelType)) {
|
||||
return;
|
||||
}
|
||||
if (state === null && responder.createInitialState !== undefined) {
|
||||
state = fiber.stateNode.state = responder.createInitialState(props);
|
||||
}
|
||||
context._fiber = fiber;
|
||||
context._responder = responder;
|
||||
responder.handleEvent(context, props, state);
|
||||
}
|
||||
|
||||
export function runResponderEventsInBatch(
|
||||
topLevelType: DOMTopLevelEventType,
|
||||
targetFiber: Fiber,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
nativeEventTarget: EventTarget,
|
||||
eventSystemFlags: EventSystemFlags,
|
||||
): void {
|
||||
const context = new DOMEventResponderContext(
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
nativeEventTarget,
|
||||
eventSystemFlags,
|
||||
);
|
||||
let node = targetFiber;
|
||||
// Traverse up the fiber tree till we find event component fibers.
|
||||
while (node !== null) {
|
||||
if (node.tag === EventComponent) {
|
||||
handleTopLevelType(topLevelType, node, context);
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
context._runEventsInBatch();
|
||||
}
|
|
@ -12,7 +12,8 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber';
|
|||
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
|
||||
|
||||
import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
|
||||
import {runExtractedEventsInBatch} from 'events/EventPluginHub';
|
||||
import {runExtractedPluginEventsInBatch} from 'events/EventPluginHub';
|
||||
import {runResponderEventsInBatch} from '../events/DOMEventResponderSystem';
|
||||
import {isFiberMounted} from 'react-reconciler/reflection';
|
||||
import {HostRoot} from 'shared/ReactWorkTags';
|
||||
import {
|
||||
|
@ -130,16 +131,27 @@ function handleTopLevel(bookKeeping: BookKeepingInstance) {
|
|||
|
||||
for (let i = 0; i < bookKeeping.ancestors.length; i++) {
|
||||
targetInst = bookKeeping.ancestors[i];
|
||||
if (bookKeeping.eventSystemFlags === PLUGIN_EVENT_SYSTEM) {
|
||||
runExtractedEventsInBatch(
|
||||
((bookKeeping.topLevelType: any): DOMTopLevelEventType),
|
||||
const eventSystemFlags = bookKeeping.eventSystemFlags;
|
||||
const eventTarget = getEventTarget(bookKeeping.nativeEvent);
|
||||
const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType);
|
||||
const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent);
|
||||
|
||||
if (eventSystemFlags === PLUGIN_EVENT_SYSTEM) {
|
||||
runExtractedPluginEventsInBatch(
|
||||
topLevelType,
|
||||
targetInst,
|
||||
((bookKeeping.nativeEvent: any): AnyNativeEvent),
|
||||
getEventTarget(bookKeeping.nativeEvent),
|
||||
nativeEvent,
|
||||
eventTarget,
|
||||
);
|
||||
} else if (enableEventAPI && targetInst !== null) {
|
||||
// Responder event system (experimental event API)
|
||||
runResponderEventsInBatch(
|
||||
topLevelType,
|
||||
targetInst,
|
||||
nativeEvent,
|
||||
eventTarget,
|
||||
eventSystemFlags,
|
||||
);
|
||||
} else {
|
||||
// RESPONDER_EVENT_SYSTEM
|
||||
// TODO: Add implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -176,9 +188,6 @@ export function trapEventForResponderEventSystem(
|
|||
passive: boolean,
|
||||
): void {
|
||||
if (enableEventAPI) {
|
||||
const dispatch = isInteractiveTopLevelEventType(topLevelType)
|
||||
? dispatchInteractiveEvent
|
||||
: dispatchEvent;
|
||||
const rawEventName = getRawEventName(topLevelType);
|
||||
let eventFlags = RESPONDER_EVENT_SYSTEM;
|
||||
|
||||
|
@ -198,7 +207,7 @@ export function trapEventForResponderEventSystem(
|
|||
eventFlags |= IS_ACTIVE;
|
||||
}
|
||||
// Check if interactive and wrap in interactiveUpdates
|
||||
const listener = dispatch.bind(null, topLevelType, eventFlags);
|
||||
const listener = dispatchEvent.bind(null, topLevelType, eventFlags);
|
||||
addEventListener(element, rawEventName, listener, {
|
||||
capture,
|
||||
passive,
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactDOM;
|
||||
|
||||
function createReactEventComponent(targetEventTypes, handleEvent) {
|
||||
const testEventResponder = {
|
||||
targetEventTypes,
|
||||
handleEvent,
|
||||
};
|
||||
|
||||
return {
|
||||
$$typeof: Symbol.for('react.event_component'),
|
||||
props: null,
|
||||
responder: testEventResponder,
|
||||
};
|
||||
}
|
||||
|
||||
function dispatchClickEvent(element) {
|
||||
const clickEvent = document.createEvent('Event');
|
||||
clickEvent.initEvent('click', true, true);
|
||||
element.dispatchEvent(clickEvent);
|
||||
}
|
||||
|
||||
// This is a new feature in Fiber so I put it in its own test file. It could
|
||||
// probably move to one of the other test files once it is official.
|
||||
describe('DOMEventResponderSystem', () => {
|
||||
let container;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableEventAPI = true;
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
container = null;
|
||||
});
|
||||
|
||||
it('the event responder handleEvent() function should fire on click event', () => {
|
||||
let eventResponderFiredCount = 0;
|
||||
let eventLog = [];
|
||||
const buttonRef = React.createRef();
|
||||
|
||||
const ClickEventComponent = createReactEventComponent(
|
||||
['click'],
|
||||
(context, props) => {
|
||||
eventResponderFiredCount++;
|
||||
eventLog.push({
|
||||
name: context.eventType,
|
||||
passive: context.isPassive(),
|
||||
passiveSupported: context.isPassiveSupported(),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const Test = () => (
|
||||
<ClickEventComponent>
|
||||
<button ref={buttonRef}>Click me!</button>
|
||||
</ClickEventComponent>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
expect(container.innerHTML).toBe('<button>Click me!</button>');
|
||||
|
||||
// Clicking the button should trigger the event responder handleEvent()
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(eventResponderFiredCount).toBe(1);
|
||||
expect(eventLog.length).toBe(1);
|
||||
// JSDOM does not support passive events, so this will be false
|
||||
expect(eventLog[0]).toEqual({
|
||||
name: 'click',
|
||||
passive: false,
|
||||
passiveSupported: false,
|
||||
});
|
||||
|
||||
// Unmounting the container and clicking should not increment anything
|
||||
ReactDOM.render(null, container);
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(eventResponderFiredCount).toBe(1);
|
||||
|
||||
// Re-rendering the container and clicking should increase the counter again
|
||||
ReactDOM.render(<Test />, container);
|
||||
buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(eventResponderFiredCount).toBe(2);
|
||||
});
|
||||
|
||||
it('the event responder handleEvent() function should fire on click event (passive events forced)', () => {
|
||||
// JSDOM does not support passive events, so this manually overrides the value to be true
|
||||
const checkPassiveEvents = require('react-dom/src/events/checkPassiveEvents');
|
||||
checkPassiveEvents.passiveBrowserEventsSupported = true;
|
||||
|
||||
let eventLog = [];
|
||||
const buttonRef = React.createRef();
|
||||
|
||||
const ClickEventComponent = createReactEventComponent(
|
||||
['click'],
|
||||
(context, props) => {
|
||||
eventLog.push({
|
||||
name: context.eventType,
|
||||
passive: context.isPassive(),
|
||||
passiveSupported: context.isPassiveSupported(),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const Test = () => (
|
||||
<ClickEventComponent>
|
||||
<button ref={buttonRef}>Click me!</button>
|
||||
</ClickEventComponent>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
|
||||
// Clicking the button should trigger the event responder handleEvent()
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(eventLog.length).toBe(1);
|
||||
expect(eventLog[0]).toEqual({
|
||||
name: 'click',
|
||||
passive: true,
|
||||
passiveSupported: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('nested event responders and their handleEvent() function should fire multiple times', () => {
|
||||
let eventResponderFiredCount = 0;
|
||||
let eventLog = [];
|
||||
const buttonRef = React.createRef();
|
||||
|
||||
const ClickEventComponent = createReactEventComponent(
|
||||
['click'],
|
||||
(context, props) => {
|
||||
eventResponderFiredCount++;
|
||||
eventLog.push({
|
||||
name: context.eventType,
|
||||
passive: context.isPassive(),
|
||||
passiveSupported: context.isPassiveSupported(),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const Test = () => (
|
||||
<ClickEventComponent>
|
||||
<ClickEventComponent>
|
||||
<button ref={buttonRef}>Click me!</button>
|
||||
</ClickEventComponent>
|
||||
</ClickEventComponent>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
|
||||
// Clicking the button should trigger the event responder handleEvent()
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(eventResponderFiredCount).toBe(2);
|
||||
expect(eventLog.length).toBe(2);
|
||||
// JSDOM does not support passive events, so this will be false
|
||||
expect(eventLog[0]).toEqual({
|
||||
name: 'click',
|
||||
passive: false,
|
||||
passiveSupported: false,
|
||||
});
|
||||
expect(eventLog[1]).toEqual({
|
||||
name: 'click',
|
||||
passive: false,
|
||||
passiveSupported: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('nested event responders and their handleEvent() should fire in the correct order', () => {
|
||||
let eventLog = [];
|
||||
const buttonRef = React.createRef();
|
||||
|
||||
const ClickEventComponentA = createReactEventComponent(
|
||||
['click'],
|
||||
(context, props) => {
|
||||
eventLog.push('A');
|
||||
},
|
||||
);
|
||||
|
||||
const ClickEventComponentB = createReactEventComponent(
|
||||
['click'],
|
||||
(context, props) => {
|
||||
eventLog.push('B');
|
||||
},
|
||||
);
|
||||
|
||||
const Test = () => (
|
||||
<ClickEventComponentA>
|
||||
<ClickEventComponentB>
|
||||
<button ref={buttonRef}>Click me!</button>
|
||||
</ClickEventComponentB>
|
||||
</ClickEventComponentA>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
|
||||
// Clicking the button should trigger the event responder handleEvent()
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
|
||||
expect(eventLog).toEqual(['B', 'A']);
|
||||
});
|
||||
|
||||
it('custom event dispatching for click -> magicClick works', () => {
|
||||
let eventLog = [];
|
||||
const buttonRef = React.createRef();
|
||||
|
||||
const ClickEventComponent = createReactEventComponent(
|
||||
['click'],
|
||||
(context, props) => {
|
||||
if (props.onMagicClick) {
|
||||
context.dispatchEvent(
|
||||
'magicclick',
|
||||
props.onMagicClick,
|
||||
context.eventTarget,
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function handleMagicEvent(e) {
|
||||
eventLog.push('magic event fired', e.type);
|
||||
}
|
||||
|
||||
const Test = () => (
|
||||
<ClickEventComponent onMagicClick={handleMagicEvent}>
|
||||
<button ref={buttonRef}>Click me!</button>
|
||||
</ClickEventComponent>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
|
||||
// Clicking the button should trigger the event responder handleEvent()
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
|
||||
expect(eventLog).toEqual(['magic event fired', 'magicclick']);
|
||||
});
|
||||
});
|
|
@ -49,10 +49,9 @@ import {
|
|||
enqueueStateRestore,
|
||||
restoreStateIfNeeded,
|
||||
} from 'events/ReactControlledComponent';
|
||||
import {
|
||||
injection as EventPluginHubInjection,
|
||||
runEventsInBatch,
|
||||
} from 'events/EventPluginHub';
|
||||
import {injection as EventPluginHubInjection} from 'events/EventPluginHub';
|
||||
|
||||
import {runEventsInBatch} from 'events/EventBatching';
|
||||
import {eventNameDispatchConfigs} from 'events/EventPluginRegistry';
|
||||
import {
|
||||
accumulateTwoPhaseDispatches,
|
||||
|
|
|
@ -9,7 +9,10 @@
|
|||
|
||||
import type {Fiber} from 'react-reconciler/src/ReactFiber';
|
||||
|
||||
import {getListener, runExtractedEventsInBatch} from 'events/EventPluginHub';
|
||||
import {
|
||||
getListener,
|
||||
runExtractedPluginEventsInBatch,
|
||||
} from 'events/EventPluginHub';
|
||||
import {registrationNameModules} from 'events/EventPluginRegistry';
|
||||
import {batchedUpdates} from 'events/ReactGenericBatching';
|
||||
|
||||
|
@ -25,7 +28,7 @@ export function dispatchEvent(
|
|||
) {
|
||||
const targetFiber = (target: null | Fiber);
|
||||
batchedUpdates(function() {
|
||||
runExtractedEventsInBatch(
|
||||
runExtractedPluginEventsInBatch(
|
||||
topLevelType,
|
||||
targetFiber,
|
||||
nativeEvent,
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import {getListener, runExtractedEventsInBatch} from 'events/EventPluginHub';
|
||||
import {
|
||||
getListener,
|
||||
runExtractedPluginEventsInBatch,
|
||||
} from 'events/EventPluginHub';
|
||||
import {registrationNameModules} from 'events/EventPluginRegistry';
|
||||
import {batchedUpdates} from 'events/ReactGenericBatching';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
@ -95,7 +98,7 @@ function _receiveRootNodeIDEvent(
|
|||
const nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT;
|
||||
const inst = getInstanceFromNode(rootNodeID);
|
||||
batchedUpdates(function() {
|
||||
runExtractedEventsInBatch(
|
||||
runExtractedPluginEventsInBatch(
|
||||
topLevelType,
|
||||
inst,
|
||||
nativeEvent,
|
||||
|
|
|
@ -623,7 +623,10 @@ export function createFiberFromEventComponent(
|
|||
const fiber = createFiber(EventComponent, pendingProps, key, mode);
|
||||
fiber.elementType = eventComponent;
|
||||
fiber.type = eventComponent;
|
||||
fiber.stateNode = new Map();
|
||||
fiber.stateNode = {
|
||||
props: pendingProps,
|
||||
state: null,
|
||||
};
|
||||
fiber.expirationTime = expirationTime;
|
||||
return fiber;
|
||||
}
|
||||
|
|
|
@ -774,6 +774,8 @@ function completeWork(
|
|||
popHostContext(workInProgress);
|
||||
const rootContainerInstance = getRootHostContainer();
|
||||
const responder = workInProgress.type.responder;
|
||||
// Update the props on the event component state node
|
||||
workInProgress.stateNode.props = newProps;
|
||||
handleEventComponent(responder, rootContainerInstance, workInProgress);
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -53,7 +53,7 @@ function initTestRenderer() {
|
|||
|
||||
// This is a new feature in Fiber so I put it in its own test file. It could
|
||||
// probably move to one of the other test files once it is official.
|
||||
describe('ReactTopLevelText', () => {
|
||||
describe('ReactFiberEvents', () => {
|
||||
describe('NoopRenderer', () => {
|
||||
beforeEach(() => {
|
||||
initNoopRenderer();
|
||||
|
|
Loading…
Reference in New Issue