Propagate events based on component tree, not IDs
Ben Alpert committed
Oct 17, 2015 at 18:10 UTC
d11ccd6956ccdcb5801e3577d5385f7826ea085c
8 files changed
+172
-141
src/renderers/dom/client/ReactDOMTreeTraversal.js
+51
@@ -70,14 +70,65 @@ function isAncestor(instA, instB) {
70
return false;
71
}
72
73
+/**
74
+ * Return the parent instance of the passed-in instance.
75
+ */
76
function getParentInstance(inst) {
77
invariant('_nativeNode' in inst, 'getParentInstance: Invalid argument.');
78
79
return inst._nativeParent;
80
}
81
82
+/**
83
+ * Simulates the traversal of a two-phase, capture/bubble event dispatch.
84
+ */
85
+function traverseTwoPhase(inst, fn, arg) {
86
+ var path = [];
87
+ while (inst) {
88
+ path.push(inst);
89
+ inst = inst._nativeParent;
90
+ }
91
+ var i;
92
+ for (i = path.length; i-- > 0;) {
93
+ fn(path[i], false, arg);
94
+ }
95
+ for (i = 0; i < path.length; i++) {
96
+ fn(path[i], true, arg);
97
+ }
98
+}
99
+
100
+/**
101
+ * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
102
+ * should would receive a `mouseEnter` or `mouseLeave` event.
103
+ *
104
+ * Does not invoke the callback on the nearest common ancestor because nothing
105
+ * "entered" or "left" that element.
106
+ */
107
+function traverseEnterLeave(from, to, fn, argFrom, argTo) {
108
+ var common = from && to ? getLowestCommonAncestor(from, to) : null;
109
+ var pathFrom = [];
110
+ while (from && from !== common) {
111
+ pathFrom.push(from);
112
+ from = from._nativeParent;
113
+ }
114
+ var pathTo = [];
115
+ while (to && to !== common) {
116
+ pathTo.push(to);
117
+ to = to._nativeParent;
118
+ }
119
+ var i;
120
+ for (i = 0; i < pathFrom.length; i++) {
121
+ fn(pathFrom[i], true, argFrom);
122
+ }
123
+ for (i = pathTo.length; i-- > 0;) {
124
+ fn(pathTo[i], false, argTo);
125
+ }
126
+}
127
+
128
module.exports = {
129
isAncestor: isAncestor,
130
getLowestCommonAncestor: getLowestCommonAncestor,
131
getParentInstance: getParentInstance,
132
+ traverseTwoPhase: traverseTwoPhase,
133
+ traverseEnterLeave: traverseEnterLeave,
134
};
src/renderers/dom/shared/ReactDefaultInjection.js
-4
@@ -27,8 +27,6 @@ var ReactDOMTextComponent = require('ReactDOMTextComponent');
27
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
28
var ReactEventListener = require('ReactEventListener');
29
var ReactInjection = require('ReactInjection');
30
-var ReactInstanceHandles = require('ReactInstanceHandles');
31
-var ReactMount = require('ReactMount');
30
var ReactReconcileTransaction = require('ReactReconcileTransaction');
31
var SVGDOMPropertyConfig = require('SVGDOMPropertyConfig');
32
var SelectEventPlugin = require('SelectEventPlugin');
@@ -53,10 +51,8 @@ function inject() {
51
* Inject modules for resolving DOM hierarchy and plugin ordering.
52
*/
53
ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder);
56
- ReactInjection.EventPluginHub.injectInstanceHandle(ReactInstanceHandles);
54
ReactInjection.EventPluginUtils.injectComponentTree(ReactDOMComponentTree);
55
ReactInjection.EventPluginUtils.injectTreeTraversal(ReactDOMTreeTraversal);
59
- ReactInjection.EventPluginUtils.injectMount(ReactMount);
56
57
/**
58
* Some important event plugins included by default (without having to require
src/renderers/shared/event/EventPluginHub.js
-36
@@ -18,7 +18,6 @@ var ReactErrorUtils = require('ReactErrorUtils');
18
var accumulateInto = require('accumulateInto');
19
var forEachAccumulated = require('forEachAccumulated');
20
var invariant = require('invariant');
21
-var warning = require('warning');
21
22
/**
23
* Internal store for event listeners
@@ -54,23 +53,6 @@ var executeDispatchesAndReleaseTopLevel = function(e) {
53
return executeDispatchesAndRelease(e, false);
54
};
55
57
-/**
58
- * - `InstanceHandle`: [required] Module that performs logical traversals of DOM
59
- * hierarchy given ids of the logical DOM elements involved.
60
- */
61
-var InstanceHandle = null;
62
-
63
-function validateInstanceHandle() {
64
- var valid =
65
- InstanceHandle &&
66
- InstanceHandle.traverseTwoPhase &&
67
- InstanceHandle.traverseEnterLeave;
68
- warning(
69
- valid,
70
- 'InstanceHandle not injected before use!'
71
- );
72
-}
73
-
56
/**
57
* This is a unified interface for event plugins to be installed and configured.
58
*
@@ -100,24 +82,6 @@ var EventPluginHub = {
82
*/
83
injection: {
84
103
- /**
104
- * @param {object} InjectedInstanceHandle
105
- * @public
106
- */
107
- injectInstanceHandle: function(InjectedInstanceHandle) {
108
- InstanceHandle = InjectedInstanceHandle;
109
- if (__DEV__) {
110
- validateInstanceHandle();
111
- }
112
- },
113
-
114
- getInstanceHandle: function() {
115
- if (__DEV__) {
116
- validateInstanceHandle();
117
- }
118
- return InstanceHandle;
119
- },
120
-
85
/**
86
* @param {array} InjectedEventPluginOrder
87
* @public
src/renderers/shared/event/EventPluginUtils.js
+43
-43
@@ -24,11 +24,8 @@ var warning = require('warning');
24
/**
25
* - `ComponentTree`: [required] Module that can convert between React instances
26
* and actual node references.
27
- * - `Mount`: [required] Module that can convert between React dom IDs and
28
- * actual node references.
27
*/
28
var ComponentTree;
31
-var Mount;
29
var TreeTraversal;
30
var injection = {
31
injectComponentTree: function(Injected) {
@@ -43,16 +40,6 @@ var injection = {
40
);
41
}
42
},
46
- injectMount: function(Injected) {
47
- Mount = Injected;
48
- if (__DEV__) {
49
- warning(
50
- Injected && Injected.getNode && Injected.getID,
51
- 'EventPluginUtils.injection.injectMount(...): Injected Mount ' +
52
- 'module is missing getNode or getID.'
53
- );
54
- }
55
- },
43
injectTreeTraversal: function(Injected) {
44
TreeTraversal = Injected;
45
if (__DEV__) {
@@ -87,17 +74,20 @@ var validateEventDispatches;
74
if (__DEV__) {
75
validateEventDispatches = function(event) {
76
var dispatchListeners = event._dispatchListeners;
90
- var dispatchIDs = event._dispatchIDs;
77
+ var dispatchInstances = event._dispatchInstances;
78
79
var listenersIsArr = Array.isArray(dispatchListeners);
93
- var idsIsArr = Array.isArray(dispatchIDs);
94
- var IDsLen = idsIsArr ? dispatchIDs.length : dispatchIDs ? 1 : 0;
80
var listenersLen = listenersIsArr ?
81
dispatchListeners.length :
82
dispatchListeners ? 1 : 0;
83
84
+ var instancesIsArr = Array.isArray(dispatchInstances);
85
+ var instancesLen = instancesIsArr ?
86
+ dispatchInstances.length :
87
+ dispatchInstances ? 1 : 0;
88
+
89
warning(
100
- idsIsArr === listenersIsArr && IDsLen === listenersLen,
90
+ instancesIsArr === listenersIsArr && instancesLen === listenersLen,
91
'EventPluginUtils: Invalid `event`.'
92
);
93
};
@@ -108,20 +98,19 @@ if (__DEV__) {
98
* @param {SyntheticEvent} event SyntheticEvent to handle
99
* @param {boolean} simulated If the event is simulated (changes exn behavior)
100
* @param {function} listener Application-level callback
111
- * @param {string} domID DOM id to pass to the callback.
101
+ * @param {*} inst Internal component instance
102
*/
113
-function executeDispatch(event, simulated, listener, domID) {
103
+function executeDispatch(event, simulated, listener, inst) {
104
var type = event.type || 'unknown-event';
115
- event.currentTarget = EventPluginUtils.getNode(domID);
105
+ event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
106
if (simulated) {
107
ReactErrorUtils.invokeGuardedCallbackWithCatch(
108
type,
109
listener,
120
- event,
121
- domID
110
+ event
111
);
112
} else {
124
- ReactErrorUtils.invokeGuardedCallback(type, listener, event, domID);
113
+ ReactErrorUtils.invokeGuardedCallback(type, listener, event);
114
}
115
event.currentTarget = null;
116
}
@@ -131,7 +120,7 @@ function executeDispatch(event, simulated, listener, domID) {
120
*/
121
function executeDispatchesInOrder(event, simulated) {
122
var dispatchListeners = event._dispatchListeners;
134
- var dispatchIDs = event._dispatchIDs;
123
+ var dispatchInstances = event._dispatchInstances;
124
if (__DEV__) {
125
validateEventDispatches(event);
126
}
@@ -140,14 +129,19 @@ function executeDispatchesInOrder(event, simulated) {
129
if (event.isPropagationStopped()) {
130
break;
131
}
143
- // Listeners and IDs are two parallel arrays that are always in sync.
144
- executeDispatch(event, simulated, dispatchListeners[i], dispatchIDs[i]);
132
+ // Listeners and Instances are two parallel arrays that are always in sync.
133
+ executeDispatch(
134
+ event,
135
+ simulated,
136
+ dispatchListeners[i],
137
+ dispatchInstances[i]
138
+ );
139
}
140
} else if (dispatchListeners) {
147
- executeDispatch(event, simulated, dispatchListeners, dispatchIDs);
141
+ executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
142
}
143
event._dispatchListeners = null;
150
- event._dispatchIDs = null;
144
+ event._dispatchInstances = null;
145
}
146
147
/**
@@ -159,7 +153,7 @@ function executeDispatchesInOrder(event, simulated) {
153
*/
154
function executeDispatchesInOrderStopAtTrueImpl(event) {
155
var dispatchListeners = event._dispatchListeners;
162
- var dispatchIDs = event._dispatchIDs;
156
+ var dispatchInstances = event._dispatchInstances;
157
if (__DEV__) {
158
validateEventDispatches(event);
159
}
@@ -168,14 +162,14 @@ function executeDispatchesInOrderStopAtTrueImpl(event) {
162
if (event.isPropagationStopped()) {
163
break;
164
}
171
- // Listeners and IDs are two parallel arrays that are always in sync.
172
- if (dispatchListeners[i](event, dispatchIDs[i])) {
173
- return dispatchIDs[i];
165
+ // Listeners and Instances are two parallel arrays that are always in sync.
166
+ if (dispatchListeners[i](event, dispatchInstances[i])) {
167
+ return dispatchInstances[i];
168
}
169
}
170
} else if (dispatchListeners) {
177
- if (dispatchListeners(event, dispatchIDs)) {
178
- return dispatchIDs;
171
+ if (dispatchListeners(event, dispatchInstances)) {
172
+ return dispatchInstances;
173
}
174
}
175
return null;
@@ -186,7 +180,7 @@ function executeDispatchesInOrderStopAtTrueImpl(event) {
180
*/
181
function executeDispatchesInOrderStopAtTrue(event) {
182
var ret = executeDispatchesInOrderStopAtTrueImpl(event);
189
- event._dispatchIDs = null;
183
+ event._dispatchInstances = null;
184
event._dispatchListeners = null;
185
return ret;
186
}
@@ -205,16 +199,16 @@ function executeDirectDispatch(event) {
199
validateEventDispatches(event);
200
}
201
var dispatchListener = event._dispatchListeners;
208
- var dispatchID = event._dispatchIDs;
202
+ var dispatchInstance = event._dispatchInstances;
203
invariant(
204
!Array.isArray(dispatchListener),
205
'executeDirectDispatch(...): Invalid `event`.'
206
);
213
- var res = dispatchListener ?
214
- dispatchListener(event, dispatchID) :
215
- null;
207
+ event.currentTarget = EventPluginUtils.getNodeFromInstance(dispatchInstance);
208
+ var res = dispatchListener ? dispatchListener(event) : null;
209
+ event.curentTarget = null;
210
event._dispatchListeners = null;
217
- event._dispatchIDs = null;
211
+ event._dispatchInstances = null;
212
return res;
213
}
214
@@ -239,12 +233,12 @@ var EventPluginUtils = {
233
executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,
234
hasDispatches: hasDispatches,
235
242
- getNode: function(id) {
243
- return Mount.getNode(id);
244
- },
236
getInstanceFromNode: function(node) {
237
return ComponentTree.getInstanceFromNode(node);
238
},
239
+ getNodeFromInstance: function(node) {
240
+ return ComponentTree.getNodeFromInstance(node);
241
+ },
242
isAncestor: function(a, b) {
243
return TreeTraversal.isAncestor(a, b);
244
},
@@ -254,6 +248,12 @@ var EventPluginUtils = {
248
getParentInstance: function(inst) {
249
return TreeTraversal.getParentInstance(inst);
250
},
251
+ traverseTwoPhase: function(target, fn, arg) {
252
+ return TreeTraversal.traverseTwoPhase(target, fn, arg);
253
+ },
254
+ traverseEnterLeave: function(from, to, fn, argFrom, argTo) {
255
+ return TreeTraversal.traverseEnterLeave(from, to, fn, argFrom, argTo);
256
+ },
257
258
injection: injection,
259
};
src/renderers/shared/event/EventPropagators.js
+23
-24
@@ -13,27 +13,23 @@
13
14
var EventConstants = require('EventConstants');
15
var EventPluginHub = require('EventPluginHub');
16
-
17
-var warning = require('warning');
16
+var EventPluginUtils = require('EventPluginUtils');
17
18
var accumulateInto = require('accumulateInto');
19
var forEachAccumulated = require('forEachAccumulated');
20
+var warning = require('warning');
21
22
var PropagationPhases = EventConstants.PropagationPhases;
23
var getListener = EventPluginHub.getListener;
24
25
-function getID(inst) {
26
- return inst ? inst._rootNodeID : '';
27
-}
28
-
25
/**
26
* Some event types have a notion of different registration names for different
27
* "phases" of propagation. This finds listeners by a given phase.
28
*/
33
-function listenerAtPhase(id, event, propagationPhase) {
29
+function listenerAtPhase(inst, event, propagationPhase) {
30
var registrationName =
31
event.dispatchConfig.phasedRegistrationNames[propagationPhase];
36
- return getListener(id, registrationName);
32
+ return getListener(inst._rootNodeID, registrationName);
33
}
34
35
/**
@@ -42,19 +38,19 @@ function listenerAtPhase(id, event, propagationPhase) {
38
* Mutating the event's members allows us to not have to create a wrapping
39
* "dispatch" object that pairs the event with the listener.
40
*/
45
-function accumulateDirectionalDispatches(domID, upwards, event) {
41
+function accumulateDirectionalDispatches(inst, upwards, event) {
42
if (__DEV__) {
43
warning(
48
- domID,
49
- 'Dispatching id must not be null'
44
+ inst,
45
+ 'Dispatching inst must not be null'
46
);
47
}
48
var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
53
- var listener = listenerAtPhase(domID, event, phase);
49
+ var listener = listenerAtPhase(inst, event, phase);
50
if (listener) {
51
event._dispatchListeners =
52
accumulateInto(event._dispatchListeners, listener);
57
- event._dispatchIDs = accumulateInto(event._dispatchIDs, domID);
53
+ event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
54
}
55
}
56
@@ -67,8 +63,8 @@ function accumulateDirectionalDispatches(domID, upwards, event) {
63
*/
64
function accumulateTwoPhaseDispatchesSingle(event) {
65
if (event && event.dispatchConfig.phasedRegistrationNames) {
70
- EventPluginHub.injection.getInstanceHandle().traverseTwoPhase(
71
- getID(event._targetInst),
66
+ EventPluginUtils.traverseTwoPhase(
67
+ event._targetInst,
68
accumulateDirectionalDispatches,
69
event
70
);
@@ -80,8 +76,11 @@ function accumulateTwoPhaseDispatchesSingle(event) {
76
*/
77
function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
78
if (event && event.dispatchConfig.phasedRegistrationNames) {
83
- EventPluginHub.injection.getInstanceHandle().traverseTwoPhaseSkipTarget(
84
- getID(event._targetInst),
79
+ var targetInst = event._targetInst;
80
+ var parentInst =
81
+ targetInst ? EventPluginUtils.getParentInstance(targetInst) : null;
82
+ EventPluginUtils.traverseTwoPhase(
83
+ parentInst,
84
accumulateDirectionalDispatches,
85
event
86
);
@@ -94,14 +93,14 @@ function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
93
* registration names. Same as `accumulateDirectDispatchesSingle` but without
94
* requiring that the `dispatchMarker` be the same as the dispatched ID.
95
*/
97
-function accumulateDispatches(id, ignoredDirection, event) {
96
+function accumulateDispatches(inst, ignoredDirection, event) {
97
if (event && event.dispatchConfig.registrationName) {
98
var registrationName = event.dispatchConfig.registrationName;
100
- var listener = getListener(id, registrationName);
99
+ var listener = getListener(inst._rootNodeID, registrationName);
100
if (listener) {
101
event._dispatchListeners =
102
accumulateInto(event._dispatchListeners, listener);
104
- event._dispatchIDs = accumulateInto(event._dispatchIDs, id);
103
+ event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
104
}
105
}
106
}
@@ -113,7 +112,7 @@ function accumulateDispatches(id, ignoredDirection, event) {
112
*/
113
function accumulateDirectDispatchesSingle(event) {
114
if (event && event.dispatchConfig.registrationName) {
116
- accumulateDispatches(getID(event._targetInst), null, event);
115
+ accumulateDispatches(event._targetInst, null, event);
116
}
117
}
118
@@ -126,9 +125,9 @@ function accumulateTwoPhaseDispatchesSkipTarget(events) {
125
}
126
127
function accumulateEnterLeaveDispatches(leave, enter, from, to) {
129
- EventPluginHub.injection.getInstanceHandle().traverseEnterLeave(
130
- getID(from),
131
- getID(to),
128
+ EventPluginUtils.traverseEnterLeave(
129
+ from,
130
+ to,
131
accumulateDispatches,
132
leave,
133
enter
src/renderers/shared/event/eventPlugins/ResponderEventPlugin.js
+2
-21
@@ -352,31 +352,12 @@ function setResponderAndExtractTransfer(
352
} else {
353
EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent);
354
}
355
- var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
355
+ var wantsResponderInst = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
356
if (!shouldSetEvent.isPersistent()) {
357
shouldSetEvent.constructor.release(shouldSetEvent);
358
}
359
360
- if (!wantsResponderID) {
361
- return null;
362
- }
363
-
364
- // HACK: Temporary tree-crawling until executeDispatchesInOrderStopAtTrue
365
- // returns the instance instead of the ID.
366
- var wantsResponderInst;
367
- for (
368
- var inst = bubbleShouldSetFrom;
369
- inst;
370
- inst = EventPluginUtils.getParentInstance(inst)
371
- ) {
372
- if (wantsResponderID === inst._rootNodeID) {
373
- wantsResponderInst = inst;
374
- break;
375
- }
376
- }
377
- invariant(wantsResponderInst, 'Requested responder instance not found.');
378
-
379
- if (wantsResponderInst === responderInst) {
360
+ if (!wantsResponderInst || wantsResponderInst === responderInst) {
361
return null;
362
}
363
var extracted;
src/renderers/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js
+8
-13
@@ -341,19 +341,6 @@ describe('ResponderEventPlugin', function() {
341
ReactInstanceHandles = require('ReactInstanceHandles');
342
ResponderEventPlugin = require('ResponderEventPlugin');
343
344
- EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles);
345
-
346
- // Only needed because SyntheticEvent supports the `currentTarget`
347
- // property.
348
- EventPluginUtils.injection.injectMount({
349
- getNode: function(id) {
350
- return id;
351
- },
352
- getID: function(nodeHandle) {
353
- return nodeHandle;
354
- },
355
- });
356
-
344
EventPluginUtils.injection.injectComponentTree({
345
getInstanceFromNode: function(id) {
346
return idToInstance[id];
@@ -385,6 +372,14 @@ describe('ResponderEventPlugin', function() {
372
var parentID = id.substr(0, id.lastIndexOf('.'));
373
return idToInstance[parentID] || null;
374
},
375
+ traverseTwoPhase: function(target, fn, arg) {
376
+ ReactInstanceHandles.traverseTwoPhase(
377
+ target._rootNodeID,
378
+ function(id, upwards) {
379
+ fn(idToInstance[id], upwards, arg);
380
+ }
381
+ );
382
+ },
383
});
384
385
topLevelTypes = EventConstants.topLevelTypes;
src/renderers/shared/reconciler/__tests__/ReactInstanceHandles-test.js
+45
@@ -216,6 +216,51 @@ describe('ReactInstanceHandles', function() {
216
});
217
});
218
219
+ describe('traverseTwoPhaseSkipTarget', function() {
220
+ it('should not traverse when traversing outside DOM', function() {
221
+ var targetID = '';
222
+ var expectedAggregation = [];
223
+ ReactInstanceHandles.traverseTwoPhaseSkipTarget(
224
+ targetID,
225
+ argAggregator,
226
+ ARG
227
+ );
228
+ expect(aggregatedArgs).toEqual(expectedAggregation);
229
+ });
230
+
231
+ it('should traverse two phase across component boundary', function() {
232
+ var parent = renderParentIntoDocument();
233
+ var targetID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
234
+ var expectedAggregation = [
235
+ {id: getNodeID(parent.refs.P), isUp: false, arg: ARG},
236
+ {id: getNodeID(parent.refs.P_P1), isUp: false, arg: ARG},
237
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: false, arg: ARG},
238
+
239
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: true, arg: ARG},
240
+ {id: getNodeID(parent.refs.P_P1), isUp: true, arg: ARG},
241
+ {id: getNodeID(parent.refs.P), isUp: true, arg: ARG},
242
+ ];
243
+ ReactInstanceHandles.traverseTwoPhaseSkipTarget(
244
+ targetID,
245
+ argAggregator,
246
+ ARG
247
+ );
248
+ expect(aggregatedArgs).toEqual(expectedAggregation);
249
+ });
250
+
251
+ it('should traverse two phase at shallowest node', function() {
252
+ var parent = renderParentIntoDocument();
253
+ var targetID = getNodeID(parent.refs.P);
254
+ var expectedAggregation = [];
255
+ ReactInstanceHandles.traverseTwoPhaseSkipTarget(
256
+ targetID,
257
+ argAggregator,
258
+ ARG
259
+ );
260
+ expect(aggregatedArgs).toEqual(expectedAggregation);
261
+ });
262
+ });
263
+
264
describe('traverseEnterLeave', function() {
265
it('should not traverse when enter/leaving outside DOM', function() {
266
var targetID = '';