@samitouri / QOS-React / commits / 80f8b0d512

Add part of the event responder system for experimental event API (#15179)

* Add part of the event responder system

Dominic Gannaway committed Mar 26, 2019 at 16:55 UTC 80f8b0d5123981969997e07c071bdc6e3884ef58
15 files changed +593 -107
packages/events/EventBatching.js new
+66
@@ -0,0 +1,66 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + * @flow
7 + */
8 +
9 +import invariant from 'shared/invariant';
10 +import {rethrowCaughtError} from 'shared/ReactErrorUtils';
11 +
12 +import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
13 +import accumulateInto from './accumulateInto';
14 +import forEachAccumulated from './forEachAccumulated';
15 +import {executeDispatchesInOrder} from './EventPluginUtils';
16 +
17 +/**
18 + * Internal queue of events that have accumulated their dispatches and are
19 + * waiting to have their dispatches executed.
20 + */
21 +let eventQueue: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent) = null;
22 +
23 +/**
24 + * Dispatches an event and releases it back into the pool, unless persistent.
25 + *
26 + * @param {?object} event Synthetic event to be dispatched.
27 + * @private
28 + */
29 +const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
30 + if (event) {
31 + executeDispatchesInOrder(event);
32 +
33 + if (!event.isPersistent()) {
34 + event.constructor.release(event);
35 + }
36 + }
37 +};
38 +const executeDispatchesAndReleaseTopLevel = function(e) {
39 + return executeDispatchesAndRelease(e);
40 +};
41 +
42 +export function runEventsInBatch(
43 + events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
44 +) {
45 + if (events !== null) {
46 + eventQueue = accumulateInto(eventQueue, events);
47 + }
48 +
49 + // Set `eventQueue` to null before processing it so that we can tell if more
50 + // events get enqueued while processing.
51 + const processingEventQueue = eventQueue;
52 + eventQueue = null;
53 +
54 + if (!processingEventQueue) {
55 + return;
56 + }
57 +
58 + forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
59 + invariant(
60 + !eventQueue,
61 + 'processEventQueue(): Additional events were enqueued while processing ' +
62 + 'an event queue. Support for this has not yet been implemented.',
63 + );
64 + // This would be a good time to rethrow if any of the event handlers threw.
65 + rethrowCaughtError();
66 +}
packages/events/EventPluginHub.js
+5 -60
@@ -6,7 +6,6 @@
6 * @flow
7 */
8
9 -import {rethrowCaughtError} from 'shared/ReactErrorUtils';
9 import invariant from 'shared/invariant';
10
11 import {
@@ -14,12 +13,9 @@ import {
13 injectEventPluginsByName,
14 plugins,
15 } from './EventPluginRegistry';
17 -import {
18 - executeDispatchesInOrder,
19 - getFiberCurrentPropsFromNode,
20 -} from './EventPluginUtils';
16 +import {getFiberCurrentPropsFromNode} from './EventPluginUtils';
17 import accumulateInto from './accumulateInto';
22 -import forEachAccumulated from './forEachAccumulated';
18 +import {runEventsInBatch} from './EventBatching';
19
20 import type {PluginModule} from './PluginModuleType';
21 import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
@@ -27,31 +23,6 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber';
23 import type {AnyNativeEvent} from './PluginModuleType';
24 import type {TopLevelType} from './TopLevelEventTypes';
25
30 -/**
31 - * Internal queue of events that have accumulated their dispatches and are
32 - * waiting to have their dispatches executed.
33 - */
34 -let eventQueue: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent) = null;
35 -
36 -/**
37 - * Dispatches an event and releases it back into the pool, unless persistent.
38 - *
39 - * @param {?object} event Synthetic event to be dispatched.
40 - * @private
41 - */
42 -const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
43 - if (event) {
44 - executeDispatchesInOrder(event);
45 -
46 - if (!event.isPersistent()) {
47 - event.constructor.release(event);
48 - }
49 - }
50 -};
51 -const executeDispatchesAndReleaseTopLevel = function(e) {
52 - return executeDispatchesAndRelease(e);
53 -};
54 -
26 function isInteractive(tag) {
27 return (
28 tag === 'button' ||
@@ -158,7 +129,7 @@ export function getListener(inst: Fiber, registrationName: string) {
129 * @return {*} An accumulation of synthetic events.
130 * @internal
131 */
161 -function extractEvents(
132 +function extractPluginEvents(
133 topLevelType: TopLevelType,
134 targetInst: null | Fiber,
135 nativeEvent: AnyNativeEvent,
@@ -183,39 +154,13 @@ function extractEvents(
154 return events;
155 }
156
186 -export function runEventsInBatch(
187 - events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
188 -) {
189 - if (events !== null) {
190 - eventQueue = accumulateInto(eventQueue, events);
191 - }
192 -
193 - // Set `eventQueue` to null before processing it so that we can tell if more
194 - // events get enqueued while processing.
195 - const processingEventQueue = eventQueue;
196 - eventQueue = null;
197 -
198 - if (!processingEventQueue) {
199 - return;
200 - }
201 -
202 - forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
203 - invariant(
204 - !eventQueue,
205 - 'processEventQueue(): Additional events were enqueued while processing ' +
206 - 'an event queue. Support for this has not yet been implemented.',
207 - );
208 - // This would be a good time to rethrow if any of the event handlers threw.
209 - rethrowCaughtError();
210 -}
211 -
212 -export function runExtractedEventsInBatch(
157 +export function runExtractedPluginEventsInBatch(
158 topLevelType: TopLevelType,
159 targetInst: null | Fiber,
160 nativeEvent: AnyNativeEvent,
161 nativeEventTarget: EventTarget,
162 ) {
218 - const events = extractEvents(
163 + const events = extractPluginEvents(
164 topLevelType,
165 targetInst,
166 nativeEvent,
packages/events/__tests__/ResponderEventPlugin-test.internal.js
+3 -3
@@ -11,7 +11,7 @@
11
12 const {HostComponent} = require('shared/ReactWorkTags');
13
14 -let EventPluginHub;
14 +let EventBatching;
15 let EventPluginUtils;
16 let ResponderEventPlugin;
17
@@ -321,7 +321,7 @@ const run = function(config, hierarchyConfig, nativeEventConfig) {
321 // At this point the negotiation events have been dispatched as part of the
322 // extraction process, but not the side effectful events. Below, we dispatch
323 // side effectful events.
324 - EventPluginHub.runEventsInBatch(extractedEvents);
324 + EventBatching.runEventsInBatch(extractedEvents);
325
326 // Ensure that every event that declared an `order`, was actually dispatched.
327 expect('number of events dispatched:' + runData.dispatchCount).toBe(
@@ -403,7 +403,7 @@ describe('ResponderEventPlugin', () => {
403 jest.resetModules();
404
405 const ReactDOMUnstableNativeDependencies = require('react-dom/unstable-native-dependencies');
406 - EventPluginHub = require('events/EventPluginHub');
406 + EventBatching = require('events/EventBatching');
407 EventPluginUtils = require('events/EventPluginUtils');
408 ResponderEventPlugin =
409 ReactDOMUnstableNativeDependencies.ResponderEventPlugin;
packages/react-dom/src/client/ReactDOM.js
+2 -4
@@ -44,10 +44,8 @@ import {
44 enqueueStateRestore,
45 restoreStateIfNeeded,
46 } from 'events/ReactControlledComponent';
47 -import {
48 - injection as EventPluginHubInjection,
49 - runEventsInBatch,
50 -} from 'events/EventPluginHub';
47 +import {injection as EventPluginHubInjection} from 'events/EventPluginHub';
48 +import {runEventsInBatch} from 'events/EventBatching';
49 import {eventNameDispatchConfigs} from 'events/EventPluginRegistry';
50 import {
51 accumulateTwoPhaseDispatches,
packages/react-dom/src/client/ReactDOMHostConfig.js
+20 -16
@@ -862,8 +862,10 @@ export function handleEventComponent(
862 rootContainerInstance: Container,
863 internalInstanceHandle: Object,
864 ): void {
865 - const rootElement = rootContainerInstance.ownerDocument;
866 - listenToEventResponderEvents(eventResponder, rootElement);
865 + if (enableEventAPI) {
866 + const rootElement = rootContainerInstance.ownerDocument;
867 + listenToEventResponderEvents(eventResponder, rootElement);
868 + }
869 }
870
871 export function handleEventTarget(
@@ -871,20 +873,22 @@ export function handleEventTarget(
873 props: Props,
874 internalInstanceHandle: Object,
875 ): void {
874 - // Touch target hit slop handling
875 - if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
876 - // Validates that there is a single element
877 - const element = getElementFromTouchHitTarget(internalInstanceHandle);
878 - if (element !== null) {
879 - // We update the event target state node to be that of the element.
880 - // We can then diff this entry to determine if we need to add the
881 - // hit slop element, or change the dimensions of the hit slop.
882 - const lastElement = internalInstanceHandle.stateNode;
883 - if (lastElement !== element) {
884 - internalInstanceHandle.stateNode = element;
885 - // TODO: Create the hit slop element and attach it to the element
886 - } else {
887 - // TODO: Diff the left, top, right, bottom props
876 + if (enableEventAPI) {
877 + // Touch target hit slop handling
878 + if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
879 + // Validates that there is a single element
880 + const element = getElementFromTouchHitTarget(internalInstanceHandle);
881 + if (element !== null) {
882 + // We update the event target state node to be that of the element.
883 + // We can then diff this entry to determine if we need to add the
884 + // hit slop element, or change the dimensions of the hit slop.
885 + const lastElement = internalInstanceHandle.stateNode;
886 + if (lastElement !== element) {
887 + internalInstanceHandle.stateNode = element;
888 + // TODO: Create the hit slop element and attach it to the element
889 + } else {
890 + // TODO: Diff the left, top, right, bottom props
891 + }
892 }
893 }
894 }
packages/react-dom/src/events/ChangeEventPlugin.js
+1 -1
@@ -5,7 +5,7 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {runEventsInBatch} from 'events/EventPluginHub';
8 +import {runEventsInBatch} from 'events/EventBatching';
9 import {accumulateTwoPhaseDispatches} from 'events/EventPropagators';
10 import {enqueueStateRestore} from 'events/ReactControlledComponent';
11 import {batchedUpdates} from 'events/ReactGenericBatching';
packages/react-dom/src/events/DOMEventResponderSystem.js new
+195
@@ -0,0 +1,195 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + * @flow
7 + */
8 +
9 +import {
10 + type EventSystemFlags,
11 + IS_PASSIVE,
12 + PASSIVE_NOT_SUPPORTED,
13 +} from 'events/EventSystemFlags';
14 +import type {AnyNativeEvent} from 'events/PluginModuleType';
15 +import {EventComponent} from 'shared/ReactWorkTags';
16 +import type {ReactEventResponder} from 'shared/ReactTypes';
17 +import warning from 'shared/warning';
18 +import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
19 +import SyntheticEvent from 'events/SyntheticEvent';
20 +import {runEventsInBatch} from 'events/EventBatching';
21 +import {interactiveUpdates} from 'events/ReactGenericBatching';
22 +import type {Fiber} from 'react-reconciler/src/ReactFiber';
23 +
24 +import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
25 +
26 +// Event responders provide us an array of target event types.
27 +// To ensure we fire the right responders for given events, we check
28 +// if the incoming event type is actually relevant for an event
29 +// responder. Instead of doing an O(n) lookup on the event responder
30 +// target event types array each time, we instead create a Set for
31 +// faster O(1) lookups.
32 +export const eventResponderValidEventTypes: Map<
33 + ReactEventResponder,
34 + Set<DOMTopLevelEventType>,
35 +> = new Map();
36 +
37 +type EventListener = (event: SyntheticEvent) => void;
38 +
39 +// TODO add context methods for dispatching events
40 +function DOMEventResponderContext(
41 + topLevelType: DOMTopLevelEventType,
42 + nativeEvent: AnyNativeEvent,
43 + nativeEventTarget: EventTarget,
44 + eventSystemFlags: EventSystemFlags,
45 +) {
46 + this.event = nativeEvent;
47 + this.eventType = topLevelType;
48 + this.eventTarget = nativeEventTarget;
49 + this._flags = eventSystemFlags;
50 + this._fiber = null;
51 + this._responder = null;
52 + this._discreteEvents = null;
53 + this._nonDiscreteEvents = null;
54 +}
55 +
56 +DOMEventResponderContext.prototype.isPassive = function(): boolean {
57 + return (this._flags & IS_PASSIVE) !== 0;
58 +};
59 +
60 +DOMEventResponderContext.prototype.isPassiveSupported = function(): boolean {
61 + return (this._flags & PASSIVE_NOT_SUPPORTED) === 0;
62 +};
63 +
64 +function copyEventProperties(eventData, syntheticEvent) {
65 + for (let propName in eventData) {
66 + syntheticEvent[propName] = eventData[propName];
67 + }
68 +}
69 +
70 +DOMEventResponderContext.prototype.dispatchEvent = function(
71 + eventName: string,
72 + eventListener: EventListener,
73 + eventTarget: AnyNativeEvent,
74 + discrete: boolean,
75 + extraProperties?: Object,
76 +): void {
77 + const eventTargetFiber = getClosestInstanceFromNode(eventTarget);
78 + const syntheticEvent = SyntheticEvent.getPooled(
79 + null,
80 + eventTargetFiber,
81 + this.event,
82 + eventTarget,
83 + );
84 + if (extraProperties !== undefined) {
85 + copyEventProperties(extraProperties, syntheticEvent);
86 + }
87 + syntheticEvent.type = eventName;
88 + syntheticEvent._dispatchInstances = [eventTargetFiber];
89 + syntheticEvent._dispatchListeners = [eventListener];
90 +
91 + let events;
92 + if (discrete) {
93 + events = this._discreteEvents;
94 + if (events === null) {
95 + events = this._discreteEvents = [];
96 + }
97 + } else {
98 + events = this._nonDiscreteEvents;
99 + if (events === null) {
100 + events = this._nonDiscreteEvents = [];
101 + }
102 + }
103 + events.push(syntheticEvent);
104 +};
105 +
106 +DOMEventResponderContext.prototype._runEventsInBatch = function(): void {
107 + if (this._discreteEvents !== null) {
108 + interactiveUpdates(() => {
109 + runEventsInBatch(this._discreteEvents);
110 + });
111 + }
112 + if (this._nonDiscreteEvents !== null) {
113 + runEventsInBatch(this._nonDiscreteEvents);
114 + }
115 +};
116 +
117 +function createValidEventTypeSet(targetEventTypes): Set<DOMTopLevelEventType> {
118 + const eventTypeSet = new Set();
119 + // Go through each target event type of the event responder
120 + for (let i = 0, length = targetEventTypes.length; i < length; ++i) {
121 + const targetEventType = targetEventTypes[i];
122 +
123 + if (typeof targetEventType === 'string') {
124 + eventTypeSet.add(((targetEventType: any): DOMTopLevelEventType));
125 + } else {
126 + if (__DEV__) {
127 + warning(
128 + typeof targetEventType === 'object' && targetEventType !== null,
129 + 'Event Responder: invalid entry in targetEventTypes array. ' +
130 + 'Entry must be string or an object. Instead, got %s.',
131 + targetEventType,
132 + );
133 + }
134 + const targetEventConfigObject = ((targetEventType: any): {
135 + name: DOMTopLevelEventType,
136 + passive?: boolean,
137 + capture?: boolean,
138 + });
139 + eventTypeSet.add(targetEventConfigObject.name);
140 + }
141 + }
142 + return eventTypeSet;
143 +}
144 +
145 +function handleTopLevelType(
146 + topLevelType: DOMTopLevelEventType,
147 + fiber: Fiber,
148 + context: Object,
149 +): void {
150 + const responder: ReactEventResponder = fiber.type.responder;
151 + let {props, state} = fiber.stateNode;
152 + let validEventTypesForResponder = eventResponderValidEventTypes.get(
153 + responder,
154 + );
155 +
156 + if (validEventTypesForResponder === undefined) {
157 + validEventTypesForResponder = createValidEventTypeSet(
158 + responder.targetEventTypes,
159 + );
160 + eventResponderValidEventTypes.set(responder, validEventTypesForResponder);
161 + }
162 + if (!validEventTypesForResponder.has(topLevelType)) {
163 + return;
164 + }
165 + if (state === null && responder.createInitialState !== undefined) {
166 + state = fiber.stateNode.state = responder.createInitialState(props);
167 + }
168 + context._fiber = fiber;
169 + context._responder = responder;
170 + responder.handleEvent(context, props, state);
171 +}
172 +
173 +export function runResponderEventsInBatch(
174 + topLevelType: DOMTopLevelEventType,
175 + targetFiber: Fiber,
176 + nativeEvent: AnyNativeEvent,
177 + nativeEventTarget: EventTarget,
178 + eventSystemFlags: EventSystemFlags,
179 +): void {
180 + const context = new DOMEventResponderContext(
181 + topLevelType,
182 + nativeEvent,
183 + nativeEventTarget,
184 + eventSystemFlags,
185 + );
186 + let node = targetFiber;
187 + // Traverse up the fiber tree till we find event component fibers.
188 + while (node !== null) {
189 + if (node.tag === EventComponent) {
190 + handleTopLevelType(topLevelType, node, context);
191 + }
192 + node = node.return;
193 + }
194 + context._runEventsInBatch();
195 +}
packages/react-dom/src/events/ReactDOMEventListener.js
+22 -13
@@ -12,7 +12,8 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber';
12 import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
13
14 import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
15 -import {runExtractedEventsInBatch} from 'events/EventPluginHub';
15 +import {runExtractedPluginEventsInBatch} from 'events/EventPluginHub';
16 +import {runResponderEventsInBatch} from '../events/DOMEventResponderSystem';
17 import {isFiberMounted} from 'react-reconciler/reflection';
18 import {HostRoot} from 'shared/ReactWorkTags';
19 import {
@@ -130,16 +131,27 @@ function handleTopLevel(bookKeeping: BookKeepingInstance) {
131
132 for (let i = 0; i < bookKeeping.ancestors.length; i++) {
133 targetInst = bookKeeping.ancestors[i];
133 - if (bookKeeping.eventSystemFlags === PLUGIN_EVENT_SYSTEM) {
134 - runExtractedEventsInBatch(
135 - ((bookKeeping.topLevelType: any): DOMTopLevelEventType),
134 + const eventSystemFlags = bookKeeping.eventSystemFlags;
135 + const eventTarget = getEventTarget(bookKeeping.nativeEvent);
136 + const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType);
137 + const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent);
138 +
139 + if (eventSystemFlags === PLUGIN_EVENT_SYSTEM) {
140 + runExtractedPluginEventsInBatch(
141 + topLevelType,
142 targetInst,
137 - ((bookKeeping.nativeEvent: any): AnyNativeEvent),
138 - getEventTarget(bookKeeping.nativeEvent),
143 + nativeEvent,
144 + eventTarget,
145 + );
146 + } else if (enableEventAPI && targetInst !== null) {
147 + // Responder event system (experimental event API)
148 + runResponderEventsInBatch(
149 + topLevelType,
150 + targetInst,
151 + nativeEvent,
152 + eventTarget,
153 + eventSystemFlags,
154 );
140 - } else {
141 - // RESPONDER_EVENT_SYSTEM
142 - // TODO: Add implementation
155 }
156 }
157 }
@@ -176,9 +188,6 @@ export function trapEventForResponderEventSystem(
188 passive: boolean,
189 ): void {
190 if (enableEventAPI) {
179 - const dispatch = isInteractiveTopLevelEventType(topLevelType)
180 - ? dispatchInteractiveEvent
181 - : dispatchEvent;
191 const rawEventName = getRawEventName(topLevelType);
192 let eventFlags = RESPONDER_EVENT_SYSTEM;
193
@@ -198,7 +207,7 @@ export function trapEventForResponderEventSystem(
207 eventFlags |= IS_ACTIVE;
208 }
209 // Check if interactive and wrap in interactiveUpdates
201 - const listener = dispatch.bind(null, topLevelType, eventFlags);
210 + const listener = dispatchEvent.bind(null, topLevelType, eventFlags);
211 addEventListener(element, rawEventName, listener, {
212 capture,
213 passive,
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js new
+259
@@ -0,0 +1,259 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactFeatureFlags;
14 +let ReactDOM;
15 +
16 +function createReactEventComponent(targetEventTypes, handleEvent) {
17 + const testEventResponder = {
18 + targetEventTypes,
19 + handleEvent,
20 + };
21 +
22 + return {
23 + $$typeof: Symbol.for('react.event_component'),
24 + props: null,
25 + responder: testEventResponder,
26 + };
27 +}
28 +
29 +function dispatchClickEvent(element) {
30 + const clickEvent = document.createEvent('Event');
31 + clickEvent.initEvent('click', true, true);
32 + element.dispatchEvent(clickEvent);
33 +}
34 +
35 +// This is a new feature in Fiber so I put it in its own test file. It could
36 +// probably move to one of the other test files once it is official.
37 +describe('DOMEventResponderSystem', () => {
38 + let container;
39 +
40 + beforeEach(() => {
41 + jest.resetModules();
42 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
43 + ReactFeatureFlags.enableEventAPI = true;
44 + React = require('react');
45 + ReactDOM = require('react-dom');
46 + container = document.createElement('div');
47 + document.body.appendChild(container);
48 + });
49 +
50 + afterEach(() => {
51 + document.body.removeChild(container);
52 + container = null;
53 + });
54 +
55 + it('the event responder handleEvent() function should fire on click event', () => {
56 + let eventResponderFiredCount = 0;
57 + let eventLog = [];
58 + const buttonRef = React.createRef();
59 +
60 + const ClickEventComponent = createReactEventComponent(
61 + ['click'],
62 + (context, props) => {
63 + eventResponderFiredCount++;
64 + eventLog.push({
65 + name: context.eventType,
66 + passive: context.isPassive(),
67 + passiveSupported: context.isPassiveSupported(),
68 + });
69 + },
70 + );
71 +
72 + const Test = () => (
73 + <ClickEventComponent>
74 + <button ref={buttonRef}>Click me!</button>
75 + </ClickEventComponent>
76 + );
77 +
78 + ReactDOM.render(<Test />, container);
79 + expect(container.innerHTML).toBe('<button>Click me!</button>');
80 +
81 + // Clicking the button should trigger the event responder handleEvent()
82 + let buttonElement = buttonRef.current;
83 + dispatchClickEvent(buttonElement);
84 + expect(eventResponderFiredCount).toBe(1);
85 + expect(eventLog.length).toBe(1);
86 + // JSDOM does not support passive events, so this will be false
87 + expect(eventLog[0]).toEqual({
88 + name: 'click',
89 + passive: false,
90 + passiveSupported: false,
91 + });
92 +
93 + // Unmounting the container and clicking should not increment anything
94 + ReactDOM.render(null, container);
95 + dispatchClickEvent(buttonElement);
96 + expect(eventResponderFiredCount).toBe(1);
97 +
98 + // Re-rendering the container and clicking should increase the counter again
99 + ReactDOM.render(<Test />, container);
100 + buttonElement = buttonRef.current;
101 + dispatchClickEvent(buttonElement);
102 + expect(eventResponderFiredCount).toBe(2);
103 + });
104 +
105 + it('the event responder handleEvent() function should fire on click event (passive events forced)', () => {
106 + // JSDOM does not support passive events, so this manually overrides the value to be true
107 + const checkPassiveEvents = require('react-dom/src/events/checkPassiveEvents');
108 + checkPassiveEvents.passiveBrowserEventsSupported = true;
109 +
110 + let eventLog = [];
111 + const buttonRef = React.createRef();
112 +
113 + const ClickEventComponent = createReactEventComponent(
114 + ['click'],
115 + (context, props) => {
116 + eventLog.push({
117 + name: context.eventType,
118 + passive: context.isPassive(),
119 + passiveSupported: context.isPassiveSupported(),
120 + });
121 + },
122 + );
123 +
124 + const Test = () => (
125 + <ClickEventComponent>
126 + <button ref={buttonRef}>Click me!</button>
127 + </ClickEventComponent>
128 + );
129 +
130 + ReactDOM.render(<Test />, container);
131 +
132 + // Clicking the button should trigger the event responder handleEvent()
133 + let buttonElement = buttonRef.current;
134 + dispatchClickEvent(buttonElement);
135 + expect(eventLog.length).toBe(1);
136 + expect(eventLog[0]).toEqual({
137 + name: 'click',
138 + passive: true,
139 + passiveSupported: true,
140 + });
141 + });
142 +
143 + it('nested event responders and their handleEvent() function should fire multiple times', () => {
144 + let eventResponderFiredCount = 0;
145 + let eventLog = [];
146 + const buttonRef = React.createRef();
147 +
148 + const ClickEventComponent = createReactEventComponent(
149 + ['click'],
150 + (context, props) => {
151 + eventResponderFiredCount++;
152 + eventLog.push({
153 + name: context.eventType,
154 + passive: context.isPassive(),
155 + passiveSupported: context.isPassiveSupported(),
156 + });
157 + },
158 + );
159 +
160 + const Test = () => (
161 + <ClickEventComponent>
162 + <ClickEventComponent>
163 + <button ref={buttonRef}>Click me!</button>
164 + </ClickEventComponent>
165 + </ClickEventComponent>
166 + );
167 +
168 + ReactDOM.render(<Test />, container);
169 +
170 + // Clicking the button should trigger the event responder handleEvent()
171 + let buttonElement = buttonRef.current;
172 + dispatchClickEvent(buttonElement);
173 + expect(eventResponderFiredCount).toBe(2);
174 + expect(eventLog.length).toBe(2);
175 + // JSDOM does not support passive events, so this will be false
176 + expect(eventLog[0]).toEqual({
177 + name: 'click',
178 + passive: false,
179 + passiveSupported: false,
180 + });
181 + expect(eventLog[1]).toEqual({
182 + name: 'click',
183 + passive: false,
184 + passiveSupported: false,
185 + });
186 + });
187 +
188 + it('nested event responders and their handleEvent() should fire in the correct order', () => {
189 + let eventLog = [];
190 + const buttonRef = React.createRef();
191 +
192 + const ClickEventComponentA = createReactEventComponent(
193 + ['click'],
194 + (context, props) => {
195 + eventLog.push('A');
196 + },
197 + );
198 +
199 + const ClickEventComponentB = createReactEventComponent(
200 + ['click'],
201 + (context, props) => {
202 + eventLog.push('B');
203 + },
204 + );
205 +
206 + const Test = () => (
207 + <ClickEventComponentA>
208 + <ClickEventComponentB>
209 + <button ref={buttonRef}>Click me!</button>
210 + </ClickEventComponentB>
211 + </ClickEventComponentA>
212 + );
213 +
214 + ReactDOM.render(<Test />, container);
215 +
216 + // Clicking the button should trigger the event responder handleEvent()
217 + let buttonElement = buttonRef.current;
218 + dispatchClickEvent(buttonElement);
219 +
220 + expect(eventLog).toEqual(['B', 'A']);
221 + });
222 +
223 + it('custom event dispatching for click -> magicClick works', () => {
224 + let eventLog = [];
225 + const buttonRef = React.createRef();
226 +
227 + const ClickEventComponent = createReactEventComponent(
228 + ['click'],
229 + (context, props) => {
230 + if (props.onMagicClick) {
231 + context.dispatchEvent(
232 + 'magicclick',
233 + props.onMagicClick,
234 + context.eventTarget,
235 + false,
236 + );
237 + }
238 + },
239 + );
240 +
241 + function handleMagicEvent(e) {
242 + eventLog.push('magic event fired', e.type);
243 + }
244 +
245 + const Test = () => (
246 + <ClickEventComponent onMagicClick={handleMagicEvent}>
247 + <button ref={buttonRef}>Click me!</button>
248 + </ClickEventComponent>
249 + );
250 +
251 + ReactDOM.render(<Test />, container);
252 +
253 + // Clicking the button should trigger the event responder handleEvent()
254 + let buttonElement = buttonRef.current;
255 + dispatchClickEvent(buttonElement);
256 +
257 + expect(eventLog).toEqual(['magic event fired', 'magicclick']);
258 + });
259 +});
packages/react-dom/src/fire/ReactFire.js
+3 -4
@@ -49,10 +49,9 @@ import {
49 enqueueStateRestore,
50 restoreStateIfNeeded,
51 } from 'events/ReactControlledComponent';
52 -import {
53 - injection as EventPluginHubInjection,
54 - runEventsInBatch,
55 -} from 'events/EventPluginHub';
52 +import {injection as EventPluginHubInjection} from 'events/EventPluginHub';
53 +
54 +import {runEventsInBatch} from 'events/EventBatching';
55 import {eventNameDispatchConfigs} from 'events/EventPluginRegistry';
56 import {
57 accumulateTwoPhaseDispatches,
packages/react-native-renderer/src/ReactFabricEventEmitter.js
+5 -2
@@ -9,7 +9,10 @@
9
10 import type {Fiber} from 'react-reconciler/src/ReactFiber';
11
12 -import {getListener, runExtractedEventsInBatch} from 'events/EventPluginHub';
12 +import {
13 + getListener,
14 + runExtractedPluginEventsInBatch,
15 +} from 'events/EventPluginHub';
16 import {registrationNameModules} from 'events/EventPluginRegistry';
17 import {batchedUpdates} from 'events/ReactGenericBatching';
18
@@ -25,7 +28,7 @@ export function dispatchEvent(
28 ) {
29 const targetFiber = (target: null | Fiber);
30 batchedUpdates(function() {
28 - runExtractedEventsInBatch(
31 + runExtractedPluginEventsInBatch(
32 topLevelType,
33 targetFiber,
34 nativeEvent,
packages/react-native-renderer/src/ReactNativeEventEmitter.js
+5 -2
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import {getListener, runExtractedEventsInBatch} from 'events/EventPluginHub';
10 +import {
11 + getListener,
12 + runExtractedPluginEventsInBatch,
13 +} from 'events/EventPluginHub';
14 import {registrationNameModules} from 'events/EventPluginRegistry';
15 import {batchedUpdates} from 'events/ReactGenericBatching';
16 import warningWithoutStack from 'shared/warningWithoutStack';
@@ -95,7 +98,7 @@ function _receiveRootNodeIDEvent(
98 const nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT;
99 const inst = getInstanceFromNode(rootNodeID);
100 batchedUpdates(function() {
98 - runExtractedEventsInBatch(
101 + runExtractedPluginEventsInBatch(
102 topLevelType,
103 inst,
104 nativeEvent,
packages/react-reconciler/src/ReactFiber.js
+4 -1
@@ -623,7 +623,10 @@ export function createFiberFromEventComponent(
623 const fiber = createFiber(EventComponent, pendingProps, key, mode);
624 fiber.elementType = eventComponent;
625 fiber.type = eventComponent;
626 - fiber.stateNode = new Map();
626 + fiber.stateNode = {
627 + props: pendingProps,
628 + state: null,
629 + };
630 fiber.expirationTime = expirationTime;
631 return fiber;
632 }
packages/react-reconciler/src/ReactFiberCompleteWork.js
+2
@@ -774,6 +774,8 @@ function completeWork(
774 popHostContext(workInProgress);
775 const rootContainerInstance = getRootHostContainer();
776 const responder = workInProgress.type.responder;
777 + // Update the props on the event component state node
778 + workInProgress.stateNode.props = newProps;
779 handleEventComponent(responder, rootContainerInstance, workInProgress);
780 }
781 break;
packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js
+1 -1
@@ -53,7 +53,7 @@ function initTestRenderer() {
53
54 // This is a new feature in Fiber so I put it in its own test file. It could
55 // probably move to one of the other test files once it is official.
56 -describe('ReactTopLevelText', () => {
56 +describe('ReactFiberEvents', () => {
57 describe('NoopRenderer', () => {
58 beforeEach(() => {
59 initNoopRenderer();