From 80f8b0d5123981969997e07c071bdc6e3884ef58 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 26 Mar 2019 16:55:25 -0700 Subject: [PATCH] Add part of the event responder system for experimental event API (#15179) * Add part of the event responder system --- packages/events/EventBatching.js | 66 +++++ packages/events/EventPluginHub.js | 65 +---- .../ResponderEventPlugin-test.internal.js | 6 +- packages/react-dom/src/client/ReactDOM.js | 6 +- .../src/client/ReactDOMHostConfig.js | 36 +-- .../react-dom/src/events/ChangeEventPlugin.js | 2 +- .../src/events/DOMEventResponderSystem.js | 195 +++++++++++++ .../src/events/ReactDOMEventListener.js | 35 ++- .../DOMEventResponderSystem-test.internal.js | 259 ++++++++++++++++++ packages/react-dom/src/fire/ReactFire.js | 7 +- .../src/ReactFabricEventEmitter.js | 7 +- .../src/ReactNativeEventEmitter.js | 7 +- packages/react-reconciler/src/ReactFiber.js | 5 +- .../src/ReactFiberCompleteWork.js | 2 + .../ReactFiberEvents-test-internal.js | 2 +- 15 files changed, 593 insertions(+), 107 deletions(-) create mode 100644 packages/events/EventBatching.js create mode 100644 packages/react-dom/src/events/DOMEventResponderSystem.js create mode 100644 packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js diff --git a/packages/events/EventBatching.js b/packages/events/EventBatching.js new file mode 100644 index 000000000000..bffc978ad6e2 --- /dev/null +++ b/packages/events/EventBatching.js @@ -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) = 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 | 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(); +} diff --git a/packages/events/EventPluginHub.js b/packages/events/EventPluginHub.js index e63911071348..297d0cbdcc44 100644 --- a/packages/events/EventPluginHub.js +++ b/packages/events/EventPluginHub.js @@ -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) = 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 | 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, diff --git a/packages/events/__tests__/ResponderEventPlugin-test.internal.js b/packages/events/__tests__/ResponderEventPlugin-test.internal.js index e828ae21e4ba..84d132993889 100644 --- a/packages/events/__tests__/ResponderEventPlugin-test.internal.js +++ b/packages/events/__tests__/ResponderEventPlugin-test.internal.js @@ -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; diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 0eac062bed74..e226843322f7 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -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, diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index baea3cbeec5a..52541406190d 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -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 + } } } } diff --git a/packages/react-dom/src/events/ChangeEventPlugin.js b/packages/react-dom/src/events/ChangeEventPlugin.js index 9e7ba2953edd..214becb6732a 100644 --- a/packages/react-dom/src/events/ChangeEventPlugin.js +++ b/packages/react-dom/src/events/ChangeEventPlugin.js @@ -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'; diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js new file mode 100644 index 000000000000..98969ac8a5b8 --- /dev/null +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -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, +> = 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 { + 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(); +} diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 51897225474d..5591646b1980 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -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, diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js new file mode 100644 index 000000000000..4af4f210dffc --- /dev/null +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -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 = () => ( + + + + ); + + ReactDOM.render(, container); + expect(container.innerHTML).toBe(''); + + // 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(, 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 = () => ( + + + + ); + + ReactDOM.render(, 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 = () => ( + + + + + + ); + + ReactDOM.render(, 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 = () => ( + + + + + + ); + + ReactDOM.render(, 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 = () => ( + + + + ); + + ReactDOM.render(, container); + + // Clicking the button should trigger the event responder handleEvent() + let buttonElement = buttonRef.current; + dispatchClickEvent(buttonElement); + + expect(eventLog).toEqual(['magic event fired', 'magicclick']); + }); +}); diff --git a/packages/react-dom/src/fire/ReactFire.js b/packages/react-dom/src/fire/ReactFire.js index 3e2f00b21f9f..525f93588225 100644 --- a/packages/react-dom/src/fire/ReactFire.js +++ b/packages/react-dom/src/fire/ReactFire.js @@ -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, diff --git a/packages/react-native-renderer/src/ReactFabricEventEmitter.js b/packages/react-native-renderer/src/ReactFabricEventEmitter.js index 7e016f8ce8c5..eead4ba1d10a 100644 --- a/packages/react-native-renderer/src/ReactFabricEventEmitter.js +++ b/packages/react-native-renderer/src/ReactFabricEventEmitter.js @@ -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, diff --git a/packages/react-native-renderer/src/ReactNativeEventEmitter.js b/packages/react-native-renderer/src/ReactNativeEventEmitter.js index a5ec6e298f13..912291cd457a 100644 --- a/packages/react-native-renderer/src/ReactNativeEventEmitter.js +++ b/packages/react-native-renderer/src/ReactNativeEventEmitter.js @@ -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, diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index b5a8b249a54c..35b62021d574 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -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; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 8a16dbe43bef..1a0fd3023226 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -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; diff --git a/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js b/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js index 9e7bcf180471..0740cb39a106 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js @@ -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();