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();