Import ResponderEventPlugin from react-native
This is a direct import of the files from react-native, with the license headers updated.
Ben Alpert committed
Mar 31, 2015 at 15:01 UTC
6ec3b651690befb4226230855fa2e102654ad35f
8 files changed
+1831
-811
src/browser/eventPlugins/ResponderEventPlugin.js
deleted
-309
@@ -1,309 +0,0 @@
1
-/**
2
- * Copyright 2013-2015, Facebook, Inc.
3
- * All rights reserved.
4
- *
5
- * This source code is licensed under the BSD-style license found in the
6
- * LICENSE file in the root directory of this source tree. An additional grant
7
- * of patent rights can be found in the PATENTS file in the same directory.
8
- *
9
- * @providesModule ResponderEventPlugin
10
- */
11
-
12
-'use strict';
13
-
14
-var EventConstants = require('EventConstants');
15
-var EventPluginUtils = require('EventPluginUtils');
16
-var EventPropagators = require('EventPropagators');
17
-var SyntheticEvent = require('SyntheticEvent');
18
-
19
-var accumulateInto = require('accumulateInto');
20
-var keyOf = require('keyOf');
21
-
22
-var isStartish = EventPluginUtils.isStartish;
23
-var isMoveish = EventPluginUtils.isMoveish;
24
-var isEndish = EventPluginUtils.isEndish;
25
-var executeDirectDispatch = EventPluginUtils.executeDirectDispatch;
26
-var hasDispatches = EventPluginUtils.hasDispatches;
27
-var executeDispatchesInOrderStopAtTrue =
28
- EventPluginUtils.executeDispatchesInOrderStopAtTrue;
29
-
30
-/**
31
- * ID of element that should respond to touch/move types of interactions, as
32
- * indicated explicitly by relevant callbacks.
33
- */
34
-var responderID = null;
35
-var isPressing = false;
36
-
37
-var eventTypes = {
38
- /**
39
- * On a `touchStart`/`mouseDown`, is it desired that this element become the
40
- * responder?
41
- */
42
- startShouldSetResponder: {
43
- phasedRegistrationNames: {
44
- bubbled: keyOf({onStartShouldSetResponder: null}),
45
- captured: keyOf({onStartShouldSetResponderCapture: null})
46
- }
47
- },
48
-
49
- /**
50
- * On a `scroll`, is it desired that this element become the responder? This
51
- * is usually not needed, but should be used to retroactively infer that a
52
- * `touchStart` had occured during momentum scroll. During a momentum scroll,
53
- * a touch start will be immediately followed by a scroll event if the view is
54
- * currently scrolling.
55
- */
56
- scrollShouldSetResponder: {
57
- phasedRegistrationNames: {
58
- bubbled: keyOf({onScrollShouldSetResponder: null}),
59
- captured: keyOf({onScrollShouldSetResponderCapture: null})
60
- }
61
- },
62
-
63
- /**
64
- * On a `touchMove`/`mouseMove`, is it desired that this element become the
65
- * responder?
66
- */
67
- moveShouldSetResponder: {
68
- phasedRegistrationNames: {
69
- bubbled: keyOf({onMoveShouldSetResponder: null}),
70
- captured: keyOf({onMoveShouldSetResponderCapture: null})
71
- }
72
- },
73
-
74
- /**
75
- * Direct responder events dispatched directly to responder. Do not bubble.
76
- */
77
- responderMove: {registrationName: keyOf({onResponderMove: null})},
78
- responderRelease: {registrationName: keyOf({onResponderRelease: null})},
79
- responderTerminationRequest: {
80
- registrationName: keyOf({onResponderTerminationRequest: null})
81
- },
82
- responderGrant: {registrationName: keyOf({onResponderGrant: null})},
83
- responderReject: {registrationName: keyOf({onResponderReject: null})},
84
- responderTerminate: {registrationName: keyOf({onResponderTerminate: null})}
85
-};
86
-
87
-/**
88
- * Performs negotiation between any existing/current responder, checks to see if
89
- * any new entity is interested in becoming responder, performs that handshake
90
- * and returns any events that must be emitted to notify the relevant parties.
91
- *
92
- * A note about event ordering in the `EventPluginHub`.
93
- *
94
- * Suppose plugins are injected in the following order:
95
- *
96
- * `[R, S, C]`
97
- *
98
- * To help illustrate the example, assume `S` is `SimpleEventPlugin` (for
99
- * `onClick` etc) and `R` is `ResponderEventPlugin`.
100
- *
101
- * "Deferred-Dispatched Events":
102
- *
103
- * - The current event plugin system will traverse the list of injected plugins,
104
- * in order, and extract events by collecting the plugin's return value of
105
- * `extractEvents()`.
106
- * - These events that are returned from `extractEvents` are "deferred
107
- * dispatched events".
108
- * - When returned from `extractEvents`, deferred-dispatched events contain an
109
- * "accumulation" of deferred dispatches.
110
- * - These deferred dispatches are accumulated/collected before they are
111
- * returned, but processed at a later time by the `EventPluginHub` (hence the
112
- * name deferred).
113
- *
114
- * In the process of returning their deferred-dispatched events, event plugins
115
- * themselves can dispatch events on-demand without returning them from
116
- * `extractEvents`. Plugins might want to do this, so that they can use event
117
- * dispatching as a tool that helps them decide which events should be extracted
118
- * in the first place.
119
- *
120
- * "On-Demand-Dispatched Events":
121
- *
122
- * - On-demand-dispatched events are not returned from `extractEvents`.
123
- * - On-demand-dispatched events are dispatched during the process of returning
124
- * the deferred-dispatched events.
125
- * - They should not have side effects.
126
- * - They should be avoided, and/or eventually be replaced with another
127
- * abstraction that allows event plugins to perform multiple "rounds" of event
128
- * extraction.
129
- *
130
- * Therefore, the sequence of event dispatches becomes:
131
- *
132
- * - `R`s on-demand events (if any) (dispatched by `R` on-demand)
133
- * - `S`s on-demand events (if any) (dispatched by `S` on-demand)
134
- * - `C`s on-demand events (if any) (dispatched by `C` on-demand)
135
- * - `R`s extracted events (if any) (dispatched by `EventPluginHub`)
136
- * - `S`s extracted events (if any) (dispatched by `EventPluginHub`)
137
- * - `C`s extracted events (if any) (dispatched by `EventPluginHub`)
138
- *
139
- * In the case of `ResponderEventPlugin`: If the `startShouldSetResponder`
140
- * on-demand dispatch returns `true` (and some other details are satisfied) the
141
- * `onResponderGrant` deferred dispatched event is returned from
142
- * `extractEvents`. The sequence of dispatch executions in this case
143
- * will appear as follows:
144
- *
145
- * - `startShouldSetResponder` (`ResponderEventPlugin` dispatches on-demand)
146
- * - `touchStartCapture` (`EventPluginHub` dispatches as usual)
147
- * - `touchStart` (`EventPluginHub` dispatches as usual)
148
- * - `responderGrant/Reject` (`EventPluginHub` dispatches as usual)
149
- *
150
- * @param {string} topLevelType Record from `EventConstants`.
151
- * @param {string} topLevelTargetID ID of deepest React rendered element.
152
- * @param {object} nativeEvent Native browser event.
153
- * @return {*} An accumulation of synthetic events.
154
- */
155
-function setResponderAndExtractTransfer(
156
- topLevelType,
157
- topLevelTargetID,
158
- nativeEvent) {
159
- var shouldSetEventType =
160
- isStartish(topLevelType) ? eventTypes.startShouldSetResponder :
161
- isMoveish(topLevelType) ? eventTypes.moveShouldSetResponder :
162
- eventTypes.scrollShouldSetResponder;
163
-
164
- var bubbleShouldSetFrom = responderID || topLevelTargetID;
165
- var shouldSetEvent = SyntheticEvent.getPooled(
166
- shouldSetEventType,
167
- bubbleShouldSetFrom,
168
- nativeEvent
169
- );
170
- EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent);
171
- var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
172
- if (!shouldSetEvent.isPersistent()) {
173
- shouldSetEvent.constructor.release(shouldSetEvent);
174
- }
175
-
176
- if (!wantsResponderID || wantsResponderID === responderID) {
177
- return null;
178
- }
179
- var extracted;
180
- var grantEvent = SyntheticEvent.getPooled(
181
- eventTypes.responderGrant,
182
- wantsResponderID,
183
- nativeEvent
184
- );
185
-
186
- EventPropagators.accumulateDirectDispatches(grantEvent);
187
- if (responderID) {
188
- var terminationRequestEvent = SyntheticEvent.getPooled(
189
- eventTypes.responderTerminationRequest,
190
- responderID,
191
- nativeEvent
192
- );
193
- EventPropagators.accumulateDirectDispatches(terminationRequestEvent);
194
- var shouldSwitch = !hasDispatches(terminationRequestEvent) ||
195
- executeDirectDispatch(terminationRequestEvent);
196
- if (!terminationRequestEvent.isPersistent()) {
197
- terminationRequestEvent.constructor.release(terminationRequestEvent);
198
- }
199
-
200
- if (shouldSwitch) {
201
- var terminateType = eventTypes.responderTerminate;
202
- var terminateEvent = SyntheticEvent.getPooled(
203
- terminateType,
204
- responderID,
205
- nativeEvent
206
- );
207
- EventPropagators.accumulateDirectDispatches(terminateEvent);
208
- extracted = accumulateInto(extracted, [grantEvent, terminateEvent]);
209
- responderID = wantsResponderID;
210
- } else {
211
- var rejectEvent = SyntheticEvent.getPooled(
212
- eventTypes.responderReject,
213
- wantsResponderID,
214
- nativeEvent
215
- );
216
- EventPropagators.accumulateDirectDispatches(rejectEvent);
217
- extracted = accumulateInto(extracted, rejectEvent);
218
- }
219
- } else {
220
- extracted = accumulateInto(extracted, grantEvent);
221
- responderID = wantsResponderID;
222
- }
223
- return extracted;
224
-}
225
-
226
-/**
227
- * A transfer is a negotiation between a currently set responder and the next
228
- * element to claim responder status. Any start event could trigger a transfer
229
- * of responderID. Any move event could trigger a transfer, so long as there is
230
- * currently a responder set (in other words as long as the user is pressing
231
- * down).
232
- *
233
- * @param {string} topLevelType Record from `EventConstants`.
234
- * @return {boolean} True if a transfer of responder could possibly occur.
235
- */
236
-function canTriggerTransfer(topLevelType) {
237
- return topLevelType === EventConstants.topLevelTypes.topScroll ||
238
- isStartish(topLevelType) ||
239
- (isPressing && isMoveish(topLevelType));
240
-}
241
-
242
-/**
243
- * Event plugin for formalizing the negotiation between claiming locks on
244
- * receiving touches.
245
- */
246
-var ResponderEventPlugin = {
247
-
248
- getResponderID: function() {
249
- return responderID;
250
- },
251
-
252
- eventTypes: eventTypes,
253
-
254
- /**
255
- * @param {string} topLevelType Record from `EventConstants`.
256
- * @param {DOMEventTarget} topLevelTarget The listening component root node.
257
- * @param {string} topLevelTargetID ID of `topLevelTarget`.
258
- * @param {object} nativeEvent Native browser event.
259
- * @return {*} An accumulation of synthetic events.
260
- * @see {EventPluginHub.extractEvents}
261
- */
262
- extractEvents: function(
263
- topLevelType,
264
- topLevelTarget,
265
- topLevelTargetID,
266
- nativeEvent) {
267
- var extracted;
268
- // Must have missed an end event - reset the state here.
269
- if (responderID && isStartish(topLevelType)) {
270
- responderID = null;
271
- }
272
- if (isStartish(topLevelType)) {
273
- isPressing = true;
274
- } else if (isEndish(topLevelType)) {
275
- isPressing = false;
276
- }
277
- if (canTriggerTransfer(topLevelType)) {
278
- var transfer = setResponderAndExtractTransfer(
279
- topLevelType,
280
- topLevelTargetID,
281
- nativeEvent
282
- );
283
- if (transfer) {
284
- extracted = accumulateInto(extracted, transfer);
285
- }
286
- }
287
- // Now that we know the responder is set correctly, we can dispatch
288
- // responder type events (directly to the responder).
289
- var type = isMoveish(topLevelType) ? eventTypes.responderMove :
290
- isEndish(topLevelType) ? eventTypes.responderRelease :
291
- isStartish(topLevelType) ? eventTypes.responderStart : null;
292
- if (type) {
293
- var gesture = SyntheticEvent.getPooled(
294
- type,
295
- responderID || '',
296
- nativeEvent
297
- );
298
- EventPropagators.accumulateDirectDispatches(gesture);
299
- extracted = accumulateInto(extracted, gesture);
300
- }
301
- if (type === eventTypes.responderRelease) {
302
- responderID = null;
303
- }
304
- return extracted;
305
- }
306
-
307
-};
308
-
309
-module.exports = ResponderEventPlugin;
src/browser/eventPlugins/__tests__/ResponderEventPlugin-test.js
deleted
-494
@@ -1,494 +0,0 @@
1
-/**
2
- * Copyright 2013-2015, Facebook, Inc.
3
- * All rights reserved.
4
- *
5
- * This source code is licensed under the BSD-style license found in the
6
- * LICENSE file in the root directory of this source tree. An additional grant
7
- * of patent rights can be found in the PATENTS file in the same directory.
8
- *
9
- * @emails react-core
10
- */
11
-
12
-'use strict';
13
-
14
-var EventPluginHub;
15
-var EventConstants;
16
-var EventPropagators;
17
-var ReactInstanceHandles;
18
-var ResponderEventPlugin;
19
-var SyntheticEvent;
20
-
21
-var GRANDPARENT_ID = '.0';
22
-var PARENT_ID = '.0.0';
23
-var CHILD_ID = '.0.0.0';
24
-
25
-var topLevelTypes;
26
-var responderEventTypes;
27
-var spies;
28
-
29
-var DUMMY_NATIVE_EVENT = {};
30
-var DUMMY_RENDERED_TARGET = {};
31
-
32
-var onStartShouldSetResponder = function(id, cb, capture) {
33
- var registrationNames = responderEventTypes
34
- .startShouldSetResponder
35
- .phasedRegistrationNames;
36
- EventPluginHub.putListener(
37
- id,
38
- capture ? registrationNames.captured : registrationNames.bubbled,
39
- cb
40
- );
41
-};
42
-
43
-var onScrollShouldSetResponder = function(id, cb, capture) {
44
- var registrationNames = responderEventTypes
45
- .scrollShouldSetResponder
46
- .phasedRegistrationNames;
47
- EventPluginHub.putListener(
48
- id,
49
- capture ? registrationNames.captured : registrationNames.bubbled,
50
- cb
51
- );
52
-};
53
-
54
-var onMoveShouldSetResponder = function(id, cb, capture) {
55
- var registrationNames = responderEventTypes
56
- .moveShouldSetResponder
57
- .phasedRegistrationNames;
58
- EventPluginHub.putListener(
59
- id,
60
- capture ? registrationNames.captured : registrationNames.bubbled,
61
- cb
62
- );
63
-};
64
-
65
-
66
-var onResponderGrant = function(id, cb) {
67
- EventPluginHub.putListener(
68
- id,
69
- responderEventTypes.responderGrant.registrationName,
70
- cb
71
- );
72
-};
73
-
74
-var extractForTouchStart = function(renderedTargetID) {
75
- return ResponderEventPlugin.extractEvents(
76
- topLevelTypes.topTouchStart,
77
- DUMMY_NATIVE_EVENT,
78
- renderedTargetID,
79
- DUMMY_RENDERED_TARGET
80
- );
81
-};
82
-
83
-var extractForTouchMove = function(renderedTargetID) {
84
- return ResponderEventPlugin.extractEvents(
85
- topLevelTypes.topTouchMove,
86
- DUMMY_NATIVE_EVENT,
87
- renderedTargetID,
88
- DUMMY_RENDERED_TARGET
89
- );
90
-};
91
-
92
-var extractForTouchEnd = function(renderedTargetID) {
93
- return ResponderEventPlugin.extractEvents(
94
- topLevelTypes.topTouchEnd,
95
- DUMMY_NATIVE_EVENT,
96
- renderedTargetID,
97
- DUMMY_RENDERED_TARGET
98
- );
99
-};
100
-
101
-var extractForMouseDown = function(renderedTargetID) {
102
- return ResponderEventPlugin.extractEvents(
103
- topLevelTypes.topMouseDown,
104
- DUMMY_NATIVE_EVENT,
105
- renderedTargetID,
106
- DUMMY_RENDERED_TARGET
107
- );
108
-};
109
-
110
-var extractForMouseMove = function(renderedTargetID) {
111
- return ResponderEventPlugin.extractEvents(
112
- topLevelTypes.topMouseMove,
113
- DUMMY_NATIVE_EVENT,
114
- renderedTargetID,
115
- DUMMY_RENDERED_TARGET
116
- );
117
-};
118
-
119
-
120
-var extractForMouseUp = function(renderedTargetID) {
121
- return ResponderEventPlugin.extractEvents(
122
- topLevelTypes.topMouseUp,
123
- DUMMY_NATIVE_EVENT,
124
- renderedTargetID,
125
- DUMMY_RENDERED_TARGET
126
- );
127
-};
128
-
129
-var extractForScroll = function(renderedTargetID) {
130
- return ResponderEventPlugin.extractEvents(
131
- topLevelTypes.topScroll,
132
- DUMMY_NATIVE_EVENT,
133
- renderedTargetID,
134
- DUMMY_RENDERED_TARGET
135
- );
136
-};
137
-
138
-
139
-var onGrantChild;
140
-var onGrantParent;
141
-var onGrantGrandParent;
142
-
143
-
144
-var existsInExtraction = function(extracted, test) {
145
- if (Array.isArray(extracted)) {
146
- for (var i = 0; i < extracted.length; i++) {
147
- if (test(extracted[i])) {
148
- return true;
149
- }
150
- }
151
- } else if (extracted) {
152
- return test(extracted);
153
- }
154
- return false;
155
-};
156
-
157
-/**
158
- * Helper validators.
159
- */
160
-function assertGrantEvent(id, extracted) {
161
- var test = function(event) {
162
- return event instanceof SyntheticEvent &&
163
- event.dispatchConfig === responderEventTypes.responderGrant &&
164
- event.dispatchMarker === id;
165
- };
166
- expect(ResponderEventPlugin.getResponderID()).toBe(id);
167
- expect(existsInExtraction(extracted, test)).toBe(true);
168
-}
169
-
170
-function assertResponderMoveEvent(id, extracted) {
171
- var test = function(event) {
172
- return event instanceof SyntheticEvent &&
173
- event.dispatchConfig === responderEventTypes.responderMove &&
174
- event.dispatchMarker === id;
175
- };
176
- expect(ResponderEventPlugin.getResponderID()).toBe(id);
177
- expect(existsInExtraction(extracted, test)).toBe(true);
178
-}
179
-
180
-function assertTerminateEvent(id, extracted) {
181
- var test = function(event) {
182
- return event instanceof SyntheticEvent &&
183
- event.dispatchConfig === responderEventTypes.responderTerminate &&
184
- event.dispatchMarker === id;
185
- };
186
- expect(ResponderEventPlugin.getResponderID()).not.toBe(id);
187
- expect(existsInExtraction(extracted, test)).toBe(true);
188
-}
189
-
190
-function assertRelease(id, extracted) {
191
- var test = function(event) {
192
- return event instanceof SyntheticEvent &&
193
- event.dispatchConfig === responderEventTypes.responderRelease &&
194
- event.dispatchMarker === id;
195
- };
196
- expect(ResponderEventPlugin.getResponderID()).toBe(null);
197
- expect(existsInExtraction(extracted, test)).toBe(true);
198
-}
199
-
200
-
201
-function assertNothingExtracted(extracted) {
202
- expect(Array.isArray(extracted)).toBe(false); // No grant events.
203
- expect(Array.isArray(extracted)).toBeFalsy();
204
-}
205
-
206
-
207
-/**
208
- * TODO:
209
- * - Test that returning false from `responderTerminationRequest` will never
210
- * cause the responder to be lost.
211
- * - Automate some of this testing by providing config data - generalize.
212
- */
213
-
214
-describe('ResponderEventPlugin', function() {
215
- beforeEach(function() {
216
- require('mock-modules').dumpCache();
217
-
218
- EventPluginHub = require('EventPluginHub');
219
- EventConstants = require('EventConstants');
220
- EventPropagators = require('EventPropagators');
221
- ReactInstanceHandles = require('ReactInstanceHandles');
222
- ResponderEventPlugin = require('ResponderEventPlugin');
223
- SyntheticEvent = require('SyntheticEvent');
224
- EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles);
225
-
226
- // dumpCache, in open-source tests, only resets existing mocks. It does not
227
- // reset module-state though -- so we need to do this explicitly in the test
228
- // for now. Once that's no longer the case, we can delete this line.
229
- EventPluginHub.__purge();
230
-
231
- topLevelTypes = EventConstants.topLevelTypes;
232
- responderEventTypes = ResponderEventPlugin.eventTypes;
233
-
234
- spies = {
235
- onStartShouldSetResponderChild: function() {},
236
- onStartShouldSetResponderParent: function() {},
237
- onStartShouldSetResponderParentCapture: function() {},
238
- onStartShouldSetResponderGrandParent: function() {},
239
- onMoveShouldSetResponderParent: function() {},
240
- onScrollShouldSetResponderParent: function() {}
241
- };
242
-
243
- onGrantChild = function() {};
244
- onGrantParent = function() {};
245
- onGrantGrandParent = function() {};
246
- });
247
-
248
- it('should not auto-set responder on touch start', function() {
249
- // Notice we're not registering the startShould* handler.
250
- var extracted = extractForTouchStart(CHILD_ID);
251
- assertNothingExtracted(extracted);
252
- expect(ResponderEventPlugin.getResponderID()).toBe(null);
253
- });
254
-
255
- it('should not auto-set responder on mouse down', function() {
256
- // Notice we're not registering the startShould* handler.
257
- var extracted = extractForMouseDown(CHILD_ID);
258
- assertNothingExtracted(extracted);
259
- expect(ResponderEventPlugin.getResponderID()).toBe(null);
260
- extractForMouseUp(CHILD_ID); // Let up!
261
- expect(ResponderEventPlugin.getResponderID()).toBe(null);
262
-
263
- // Register `onMoveShould*` handler.
264
- spyOn(spies, 'onMoveShouldSetResponderParent').andReturn(true);
265
- onMoveShouldSetResponder(PARENT_ID, spies.onMoveShouldSetResponderParent);
266
- onResponderGrant(PARENT_ID, onGrantParent);
267
- // Move mouse while not pressing down
268
- extracted = extractForMouseMove(CHILD_ID);
269
- assertNothingExtracted(extracted);
270
- // Not going to call `onMoveShould`* if not touching.
271
- expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(0);
272
- expect(ResponderEventPlugin.getResponderID()).toBe(null);
273
-
274
- // Now try the move extraction again, this time while holding down, and not
275
- // letting up.
276
- extracted = extractForMouseDown(CHILD_ID);
277
- assertNothingExtracted(extracted);
278
- expect(ResponderEventPlugin.getResponderID()).toBe(null);
279
-
280
- // Now moving can set the responder, if pressing down, even if there is no
281
- // current responder.
282
- extracted = extractForMouseMove(CHILD_ID);
283
- expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(1);
284
- expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
285
- assertGrantEvent(PARENT_ID, extracted);
286
-
287
- extractForMouseUp(CHILD_ID);
288
- expect(ResponderEventPlugin.getResponderID()).toBe(null);
289
- });
290
-
291
- it('should not extract a grant/release event if double start', function() {
292
- // Return true - we should become the responder.
293
- var extracted;
294
- spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
295
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
296
- onResponderGrant(CHILD_ID, onGrantChild);
297
-
298
- extracted = extractForTouchStart(CHILD_ID);
299
- assertGrantEvent(CHILD_ID, extracted);
300
- expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
301
-
302
- // Now we do *not* clear out the touch via a simulated touch end. This mocks
303
- // out an environment that likely will never happen, but could in some odd
304
- // error state so it's nice to make sure we recover gracefully.
305
- // extractForTouchEnd(CHILD_ID); // Clear the responder
306
- extracted = extractForTouchStart(CHILD_ID);
307
- assertNothingExtracted();
308
- expect(spies.onStartShouldSetResponderChild.calls.length).toBe(2);
309
- });
310
-
311
- it('should bubble/capture responder on start', function() {
312
- // Return true - we should become the responder.
313
- var extracted;
314
- spyOn(spies, 'onStartShouldSetResponderParent').andReturn(true);
315
- spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
316
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
317
- onStartShouldSetResponder(PARENT_ID, spies.onStartShouldSetResponderParent);
318
- onResponderGrant(CHILD_ID, onGrantChild);
319
- onResponderGrant(PARENT_ID, onGrantParent);
320
-
321
- // Nothing extracted if no responder.
322
- extracted = extractForTouchMove(GRANDPARENT_ID);
323
- assertNothingExtracted(extracted);
324
-
325
- extracted = extractForTouchStart(CHILD_ID);
326
- assertGrantEvent(CHILD_ID, extracted);
327
- expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
328
- expect(spies.onStartShouldSetResponderParent.calls.length).toBe(0);
329
-
330
- // Even if moving on the grandparent, the child will receive responder moves
331
- // (This is even true for mouse interactions - which we should absolutely
332
- // test)
333
- extracted = extractForTouchMove(GRANDPARENT_ID);
334
- assertResponderMoveEvent(CHILD_ID, extracted);
335
- extracted = extractForTouchMove(CHILD_ID); // Test move on child node too.
336
- assertResponderMoveEvent(CHILD_ID, extracted);
337
-
338
- // Reset the responder - id passed here shouldn't matter:
339
- // TODO: Test varying the id here.
340
- extracted = extractForTouchEnd(GRANDPARENT_ID); // Clear the responder
341
- assertRelease(CHILD_ID, extracted);
342
-
343
- // Now make sure the parent requests responder on capture.
344
- spyOn(spies, 'onStartShouldSetResponderParentCapture').andReturn(true);
345
- onStartShouldSetResponder(
346
- PARENT_ID,
347
- spies.onStartShouldSetResponderParent,
348
- true // Capture
349
- );
350
- onResponderGrant(PARENT_ID, onGrantGrandParent);
351
- extracted = extractForTouchStart(PARENT_ID);
352
- expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
353
- assertGrantEvent(PARENT_ID, extracted);
354
- // Now move on various nodes, ensuring that the responder move is emitted to
355
- // the parent node.
356
- extracted = extractForTouchMove(GRANDPARENT_ID);
357
- assertResponderMoveEvent(PARENT_ID, extracted);
358
- extracted = extractForTouchMove(CHILD_ID); // Test move on child node too.
359
- assertResponderMoveEvent(PARENT_ID, extracted);
360
-
361
- // Reset the responder - id passed here shouldn't matter:
362
- // TODO: Test varying the id here.
363
- extracted = extractForTouchEnd(GRANDPARENT_ID); // Clear the responder
364
- assertRelease(PARENT_ID, extracted);
365
-
366
- });
367
-
368
- it('should invoke callback to ask if responder is desired', function() {
369
- // Return true - we should become the responder.
370
- spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
371
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
372
-
373
- var extracted = extractForTouchStart(CHILD_ID);
374
- assertNothingExtracted(extracted);
375
- expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
376
- expect(ResponderEventPlugin.getResponderID()).toBe(CHILD_ID);
377
- extractForTouchEnd(CHILD_ID); // Clear the responder
378
-
379
- // Now try returning false - we should not become the responder.
380
- spies.onStartShouldSetResponderChild.andReturn(false);
381
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
382
- extracted = extractForTouchStart(CHILD_ID);
383
- assertNothingExtracted(extracted);
384
- expect(spies.onStartShouldSetResponderChild.calls.length).toBe(2);
385
- expect(ResponderEventPlugin.getResponderID()).toBe(null);
386
- extractForTouchEnd(CHILD_ID);
387
- expect(ResponderEventPlugin.getResponderID()).toBe(null); // Still null
388
-
389
- // Same thing as before but return true from "shouldSet".
390
- spies.onStartShouldSetResponderChild.andReturn(true);
391
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
392
- onResponderGrant(CHILD_ID, onGrantChild);
393
- extracted = extractForTouchStart(CHILD_ID);
394
- expect(spies.onStartShouldSetResponderChild.calls.length).toBe(3);
395
- assertGrantEvent(CHILD_ID, extracted);
396
- extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
397
- assertRelease(CHILD_ID, extracted);
398
- });
399
-
400
- it('should give up responder to parent on move iff allowed', function() {
401
- // Return true - we should become the responder.
402
- var extracted;
403
- spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
404
- spyOn(spies, 'onMoveShouldSetResponderParent').andReturn(true);
405
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
406
- onMoveShouldSetResponder(PARENT_ID, spies.onMoveShouldSetResponderParent);
407
- onResponderGrant(CHILD_ID, onGrantChild);
408
- onResponderGrant(PARENT_ID, onGrantParent);
409
-
410
- spies.onStartShouldSetResponderChild.andReturn(true);
411
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
412
- extracted = extractForTouchStart(CHILD_ID);
413
- expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
414
- expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(0); // none yet
415
- assertGrantEvent(CHILD_ID, extracted); // Child is the current responder
416
-
417
- extracted = extractForTouchMove(CHILD_ID);
418
- expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(1);
419
- assertGrantEvent(PARENT_ID, extracted);
420
- assertTerminateEvent(CHILD_ID, extracted);
421
-
422
- extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
423
- assertRelease(PARENT_ID, extracted);
424
- });
425
-
426
- it('should responder move only on direct responder', function() {
427
- // Return true - we should become the responder.
428
- spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
429
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
430
-
431
- var extracted = extractForTouchStart(CHILD_ID);
432
- assertNothingExtracted(extracted);
433
- expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
434
- expect(ResponderEventPlugin.getResponderID()).toBe(CHILD_ID);
435
- extractForTouchEnd(CHILD_ID); // Clear the responder
436
- expect(ResponderEventPlugin.getResponderID()).toBe(null);
437
-
438
- // Now try returning false - we should not become the responder.
439
- spies.onStartShouldSetResponderChild.andReturn(false);
440
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
441
- extracted = extractForTouchStart(CHILD_ID);
442
- assertNothingExtracted(extracted);
443
- expect(spies.onStartShouldSetResponderChild.calls.length).toBe(2);
444
- expect(ResponderEventPlugin.getResponderID()).toBe(null);
445
- extractForTouchEnd(CHILD_ID); // Clear the responder
446
-
447
- // Same thing as before but return true from "shouldSet".
448
- spies.onStartShouldSetResponderChild.andReturn(true);
449
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
450
- onResponderGrant(CHILD_ID, onGrantChild);
451
- extracted = extractForTouchStart(CHILD_ID);
452
- expect(spies.onStartShouldSetResponderChild.calls.length).toBe(3);
453
- assertGrantEvent(CHILD_ID, extracted);
454
- extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
455
- assertRelease(CHILD_ID, extracted);
456
- });
457
-
458
- it('should give up responder to parent on scroll iff allowed', function() {
459
- // Return true - we should become the responder.
460
- var extracted;
461
- spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
462
- spyOn(spies, 'onMoveShouldSetResponderParent').andReturn(false);
463
- spyOn(spies, 'onScrollShouldSetResponderParent').andReturn(true);
464
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
465
- onMoveShouldSetResponder(PARENT_ID, spies.onMoveShouldSetResponderParent);
466
- onScrollShouldSetResponder(
467
- PARENT_ID,
468
- spies.onScrollShouldSetResponderParent
469
- );
470
- onResponderGrant(CHILD_ID, onGrantChild);
471
- onResponderGrant(PARENT_ID, onGrantParent);
472
-
473
- spies.onStartShouldSetResponderChild.andReturn(true);
474
- onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
475
- extracted = extractForTouchStart(CHILD_ID);
476
- expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
477
- expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(0); // none yet
478
- assertGrantEvent(CHILD_ID, extracted); // Child is the current responder
479
-
480
- extracted = extractForTouchMove(CHILD_ID);
481
- expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(1);
482
- assertNothingExtracted(extracted);
483
-
484
- extracted = extractForScroll(CHILD_ID); // Could have been parent here too.
485
- expect(spies.onScrollShouldSetResponderParent.calls.length).toBe(1);
486
- assertGrantEvent(PARENT_ID, extracted);
487
- assertTerminateEvent(CHILD_ID, extracted);
488
-
489
- extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
490
- assertRelease(PARENT_ID, extracted);
491
- });
492
-
493
-
494
-});
src/core/ReactInstanceHandles.js
+11
-2
@@ -115,8 +115,7 @@ function getNextDescendantID(ancestorID, destinationID) {
115
// Skip over the ancestor and the immediate separator. Traverse until we hit
116
// another separator or we reach the end of `destinationID`.
117
var start = ancestorID.length + SEPARATOR_LENGTH;
118
- var i;
119
- for (i = start; i < destinationID.length; i++) {
118
+ for (var i = start; i < destinationID.length; i++) {
119
if (isBoundary(destinationID, i)) {
120
break;
121
}
@@ -295,6 +294,16 @@ var ReactInstanceHandles = {
294
}
295
},
296
297
+ /**
298
+ * Same as `traverseTwoPhase` but skips the `targetID`.
299
+ */
300
+ traverseTwoPhaseSkipTarget: function(targetID, cb, arg) {
301
+ if (targetID) {
302
+ traverseParentPath('', targetID, cb, arg, true, true);
303
+ traverseParentPath(targetID, '', cb, arg, true, true);
304
+ }
305
+ },
306
+
307
/**
308
* Traverse a node ID, calling the supplied `cb` for each ancestor ID. For
309
* example, passing `.0.$row-0.1` would result in `cb` getting called
src/event/EventPropagators.js
+21
-6
@@ -14,8 +14,6 @@
14
var EventConstants = require('EventConstants');
15
var EventPluginHub = require('EventPluginHub');
16
17
-var warning = require('warning');
18
-
17
var accumulateInto = require('accumulateInto');
18
var forEachAccumulated = require('forEachAccumulated');
19
@@ -40,10 +38,9 @@ function listenerAtPhase(id, event, propagationPhase) {
38
*/
39
function accumulateDirectionalDispatches(domID, upwards, event) {
40
if (__DEV__) {
43
- warning(
44
- domID,
45
- 'Dispatching id must not be null'
46
- );
41
+ if (!domID) {
42
+ throw new Error('Dispatching id must not be null');
43
+ }
44
}
45
var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
46
var listener = listenerAtPhase(domID, event, phase);
@@ -71,6 +68,19 @@ function accumulateTwoPhaseDispatchesSingle(event) {
68
}
69
}
70
71
+/**
72
+ * Same as `accumulateTwoPhaseDispatchesSingle`, but skips over the targetID.
73
+ */
74
+function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
75
+ if (event && event.dispatchConfig.phasedRegistrationNames) {
76
+ EventPluginHub.injection.getInstanceHandle().traverseTwoPhaseSkipTarget(
77
+ event.dispatchMarker,
78
+ accumulateDirectionalDispatches,
79
+ event
80
+ );
81
+ }
82
+}
83
+
84
85
/**
86
* Accumulates without regard to direction, does not look for phased
@@ -104,6 +114,10 @@ function accumulateTwoPhaseDispatches(events) {
114
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
115
}
116
117
+function accumulateTwoPhaseDispatchesSkipTarget(events) {
118
+ forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget);
119
+}
120
+
121
function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) {
122
EventPluginHub.injection.getInstanceHandle().traverseEnterLeave(
123
fromID,
@@ -134,6 +148,7 @@ function accumulateDirectDispatches(events) {
148
*/
149
var EventPropagators = {
150
accumulateTwoPhaseDispatches: accumulateTwoPhaseDispatches,
151
+ accumulateTwoPhaseDispatchesSkipTarget: accumulateTwoPhaseDispatchesSkipTarget,
152
accumulateDirectDispatches: accumulateDirectDispatches,
153
accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches
154
};
src/event/eventPlugins/ResponderEventPlugin.js
new
+592
@@ -0,0 +1,592 @@
1
+/**
2
+ * Copyright 2013-2015, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @providesModule ResponderEventPlugin
10
+ */
11
+
12
+'use strict';
13
+
14
+var EventConstants = require('EventConstants');
15
+var EventPluginUtils = require('EventPluginUtils');
16
+var EventPropagators = require('EventPropagators');
17
+var NodeHandle = require('NodeHandle');
18
+var ReactInstanceHandles = require('ReactInstanceHandles');
19
+var ResponderSyntheticEvent = require('ResponderSyntheticEvent');
20
+var ResponderTouchHistoryStore = require('ResponderTouchHistoryStore');
21
+
22
+var accumulate = require('accumulate');
23
+var invariant = require('invariant');
24
+var keyOf = require('keyOf');
25
+
26
+var isStartish = EventPluginUtils.isStartish;
27
+var isMoveish = EventPluginUtils.isMoveish;
28
+var isEndish = EventPluginUtils.isEndish;
29
+var executeDirectDispatch = EventPluginUtils.executeDirectDispatch;
30
+var hasDispatches = EventPluginUtils.hasDispatches;
31
+var executeDispatchesInOrderStopAtTrue =
32
+ EventPluginUtils.executeDispatchesInOrderStopAtTrue;
33
+
34
+/**
35
+ * ID of element that should respond to touch/move types of interactions, as
36
+ * indicated explicitly by relevant callbacks.
37
+ */
38
+var responderID = null;
39
+
40
+/**
41
+ * Count of current touches. A textInput should become responder iff the
42
+ * the selection changes while there is a touch on the screen.
43
+ */
44
+var trackedTouchCount = 0;
45
+
46
+/**
47
+ * Last reported number of active touches.
48
+ */
49
+var previousActiveTouches = 0;
50
+
51
+var changeResponder = function(nextResponderID) {
52
+ var oldResponderID = responderID;
53
+ responderID = nextResponderID;
54
+ if (ResponderEventPlugin.GlobalResponderHandler !== null) {
55
+ ResponderEventPlugin.GlobalResponderHandler.onChange(
56
+ oldResponderID,
57
+ nextResponderID
58
+ );
59
+ }
60
+};
61
+
62
+var eventTypes = {
63
+ /**
64
+ * On a `touchStart`/`mouseDown`, is it desired that this element become the
65
+ * responder?
66
+ */
67
+ startShouldSetResponder: {
68
+ phasedRegistrationNames: {
69
+ bubbled: keyOf({onStartShouldSetResponder: null}),
70
+ captured: keyOf({onStartShouldSetResponderCapture: null})
71
+ }
72
+ },
73
+
74
+ /**
75
+ * On a `scroll`, is it desired that this element become the responder? This
76
+ * is usually not needed, but should be used to retroactively infer that a
77
+ * `touchStart` had occured during momentum scroll. During a momentum scroll,
78
+ * a touch start will be immediately followed by a scroll event if the view is
79
+ * currently scrolling.
80
+ *
81
+ * TODO: This shouldn't bubble.
82
+ */
83
+ scrollShouldSetResponder: {
84
+ phasedRegistrationNames: {
85
+ bubbled: keyOf({onScrollShouldSetResponder: null}),
86
+ captured: keyOf({onScrollShouldSetResponderCapture: null})
87
+ }
88
+ },
89
+
90
+ /**
91
+ * On text selection change, should this element become the responder? This
92
+ * is needed for text inputs or other views with native selection, so the
93
+ * JS view can claim the responder.
94
+ *
95
+ * TODO: This shouldn't bubble.
96
+ */
97
+ selectionChangeShouldSetResponder: {
98
+ phasedRegistrationNames: {
99
+ bubbled: keyOf({onSelectionChangeShouldSetResponder: null}),
100
+ captured: keyOf({onSelectionChangeShouldSetResponderCapture: null})
101
+ }
102
+ },
103
+
104
+ /**
105
+ * On a `touchMove`/`mouseMove`, is it desired that this element become the
106
+ * responder?
107
+ */
108
+ moveShouldSetResponder: {
109
+ phasedRegistrationNames: {
110
+ bubbled: keyOf({onMoveShouldSetResponder: null}),
111
+ captured: keyOf({onMoveShouldSetResponderCapture: null})
112
+ }
113
+ },
114
+
115
+ /**
116
+ * Direct responder events dispatched directly to responder. Do not bubble.
117
+ */
118
+ responderStart: {registrationName: keyOf({onResponderStart: null})},
119
+ responderMove: {registrationName: keyOf({onResponderMove: null})},
120
+ responderEnd: {registrationName: keyOf({onResponderEnd: null})},
121
+ responderRelease: {registrationName: keyOf({onResponderRelease: null})},
122
+ responderTerminationRequest: {
123
+ registrationName: keyOf({onResponderTerminationRequest: null})
124
+ },
125
+ responderGrant: {registrationName: keyOf({onResponderGrant: null})},
126
+ responderReject: {registrationName: keyOf({onResponderReject: null})},
127
+ responderTerminate: {registrationName: keyOf({onResponderTerminate: null})}
128
+};
129
+
130
+/**
131
+ *
132
+ * Responder System:
133
+ * ----------------
134
+ *
135
+ * - A global, solitary "interaction lock" on a view.
136
+ * - If a `NodeHandle` becomes the responder, it should convey visual feedback
137
+ * immediately to indicate so, either by highlighting or moving accordingly.
138
+ * - To be the responder means, that touches are exclusively important to that
139
+ * responder view, and no other view.
140
+ * - While touches are still occuring, the responder lock can be transfered to
141
+ * a new view, but only to increasingly "higher" views (meaning ancestors of
142
+ * the current responder).
143
+ *
144
+ * Responder being granted:
145
+ * ------------------------
146
+ *
147
+ * - Touch starts, moves, and scrolls can cause an ID to become the responder.
148
+ * - We capture/bubble `startShouldSetResponder`/`moveShouldSetResponder` to
149
+ * the "appropriate place".
150
+ * - If nothing is currently the responder, the "appropriate place" is the
151
+ * initiating event's `targetID`.
152
+ * - If something *is* already the responder, the "appropriate place" is the
153
+ * first common ancestor of the event target and the current `responderID`.
154
+ * - Some negotiation happens: See the timing diagram below.
155
+ * - Scrolled views automatically become responder. The reasoning is that a
156
+ * platform scroll view that isn't built on top of the responder system has
157
+ * began scrolling, and the active responder must now be notified that the
158
+ * interaction is no longer locked to it - the system has taken over.
159
+ *
160
+ * - Responder being released:
161
+ * As soon as no more touches that *started* inside of descendents of the
162
+ * *current* responderID, an `onResponderRelease` event is dispatched to the
163
+ * current responder, and the responder lock is released.
164
+ *
165
+ * TODO:
166
+ * - on "end", a callback hook for `onResponderEndShouldRemainResponder` that
167
+ * determines if the responder lock should remain.
168
+ * - If a view shouldn't "remain" the responder, any active touches should by
169
+ * default be considered "dead" and do not influence future negotiations or
170
+ * bubble paths. It should be as if those touches do not exist.
171
+ * -- For multitouch: Usually a translate-z will choose to "remain" responder
172
+ * after one out of many touches ended. For translate-y, usually the view
173
+ * doesn't wish to "remain" responder after one of many touches end.
174
+ * - Consider building this on top of a `stopPropagation` model similar to
175
+ * `W3C` events.
176
+ * - Ensure that `onResponderTerminate` is called on touch cancels, whether or
177
+ * not `onResponderTerminationRequest` returns `true` or `false`.
178
+ *
179
+ */
180
+
181
+/* Negotiation Performed
182
+ +-----------------------+
183
+ / \
184
+Process low level events to + Current Responder + wantsResponderID
185
+determine who to perform negot-| (if any exists at all) |
186
+iation/transition | Otherwise just pass through|
187
+-------------------------------+----------------------------+------------------+
188
+Bubble to find first ID | |
189
+to return true:wantsResponderID| |
190
+ | |
191
+ +-------------+ | |
192
+ | onTouchStart| | |
193
+ +------+------+ none | |
194
+ | return| |
195
++-----------v-------------+true| +------------------------+ |
196
+|onStartShouldSetResponder|----->|onResponderStart (cur) |<-----------+
197
++-----------+-------------+ | +------------------------+ | |
198
+ | | | +--------+-------+
199
+ | returned true for| false:REJECT +-------->|onResponderReject
200
+ | wantsResponderID | | | +----------------+
201
+ | (now attempt | +------------------+-----+ |
202
+ | handoff) | | onResponder | |
203
+ +------------------->| TerminationRequest| |
204
+ | +------------------+-----+ |
205
+ | | | +----------------+
206
+ | true:GRANT +-------->|onResponderGrant|
207
+ | | +--------+-------+
208
+ | +------------------------+ | |
209
+ | | onResponderTerminate |<-----------+
210
+ | +------------------+-----+ |
211
+ | | | +----------------+
212
+ | +-------->|onResponderStart|
213
+ | | +----------------+
214
+Bubble to find first ID | |
215
+to return true:wantsResponderID| |
216
+ | |
217
+ +-------------+ | |
218
+ | onTouchMove | | |
219
+ +------+------+ none | |
220
+ | return| |
221
++-----------v-------------+true| +------------------------+ |
222
+|onMoveShouldSetResponder |----->|onResponderMove (cur) |<-----------+
223
++-----------+-------------+ | +------------------------+ | |
224
+ | | | +--------+-------+
225
+ | returned true for| false:REJECT +-------->|onResponderRejec|
226
+ | wantsResponderID | | | +----------------+
227
+ | (now attempt | +------------------+-----+ |
228
+ | handoff) | | onResponder | |
229
+ +------------------->| TerminationRequest| |
230
+ | +------------------+-----+ |
231
+ | | | +----------------+
232
+ | true:GRANT +-------->|onResponderGrant|
233
+ | | +--------+-------+
234
+ | +------------------------+ | |
235
+ | | onResponderTerminate |<-----------+
236
+ | +------------------+-----+ |
237
+ | | | +----------------+
238
+ | +-------->|onResponderMove |
239
+ | | +----------------+
240
+ | |
241
+ | |
242
+ Some active touch started| |
243
+ inside current responder | +------------------------+ |
244
+ +------------------------->| onResponderEnd | |
245
+ | | +------------------------+ |
246
+ +---+---------+ | |
247
+ | onTouchEnd | | |
248
+ +---+---------+ | |
249
+ | | +------------------------+ |
250
+ +------------------------->| onResponderEnd | |
251
+ No active touches started| +-----------+------------+ |
252
+ inside current responder | | |
253
+ | v |
254
+ | +------------------------+ |
255
+ | | onResponderRelease | |
256
+ | +------------------------+ |
257
+ | |
258
+ + + */
259
+
260
+
261
+
262
+/**
263
+ * A note about event ordering in the `EventPluginHub`.
264
+ *
265
+ * Suppose plugins are injected in the following order:
266
+ *
267
+ * `[R, S, C]`
268
+ *
269
+ * To help illustrate the example, assume `S` is `SimpleEventPlugin` (for
270
+ * `onClick` etc) and `R` is `ResponderEventPlugin`.
271
+ *
272
+ * "Deferred-Dispatched Events":
273
+ *
274
+ * - The current event plugin system will traverse the list of injected plugins,
275
+ * in order, and extract events by collecting the plugin's return value of
276
+ * `extractEvents()`.
277
+ * - These events that are returned from `extractEvents` are "deferred
278
+ * dispatched events".
279
+ * - When returned from `extractEvents`, deferred-dispatched events contain an
280
+ * "accumulation" of deferred dispatches.
281
+ * - These deferred dispatches are accumulated/collected before they are
282
+ * returned, but processed at a later time by the `EventPluginHub` (hence the
283
+ * name deferred).
284
+ *
285
+ * In the process of returning their deferred-dispatched events, event plugins
286
+ * themselves can dispatch events on-demand without returning them from
287
+ * `extractEvents`. Plugins might want to do this, so that they can use event
288
+ * dispatching as a tool that helps them decide which events should be extracted
289
+ * in the first place.
290
+ *
291
+ * "On-Demand-Dispatched Events":
292
+ *
293
+ * - On-demand-dispatched events are not returned from `extractEvents`.
294
+ * - On-demand-dispatched events are dispatched during the process of returning
295
+ * the deferred-dispatched events.
296
+ * - They should not have side effects.
297
+ * - They should be avoided, and/or eventually be replaced with another
298
+ * abstraction that allows event plugins to perform multiple "rounds" of event
299
+ * extraction.
300
+ *
301
+ * Therefore, the sequence of event dispatches becomes:
302
+ *
303
+ * - `R`s on-demand events (if any) (dispatched by `R` on-demand)
304
+ * - `S`s on-demand events (if any) (dispatched by `S` on-demand)
305
+ * - `C`s on-demand events (if any) (dispatched by `C` on-demand)
306
+ * - `R`s extracted events (if any) (dispatched by `EventPluginHub`)
307
+ * - `S`s extracted events (if any) (dispatched by `EventPluginHub`)
308
+ * - `C`s extracted events (if any) (dispatched by `EventPluginHub`)
309
+ *
310
+ * In the case of `ResponderEventPlugin`: If the `startShouldSetResponder`
311
+ * on-demand dispatch returns `true` (and some other details are satisfied) the
312
+ * `onResponderGrant` deferred dispatched event is returned from
313
+ * `extractEvents`. The sequence of dispatch executions in this case
314
+ * will appear as follows:
315
+ *
316
+ * - `startShouldSetResponder` (`ResponderEventPlugin` dispatches on-demand)
317
+ * - `touchStartCapture` (`EventPluginHub` dispatches as usual)
318
+ * - `touchStart` (`EventPluginHub` dispatches as usual)
319
+ * - `responderGrant/Reject` (`EventPluginHub` dispatches as usual)
320
+ *
321
+ * @param {string} topLevelType Record from `EventConstants`.
322
+ * @param {string} topLevelTargetID ID of deepest React rendered element.
323
+ * @param {object} nativeEvent Native browser event.
324
+ * @return {*} An accumulation of synthetic events.
325
+ */
326
+function setResponderAndExtractTransfer(
327
+ topLevelType,
328
+ topLevelTargetID,
329
+ nativeEvent) {
330
+ var shouldSetEventType =
331
+ isStartish(topLevelType) ? eventTypes.startShouldSetResponder :
332
+ isMoveish(topLevelType) ? eventTypes.moveShouldSetResponder :
333
+ topLevelType === EventConstants.topLevelTypes.topSelectionChange ?
334
+ eventTypes.selectionChangeShouldSetResponder :
335
+ eventTypes.scrollShouldSetResponder;
336
+
337
+ // TODO: stop one short of the the current responder.
338
+ var bubbleShouldSetFrom = !responderID ?
339
+ topLevelTargetID :
340
+ ReactInstanceHandles._getFirstCommonAncestorID(responderID, topLevelTargetID);
341
+
342
+ // When capturing/bubbling the "shouldSet" event, we want to skip the target
343
+ // (deepest ID) if it happens to be the current responder. The reasoning:
344
+ // It's strange to get an `onMoveShouldSetResponder` when you're *already*
345
+ // the responder.
346
+ var skipOverBubbleShouldSetFrom = bubbleShouldSetFrom === responderID;
347
+ var shouldSetEvent = ResponderSyntheticEvent.getPooled(
348
+ shouldSetEventType,
349
+ bubbleShouldSetFrom,
350
+ nativeEvent
351
+ );
352
+ shouldSetEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
353
+ if (skipOverBubbleShouldSetFrom) {
354
+ EventPropagators.accumulateTwoPhaseDispatchesSkipTarget(shouldSetEvent);
355
+ } else {
356
+ EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent);
357
+ }
358
+ var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
359
+ if (!shouldSetEvent.isPersistent()) {
360
+ shouldSetEvent.constructor.release(shouldSetEvent);
361
+ }
362
+
363
+ if (!wantsResponderID || wantsResponderID === responderID) {
364
+ return null;
365
+ }
366
+ var extracted;
367
+ var grantEvent = ResponderSyntheticEvent.getPooled(
368
+ eventTypes.responderGrant,
369
+ wantsResponderID,
370
+ nativeEvent
371
+ );
372
+ grantEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
373
+
374
+ EventPropagators.accumulateDirectDispatches(grantEvent);
375
+ if (responderID) {
376
+
377
+ var terminationRequestEvent = ResponderSyntheticEvent.getPooled(
378
+ eventTypes.responderTerminationRequest,
379
+ responderID,
380
+ nativeEvent
381
+ );
382
+ terminationRequestEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
383
+ EventPropagators.accumulateDirectDispatches(terminationRequestEvent);
384
+ var shouldSwitch = !hasDispatches(terminationRequestEvent) ||
385
+ executeDirectDispatch(terminationRequestEvent);
386
+ if (!terminationRequestEvent.isPersistent()) {
387
+ terminationRequestEvent.constructor.release(terminationRequestEvent);
388
+ }
389
+
390
+ if (shouldSwitch) {
391
+ var terminateType = eventTypes.responderTerminate;
392
+ var terminateEvent = ResponderSyntheticEvent.getPooled(
393
+ terminateType,
394
+ responderID,
395
+ nativeEvent
396
+ );
397
+ terminateEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
398
+ EventPropagators.accumulateDirectDispatches(terminateEvent);
399
+ extracted = accumulate(extracted, [grantEvent, terminateEvent]);
400
+ changeResponder(wantsResponderID);
401
+ } else {
402
+ var rejectEvent = ResponderSyntheticEvent.getPooled(
403
+ eventTypes.responderReject,
404
+ wantsResponderID,
405
+ nativeEvent
406
+ );
407
+ rejectEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
408
+ EventPropagators.accumulateDirectDispatches(rejectEvent);
409
+ extracted = accumulate(extracted, rejectEvent);
410
+ }
411
+ } else {
412
+ extracted = accumulate(extracted, grantEvent);
413
+ changeResponder(wantsResponderID);
414
+ }
415
+ return extracted;
416
+}
417
+
418
+/**
419
+ * A transfer is a negotiation between a currently set responder and the next
420
+ * element to claim responder status. Any start event could trigger a transfer
421
+ * of responderID. Any move event could trigger a transfer.
422
+ *
423
+ * @param {string} topLevelType Record from `EventConstants`.
424
+ * @return {boolean} True if a transfer of responder could possibly occur.
425
+ */
426
+function canTriggerTransfer(topLevelType, topLevelTargetID) {
427
+ return topLevelTargetID && (
428
+ topLevelType === EventConstants.topLevelTypes.topScroll ||
429
+ (trackedTouchCount > 0 &&
430
+ topLevelType === EventConstants.topLevelTypes.topSelectionChange) ||
431
+ isStartish(topLevelType) ||
432
+ isMoveish(topLevelType)
433
+ );
434
+}
435
+
436
+/**
437
+ * Returns whether or not this touch end event makes it such that there are no
438
+ * longer any touches that started inside of the current `responderID`.
439
+ *
440
+ * @param {NativeEvent} nativeEvent Native touch end event.
441
+ * @return {bool} Whether or not this touch end event ends the responder.
442
+ */
443
+function noResponderTouches(nativeEvent) {
444
+ var touches = nativeEvent.touches;
445
+ if (!touches || touches.length === 0) {
446
+ return true;
447
+ }
448
+ for (var i = 0; i < touches.length; i++) {
449
+ var activeTouch = touches[i];
450
+ var target = activeTouch.target;
451
+ if (target !== null && target !== undefined && target !== 0) {
452
+ // Is the original touch location inside of the current responder?
453
+ var commonAncestor =
454
+ ReactInstanceHandles._getFirstCommonAncestorID(
455
+ responderID,
456
+ NodeHandle.getRootNodeID(target)
457
+ );
458
+ if (commonAncestor === responderID) {
459
+ return false;
460
+ }
461
+ }
462
+ }
463
+ return true;
464
+}
465
+
466
+
467
+var ResponderEventPlugin = {
468
+
469
+ getResponderID: function() {
470
+ return responderID;
471
+ },
472
+
473
+ eventTypes: eventTypes,
474
+
475
+ /**
476
+ * We must be resilient to `topLevelTargetID` being `undefined` on
477
+ * `touchMove`, or `touchEnd`. On certain platforms, this means that a native
478
+ * scroll has assumed control and the original touch targets are destroyed.
479
+ *
480
+ * @param {string} topLevelType Record from `EventConstants`.
481
+ * @param {DOMEventTarget} topLevelTarget The listening component root node.
482
+ * @param {string} topLevelTargetID ID of `topLevelTarget`.
483
+ * @param {object} nativeEvent Native browser event.
484
+ * @return {*} An accumulation of synthetic events.
485
+ * @see {EventPluginHub.extractEvents}
486
+ */
487
+ extractEvents: function(
488
+ topLevelType,
489
+ topLevelTarget,
490
+ topLevelTargetID,
491
+ nativeEvent) {
492
+
493
+ if (isStartish(topLevelType)) {
494
+ trackedTouchCount += 1;
495
+ } else if (isEndish(topLevelType)) {
496
+ trackedTouchCount -= 1;
497
+ invariant(
498
+ trackedTouchCount >= 0,
499
+ 'Ended a touch event which was not counted in trackedTouchCount.'
500
+ );
501
+ }
502
+
503
+ ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent);
504
+
505
+ var extracted = canTriggerTransfer(topLevelType, topLevelTargetID) ?
506
+ setResponderAndExtractTransfer(topLevelType, topLevelTargetID, nativeEvent) :
507
+ null;
508
+ // Responder may or may not have transfered on a new touch start/move.
509
+ // Regardless, whoever is the responder after any potential transfer, we
510
+ // direct all touch start/move/ends to them in the form of
511
+ // `onResponderMove/Start/End`. These will be called for *every* additional
512
+ // finger that move/start/end, dispatched directly to whoever is the
513
+ // current responder at that moment, until the responder is "released".
514
+ //
515
+ // These multiple individual change touch events are are always bookended
516
+ // by `onResponderGrant`, and one of
517
+ // (`onResponderRelease/onResponderTerminate`).
518
+ var isResponderTouchStart = responderID && isStartish(topLevelType);
519
+ var isResponderTouchMove = responderID && isMoveish(topLevelType);
520
+ var isResponderTouchEnd = responderID && isEndish(topLevelType);
521
+ var incrementalTouch =
522
+ isResponderTouchStart ? eventTypes.responderStart :
523
+ isResponderTouchMove ? eventTypes.responderMove :
524
+ isResponderTouchEnd ? eventTypes.responderEnd :
525
+ null;
526
+
527
+ if (incrementalTouch) {
528
+ var gesture =
529
+ ResponderSyntheticEvent.getPooled(incrementalTouch, responderID, nativeEvent);
530
+ gesture.touchHistory = ResponderTouchHistoryStore.touchHistory;
531
+ EventPropagators.accumulateDirectDispatches(gesture);
532
+ extracted = accumulate(extracted, gesture);
533
+ }
534
+
535
+ var isResponderTerminate =
536
+ responderID &&
537
+ topLevelType === EventConstants.topLevelTypes.topTouchCancel;
538
+ var isResponderRelease =
539
+ responderID &&
540
+ !isResponderTerminate &&
541
+ isEndish(topLevelType) &&
542
+ noResponderTouches(nativeEvent);
543
+ var finalTouch =
544
+ isResponderTerminate ? eventTypes.responderTerminate :
545
+ isResponderRelease ? eventTypes.responderRelease :
546
+ null;
547
+ if (finalTouch) {
548
+ var finalEvent =
549
+ ResponderSyntheticEvent.getPooled(finalTouch, responderID, nativeEvent);
550
+ finalEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
551
+ EventPropagators.accumulateDirectDispatches(finalEvent);
552
+ extracted = accumulate(extracted, finalEvent);
553
+ changeResponder(null);
554
+ }
555
+
556
+ var numberActiveTouches =
557
+ ResponderTouchHistoryStore.touchHistory.numberActiveTouches;
558
+ if (ResponderEventPlugin.GlobalInteractionHandler &&
559
+ numberActiveTouches !== previousActiveTouches) {
560
+ ResponderEventPlugin.GlobalInteractionHandler.onChange(
561
+ numberActiveTouches
562
+ );
563
+ }
564
+ previousActiveTouches = numberActiveTouches;
565
+
566
+ return extracted;
567
+ },
568
+
569
+ GlobalResponderHandler: null,
570
+ GlobalInteractionHandler: null,
571
+
572
+ injection: {
573
+ /**
574
+ * @param {{onChange: (ReactID, ReactID) => void} GlobalResponderHandler
575
+ * Object that handles any change in responder. Use this to inject
576
+ * integration with an existing touch handling system etc.
577
+ */
578
+ injectGlobalResponderHandler: function(GlobalResponderHandler) {
579
+ ResponderEventPlugin.GlobalResponderHandler = GlobalResponderHandler;
580
+ },
581
+
582
+ /**
583
+ * @param {{onChange: (numberActiveTouches) => void} GlobalInteractionHandler
584
+ * Object that handles any change in the number of active touches.
585
+ */
586
+ injectGlobalInteractionHandler: function(GlobalInteractionHandler) {
587
+ ResponderEventPlugin.GlobalInteractionHandler = GlobalInteractionHandler;
588
+ },
589
+ }
590
+};
591
+
592
+module.exports = ResponderEventPlugin;
src/event/eventPlugins/ResponderSyntheticEvent.js
new
+40
@@ -0,0 +1,40 @@
1
+/**
2
+ * Copyright 2013-2015, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @providesModule ResponderSyntheticEvent
10
+ * @typechecks static-only
11
+ */
12
+
13
+"use strict";
14
+
15
+var SyntheticEvent = require('SyntheticEvent');
16
+
17
+/**
18
+ * `touchHistory` isn't actually on the native event, but putting it in the
19
+ * interface will ensure that it is cleaned up when pooled/destroyed. The
20
+ * `ResponderEventPlugin` will populate it appropriately.
21
+ */
22
+var ResponderEventInterface = {
23
+ touchHistory: function(nativeEvent) {
24
+ return null; // Actually doesn't even look at the native event.
25
+ },
26
+};
27
+
28
+/**
29
+ * @param {object} dispatchConfig Configuration used to dispatch this event.
30
+ * @param {string} dispatchMarker Marker identifying the event target.
31
+ * @param {object} nativeEvent Native event.
32
+ * @extends {SyntheticEvent}
33
+ */
34
+function ResponderSyntheticEvent(dispatchConfig, dispatchMarker, nativeEvent) {
35
+ SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);
36
+}
37
+
38
+SyntheticEvent.augmentClass(ResponderSyntheticEvent, ResponderEventInterface);
39
+
40
+module.exports = ResponderSyntheticEvent;
src/event/eventPlugins/ResponderTouchHistoryStore.js
new
+185
@@ -0,0 +1,185 @@
1
+/**
2
+ * Copyright 2013-2015, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @providesModule ResponderTouchHistoryStore
10
+ */
11
+
12
+"use strict";
13
+
14
+var EventPluginUtils = require('EventPluginUtils');
15
+
16
+var invariant = require('invariant');
17
+
18
+var isMoveish = EventPluginUtils.isMoveish;
19
+var isStartish = EventPluginUtils.isStartish;
20
+var isEndish = EventPluginUtils.isEndish;
21
+
22
+var MAX_TOUCH_BANK = 20;
23
+
24
+/**
25
+ * Touch position/time tracking information by touchID. Typically, we'll only
26
+ * see IDs with a range of 1-20 (they are recycled when touches end and then
27
+ * start again). This data is commonly needed by many different interaction
28
+ * logic modules so precomputing it is very helpful to do once.
29
+ * Each touch object in `touchBank` is of the following form:
30
+ * { touchActive: boolean,
31
+ * startTimeStamp: number,
32
+ * startPageX: number,
33
+ * startPageY: number,
34
+ * currentPageX: number,
35
+ * currentPageY: number,
36
+ * currentTimeStamp: number
37
+ * }
38
+ */
39
+var touchHistory = {
40
+ touchBank: [ ],
41
+ numberActiveTouches: 0,
42
+ // If there is only one active touch, we remember its location. This prevents
43
+ // us having to loop through all of the touches all the time in the most
44
+ // common case.
45
+ indexOfSingleActiveTouch: -1,
46
+ mostRecentTimeStamp: 0,
47
+};
48
+
49
+var timestampForTouch = function(touch) {
50
+ // The legacy internal implementation provides "timeStamp", which has been
51
+ // renamed to "timestamp". Let both work for now while we iron it out
52
+ // TODO (evv): rename timeStamp to timestamp in internal code
53
+ return touch.timeStamp || touch.timestamp;
54
+};
55
+
56
+/**
57
+ * TODO: Instead of making gestures recompute filtered velocity, we could
58
+ * include a built in velocity computation that can be reused globally.
59
+ * @param {Touch} touch Native touch object.
60
+ */
61
+var initializeTouchData = function(touch) {
62
+ return {
63
+ touchActive: true,
64
+ startTimeStamp: timestampForTouch(touch),
65
+ startPageX: touch.pageX,
66
+ startPageY: touch.pageY,
67
+ currentPageX: touch.pageX,
68
+ currentPageY: touch.pageY,
69
+ currentTimeStamp: timestampForTouch(touch),
70
+ previousPageX: touch.pageX,
71
+ previousPageY: touch.pageY,
72
+ previousTimeStamp: timestampForTouch(touch),
73
+ };
74
+};
75
+
76
+var reinitializeTouchTrack = function(touchTrack, touch) {
77
+ touchTrack.touchActive = true;
78
+ touchTrack.startTimeStamp = timestampForTouch(touch);
79
+ touchTrack.startPageX = touch.pageX;
80
+ touchTrack.startPageY = touch.pageY;
81
+ touchTrack.currentPageX = touch.pageX;
82
+ touchTrack.currentPageY = touch.pageY;
83
+ touchTrack.currentTimeStamp = timestampForTouch(touch);
84
+ touchTrack.previousPageX = touch.pageX;
85
+ touchTrack.previousPageY = touch.pageY;
86
+ touchTrack.previousTimeStamp = timestampForTouch(touch);
87
+};
88
+
89
+var validateTouch = function(touch) {
90
+ var identifier = touch.identifier;
91
+ invariant(identifier != null, 'Touch object is missing identifier');
92
+ if (identifier > MAX_TOUCH_BANK) {
93
+ console.warn(
94
+ 'Touch identifier ' + identifier + ' is greater than maximum ' +
95
+ 'supported ' + MAX_TOUCH_BANK + ' which causes performance issues ' +
96
+ 'backfilling array locations for all of the indices.'
97
+ );
98
+ }
99
+};
100
+
101
+var recordStartTouchData = function(touch) {
102
+ var touchBank = touchHistory.touchBank;
103
+ var identifier = touch.identifier;
104
+ var touchTrack = touchBank[identifier];
105
+ if (__DEV__) {
106
+ validateTouch(touch);
107
+ }
108
+ if (!touchTrack) {
109
+ touchBank[touch.identifier] = initializeTouchData(touch);
110
+ } else {
111
+ reinitializeTouchTrack(touchTrack, touch);
112
+ }
113
+ touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
114
+};
115
+
116
+var recordMoveTouchData = function(touch) {
117
+ var touchBank = touchHistory.touchBank;
118
+ var touchTrack = touchBank[touch.identifier];
119
+ if (__DEV__) {
120
+ validateTouch(touch);
121
+ invariant(touchTrack, 'Touch data should have been recorded on start');
122
+ }
123
+ touchTrack.touchActive = true;
124
+ touchTrack.previousPageX = touchTrack.currentPageX;
125
+ touchTrack.previousPageY = touchTrack.currentPageY;
126
+ touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
127
+ touchTrack.currentPageX = touch.pageX;
128
+ touchTrack.currentPageY = touch.pageY;
129
+ touchTrack.currentTimeStamp = timestampForTouch(touch);
130
+ touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
131
+};
132
+
133
+var recordEndTouchData = function(touch) {
134
+ var touchBank = touchHistory.touchBank;
135
+ var touchTrack = touchBank[touch.identifier];
136
+ if (__DEV__) {
137
+ validateTouch(touch);
138
+ invariant(touchTrack, 'Touch data should have been recorded on start');
139
+ }
140
+ touchTrack.previousPageX = touchTrack.currentPageX;
141
+ touchTrack.previousPageY = touchTrack.currentPageY;
142
+ touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
143
+ touchTrack.currentPageX = touch.pageX;
144
+ touchTrack.currentPageY = touch.pageY;
145
+ touchTrack.currentTimeStamp = timestampForTouch(touch);
146
+ touchTrack.touchActive = false;
147
+ touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
148
+};
149
+
150
+var ResponderTouchHistoryStore = {
151
+ recordTouchTrack: function(topLevelType, nativeEvent) {
152
+ var touchBank = touchHistory.touchBank;
153
+ if (isMoveish(topLevelType)) {
154
+ nativeEvent.changedTouches.forEach(recordMoveTouchData);
155
+ } else if (isStartish(topLevelType)) {
156
+ nativeEvent.changedTouches.forEach(recordStartTouchData);
157
+ touchHistory.numberActiveTouches = nativeEvent.touches.length;
158
+ if (touchHistory.numberActiveTouches === 1) {
159
+ touchHistory.indexOfSingleActiveTouch = nativeEvent.touches[0].identifier;
160
+ }
161
+ } else if (isEndish(topLevelType)) {
162
+ nativeEvent.changedTouches.forEach(recordEndTouchData);
163
+ touchHistory.numberActiveTouches = nativeEvent.touches.length;
164
+ if (touchHistory.numberActiveTouches === 1) {
165
+ for (var i = 0; i < touchBank.length; i++) {
166
+ var touchTrackToCheck = touchBank[i];
167
+ if (touchTrackToCheck != null && touchTrackToCheck.touchActive) {
168
+ touchHistory.indexOfSingleActiveTouch = i;
169
+ break;
170
+ }
171
+ }
172
+ if (__DEV__) {
173
+ var activeTouchData = touchBank[touchHistory.indexOfSingleActiveTouch];
174
+ var foundActive = activeTouchData != null && !!activeTouchData.touchActive;
175
+ invariant(foundActive, 'Cannot find single active touch');
176
+ }
177
+ }
178
+ }
179
+ },
180
+
181
+ touchHistory: touchHistory,
182
+};
183
+
184
+
185
+module.exports = ResponderTouchHistoryStore;
src/event/eventPlugins/__tests__/ResponderEventPlugin-test.js
new
+982
@@ -0,0 +1,982 @@
1
+/**
2
+ * Copyright 2013-2015, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @emails react-core
10
+ */
11
+
12
+'use strict';
13
+
14
+jest.autoMockOff();
15
+
16
+var EventPluginHub;
17
+var EventConstants;
18
+var EventPropagators;
19
+var NodeHandle;
20
+var ReactInstanceHandles;
21
+var ResponderEventPlugin;
22
+var SyntheticEvent;
23
+var EventPluginUtils;
24
+
25
+var GRANDPARENT_ID = '.0';
26
+var PARENT_ID = '.0.0';
27
+var CHILD_ID = '.0.0.0';
28
+var CHILD_ID2 = '.0.0.1';
29
+
30
+var topLevelTypes;
31
+var responderEventTypes;
32
+
33
+var eachKeyVal = require('eachKeyVal');
34
+
35
+var touch = function(nodeHandle, i) {
36
+ return {target: nodeHandle, identifier: i};
37
+};
38
+
39
+/**
40
+ * @param {NodeHandle} nodeHandle @see NodeHandle. Handle of target.
41
+ * @param {Array<Touch>} touches All active touches.
42
+ * @param {Array<Touch>} changedTouches Only the touches that have changed.
43
+ * @return {TouchEvent} Model of a touch event that is compliant with responder
44
+ * system plugin.
45
+ */
46
+var touchEvent = function(nodeHandle, touches, changedTouches) {
47
+ return {
48
+ target: nodeHandle,
49
+ changedTouches: changedTouches,
50
+ touches: touches
51
+ };
52
+};
53
+
54
+var subsequence = function(arr, indices) {
55
+ var ret = [];
56
+ for (var i = 0; i < indices.length; i++) {
57
+ var index = indices[i];
58
+ ret.push(arr[index]);
59
+ }
60
+ return ret;
61
+};
62
+
63
+var antiSubsequence = function(arr, indices) {
64
+ var ret = [];
65
+ for (var i = 0; i < arr.length; i++) {
66
+ if (indices.indexOf(i) === -1) {
67
+ ret.push(arr[i]);
68
+ }
69
+ }
70
+ return ret;
71
+};
72
+
73
+/**
74
+ * Helper for creating touch test config data.
75
+ * @param allTouchHandles
76
+ */
77
+var _touchConfig =
78
+ function(topType, targetNodeHandle, allTouchHandles, changedIndices) {
79
+ var allTouchObjects = allTouchHandles.map(touch);
80
+ var changedTouchObjects = subsequence(allTouchObjects, changedIndices);
81
+ var activeTouchObjects =
82
+ topType === 'topTouchStart' ? allTouchObjects :
83
+ topType === 'topTouchMove' ? allTouchObjects :
84
+ topType === 'topTouchEnd' ? antiSubsequence(allTouchObjects, changedIndices) :
85
+ topType === 'topTouchCancel' ? antiSubsequence(allTouchObjects, changedIndices) :
86
+ null;
87
+
88
+ return {
89
+ nativeEvent: touchEvent(
90
+ targetNodeHandle,
91
+ activeTouchObjects,
92
+ changedTouchObjects
93
+ ),
94
+ topLevelType: topType,
95
+ target: targetNodeHandle, // Because of our injected `NodeHandle` impl.
96
+ targetID: targetNodeHandle,
97
+ };
98
+};
99
+
100
+/**
101
+ * Creates test data for touch events using environment agnostic "node
102
+ * handles".
103
+ *
104
+ * @param {NodeHandle} nodeHandle Environment agnostic handle to DOM node.
105
+ * @param {Array<NodeHandle>} allTouchHandles Encoding of all "touches" in the
106
+ * form of a mapping from integer (touch `identifier`) to touch target. This is
107
+ * encoded in array form. Because of this, it is possible for two separate
108
+ * touches (meaning two separate indices) to have the same touch target ID -
109
+ * this corresponds to real world cases where two separate unique touches have
110
+ * the same target. These touches don't just represent all active touches,
111
+ * rather it also includes any touches that are not active, but are in the
112
+ * process of being removed.
113
+ * @param {Array<NodeHandle>} changedIndices Indices of `allTouchHandles` that
114
+ * have changed.
115
+ * @return {object} Config data used by test cases for extracting responder
116
+ * events.
117
+ */
118
+var startConfig = function(nodeHandle, allTouchHandles, changedIndices) {
119
+ return _touchConfig(
120
+ topLevelTypes.topTouchStart,
121
+ nodeHandle,
122
+ allTouchHandles,
123
+ changedIndices
124
+ );
125
+};
126
+
127
+/**
128
+ * @see `startConfig`
129
+ */
130
+var moveConfig = function(nodeHandle, allTouchHandles, changedIndices) {
131
+ return _touchConfig(
132
+ topLevelTypes.topTouchMove,
133
+ nodeHandle,
134
+ allTouchHandles,
135
+ changedIndices
136
+ );
137
+};
138
+
139
+/**
140
+ * @see `startConfig`
141
+ */
142
+var endConfig = function(nodeHandle, allTouchHandles, changedIndices) {
143
+ return _touchConfig(
144
+ topLevelTypes.topTouchEnd,
145
+ nodeHandle,
146
+ allTouchHandles,
147
+ changedIndices
148
+ );
149
+};
150
+
151
+/**
152
+ * Test config for events that aren't negotiation related, but rather result of
153
+ * a negotiation.
154
+ *
155
+ * Returns object of the form:
156
+ *
157
+ * {
158
+ * responderReject: {
159
+ * // Whatever "readableIDToID" was passed in.
160
+ * grandParent: {order: NA, assertEvent: null, returnVal: blah},
161
+ * ...
162
+ * child: {order: NA, assertEvent: null, returnVal: blah},
163
+ * }
164
+ * responderGrant: {
165
+ * grandParent: {order: NA, assertEvent: null, returnVal: blah},
166
+ * ...
167
+ * child: {order: NA, assertEvent: null, returnVal: blah}
168
+ * }
169
+ * ...
170
+ * }
171
+ *
172
+ * After this is created, a test case would configure specific event orderings
173
+ * and optional assertions. Anything left with an `order` of `NA` will be
174
+ * required to never be invoked (the test runner will make sure it throws if
175
+ * ever invoked).
176
+ *
177
+ */
178
+var NA = -1;
179
+var oneEventLoopTestConfig = function(readableIDToID) {
180
+ var ret = {
181
+ // Negotiation
182
+ scrollShouldSetResponder: {bubbled: {}, captured: {}},
183
+ startShouldSetResponder: {bubbled: {}, captured: {}},
184
+ moveShouldSetResponder: {bubbled: {}, captured: {}},
185
+ responderTerminationRequest: {},
186
+
187
+ // Non-negotiation
188
+ responderReject: {}, // These do not bubble capture.
189
+ responderGrant: {},
190
+ responderStart: {},
191
+ responderMove: {},
192
+ responderTerminate: {},
193
+ responderEnd: {},
194
+ responderRelease: {},
195
+ };
196
+ for (var eventName in ret) {
197
+ for (var readableNodeName in readableIDToID) {
198
+ if (ret[eventName].bubbled) {
199
+ // Two phase
200
+ ret[eventName].bubbled[readableNodeName] =
201
+ {order: NA, assertEvent: null, returnVal: undefined};
202
+ ret[eventName].captured[readableNodeName] =
203
+ {order: NA, assertEvent: null, returnVal: undefined};
204
+ } else {
205
+ ret[eventName][readableNodeName] =
206
+ {order: NA, assertEvent: null, returnVal: undefined};
207
+ }
208
+ }
209
+ }
210
+ return ret;
211
+};
212
+
213
+/**
214
+ * @param {object} sequence See `oneEventLoopTestConfig`.
215
+ */
216
+var registerTestHandlers = function(eventTestConfig, readableIDToID) {
217
+ var runs = {dispatchCount: 0};
218
+ var neverFire = function (readableID, registrationName) {
219
+ runs.dispatchCount++;
220
+ expect('').toBe(
221
+ 'Event type: ' + registrationName +
222
+ '\nShould never occur on:' + readableID +
223
+ '\nFor event test config:\n' + JSON.stringify(eventTestConfig) + '\n'
224
+ );
225
+ };
226
+ var registerOneEventType = function(registrationName, eventTypeTestConfig) {
227
+ for (var readableID in eventTypeTestConfig) {
228
+ var nodeConfig = eventTypeTestConfig[readableID];
229
+ var id = readableIDToID[readableID];
230
+ var handler = nodeConfig.order === NA ? neverFire.bind(null, readableID, registrationName) :
231
+ function(readableID, registrationName, nodeConfig, e) {
232
+ expect(
233
+ readableID + '->' + registrationName + ' index:' + runs.dispatchCount++
234
+ ).toBe(
235
+ readableID + '->' + registrationName + ' index:' + nodeConfig.order
236
+ );
237
+ nodeConfig.assertEvent && nodeConfig.assertEvent(e);
238
+ return nodeConfig.returnVal;
239
+ }.bind(null, readableID, registrationName, nodeConfig);
240
+ EventPluginHub.putListener(id, registrationName, handler);
241
+ }
242
+ };
243
+ eachKeyVal(eventTestConfig, function(eventName, oneEventTypeTestConfig) {
244
+ var hasTwoPhase = !!oneEventTypeTestConfig.bubbled;
245
+ if (hasTwoPhase) {
246
+ registerOneEventType(
247
+ ResponderEventPlugin.eventTypes[eventName].phasedRegistrationNames.bubbled,
248
+ oneEventTypeTestConfig.bubbled
249
+ );
250
+ registerOneEventType(
251
+ ResponderEventPlugin.eventTypes[eventName].phasedRegistrationNames.captured,
252
+ oneEventTypeTestConfig.captured
253
+ );
254
+ } else {
255
+ registerOneEventType(
256
+ ResponderEventPlugin.eventTypes[eventName].registrationName,
257
+ oneEventTypeTestConfig
258
+ );
259
+ }
260
+ });
261
+ return runs;
262
+};
263
+
264
+
265
+
266
+
267
+var run = function(config, hierarchyConfig, nativeEventConfig) {
268
+ var max = NA;
269
+ var searchForMax = function(nodeConfig) {
270
+ for (var readableID in nodeConfig) {
271
+ var order = nodeConfig[readableID].order;
272
+ max = order > max ? order : max;
273
+ }
274
+ };
275
+ for (var eventName in config) {
276
+ var eventConfig = config[eventName];
277
+ if (eventConfig.bubbled) {
278
+ searchForMax(eventConfig.bubbled);
279
+ searchForMax(eventConfig.captured);
280
+ } else {
281
+ searchForMax(eventConfig);
282
+ }
283
+ }
284
+
285
+ // Register the handlers
286
+ var runData = registerTestHandlers(config, hierarchyConfig);
287
+
288
+ // Trigger the event
289
+ var extractedEvents = ResponderEventPlugin.extractEvents(
290
+ nativeEventConfig.topLevelType,
291
+ nativeEventConfig.target,
292
+ nativeEventConfig.targetID,
293
+ nativeEventConfig.nativeEvent
294
+ );
295
+
296
+ // At this point the negotiation events have been dispatched as part of the
297
+ // extraction process, but not the side effectful events. Below, we dispatch
298
+ // side effectful events.
299
+ EventPluginHub.enqueueEvents(extractedEvents);
300
+ EventPluginHub.processEventQueue();
301
+
302
+ // Ensure that every event that declared an `order`, was actually dispatched.
303
+ expect(
304
+ 'number of events dispatched:' + runData.dispatchCount
305
+ ).toBe(
306
+ 'number of events dispatched:' + (max + 1)
307
+ ); // +1 for extra ++
308
+};
309
+
310
+var three = {
311
+ grandParent: GRANDPARENT_ID,
312
+ parent: PARENT_ID,
313
+ child: CHILD_ID,
314
+};
315
+
316
+var siblings = {
317
+ parent: PARENT_ID,
318
+ childOne: CHILD_ID,
319
+ childTwo: CHILD_ID2,
320
+};
321
+
322
+describe('ResponderEventPlugin', function() {
323
+ beforeEach(function() {
324
+ jest.resetModuleRegistry();
325
+
326
+ EventConstants = require('EventConstants');
327
+ EventPluginHub = require('EventPluginHub');
328
+ EventPluginUtils = require('EventPluginUtils');
329
+ EventPropagators = require('EventPropagators');
330
+ NodeHandle = require('NodeHandle');
331
+ ReactInstanceHandles = require('ReactInstanceHandles');
332
+ ResponderEventPlugin = require('ResponderEventPlugin');
333
+ SyntheticEvent = require('SyntheticEvent');
334
+
335
+ EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles);
336
+
337
+ // dumpCache, in open-source tests, only resets existing mocks. It does not
338
+ // reset module-state though -- so we need to do this explicitly in the test
339
+ // for now. Once that's no longer the case, we can delete this line.
340
+ EventPluginHub.__purge();
341
+ // Only needed because SyntheticEvent supports the `currentTarget`
342
+ // property.
343
+ EventPluginUtils.injection.injectMount({
344
+ getNode: function(id) {
345
+ return id + '_thisDoesntReallyMatterForThisTestCase';
346
+ }
347
+ });
348
+
349
+ // Raw event stream event `.target`s must be `NodeHandle`s. For these test
350
+ // cases, we'll assume that we create the node handles as the exact IDs
351
+ // they represent.
352
+ NodeHandle.injection.injectImplementation({
353
+ getRootNodeID: function(nodeHandle) {
354
+ return nodeHandle;
355
+ }
356
+ });
357
+
358
+ topLevelTypes = EventConstants.topLevelTypes;
359
+ responderEventTypes = ResponderEventPlugin.eventTypes;
360
+ });
361
+
362
+ it('should do nothing when no one wants to respond', function() {
363
+ var config = oneEventLoopTestConfig(three);
364
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
365
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
366
+ config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
367
+ config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
368
+ config.startShouldSetResponder.bubbled.parent = {order: 4, returnVal: false};
369
+ config.startShouldSetResponder.bubbled.grandParent = {order: 5, returnVal: false};
370
+ run(config, three, startConfig(three.child, [three.child], [0]));
371
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
372
+
373
+ // Now no handlers should be called on `touchEnd`.
374
+ config = oneEventLoopTestConfig(three);
375
+ run(config, three, endConfig(three.child, [three.child], [0]));
376
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
377
+ });
378
+
379
+
380
+ /**
381
+ * Simple Start Granting
382
+ * --------------------
383
+ */
384
+
385
+
386
+ it('should grant responder grandParent while capturing', () => {
387
+ var config = oneEventLoopTestConfig(three);
388
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: true};
389
+ config.responderGrant.grandParent = {order: 1};
390
+ config.responderStart.grandParent = {order: 2};
391
+ run(config, three, startConfig(three.child, [three.child], [0]));
392
+ expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
393
+
394
+ config = oneEventLoopTestConfig(three);
395
+ config.responderEnd.grandParent = {order: 0};
396
+ config.responderRelease.grandParent = {order: 1};
397
+ run(config, three, endConfig(three.child, [three.child], [0]));
398
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
399
+ });
400
+
401
+ it('should grant responder parent while capturing', () => {
402
+ var config = oneEventLoopTestConfig(three);
403
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
404
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: true};
405
+ config.responderGrant.parent = {order: 2};
406
+ config.responderStart.parent = {order: 3};
407
+ run(config, three, startConfig(three.child, [three.child], [0]));
408
+ expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
409
+
410
+ config = oneEventLoopTestConfig(three);
411
+ config.responderEnd.parent = {order: 0};
412
+ config.responderRelease.parent = {order: 1};
413
+ run(config, three, endConfig(three.child, [three.child], [0]));
414
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
415
+ });
416
+
417
+ it('should grant responder child while capturing', () => {
418
+ var config = oneEventLoopTestConfig(three);
419
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
420
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
421
+ config.startShouldSetResponder.captured.child = {order: 2, returnVal: true};
422
+ config.responderGrant.child = {order: 3};
423
+ config.responderStart.child = {order: 4};
424
+ run(config, three, startConfig(three.child, [three.child], [0]));
425
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
426
+
427
+ config = oneEventLoopTestConfig(three);
428
+ config.responderEnd.child = {order: 0};
429
+ config.responderRelease.child = {order: 1};
430
+ run(config, three, endConfig(three.child, [three.child], [0]));
431
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
432
+ });
433
+
434
+ it('should grant responder child while bubbling', () => {
435
+ var config = oneEventLoopTestConfig(three);
436
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
437
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
438
+ config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
439
+ config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
440
+ config.responderGrant.child = {order: 4};
441
+ config.responderStart.child = {order: 5};
442
+ run(config, three, startConfig(three.child, [three.child], [0]));
443
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
444
+
445
+ config = oneEventLoopTestConfig(three);
446
+ config.responderEnd.child = {order: 0};
447
+ config.responderRelease.child = {order: 1};
448
+ run(config, three, endConfig(three.child, [three.child], [0]));
449
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
450
+ });
451
+
452
+ it('should grant responder parent while bubbling', () => {
453
+ var config = oneEventLoopTestConfig(three);
454
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
455
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
456
+ config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
457
+ config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
458
+ config.startShouldSetResponder.bubbled.parent = {order: 4, returnVal: true};
459
+ config.responderGrant.parent = {order: 5};
460
+ config.responderStart.parent = {order: 6};
461
+ run(config, three, startConfig(three.child, [three.child], [0]));
462
+ expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
463
+
464
+ config = oneEventLoopTestConfig(three);
465
+ config.responderEnd.parent = {order: 0};
466
+ config.responderRelease.parent = {order: 1};
467
+ run(config, three, endConfig(three.child, [three.child], [0]));
468
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
469
+ });
470
+
471
+ it('should grant responder grandParent while bubbling', () => {
472
+ var config = oneEventLoopTestConfig(three);
473
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
474
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
475
+ config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
476
+ config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
477
+ config.startShouldSetResponder.bubbled.parent = {order: 4, returnVal: false};
478
+ config.startShouldSetResponder.bubbled.grandParent = {order: 5, returnVal: true};
479
+ config.responderGrant.grandParent = {order: 6};
480
+ config.responderStart.grandParent = {order: 7};
481
+ run(config, three, startConfig(three.child, [three.child], [0]));
482
+ expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
483
+
484
+ config = oneEventLoopTestConfig(three);
485
+ config.responderEnd.grandParent = {order: 0};
486
+ config.responderRelease.grandParent = {order: 1};
487
+ run(config, three, endConfig(three.child, [three.child], [0]));
488
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
489
+ });
490
+
491
+
492
+
493
+ /**
494
+ * Simple Move Granting
495
+ * --------------------
496
+ */
497
+
498
+ it('should grant responder grandParent while capturing move', () => {
499
+ var config = oneEventLoopTestConfig(three);
500
+
501
+ config.startShouldSetResponder.captured.grandParent = {order: 0};
502
+ config.startShouldSetResponder.captured.parent = {order: 1};
503
+ config.startShouldSetResponder.captured.child = {order: 2};
504
+ config.startShouldSetResponder.bubbled.child = {order: 3};
505
+ config.startShouldSetResponder.bubbled.parent = {order: 4};
506
+ config.startShouldSetResponder.bubbled.grandParent = {order: 5};
507
+ run(config, three, startConfig(three.child, [three.child], [0]));
508
+
509
+ config = oneEventLoopTestConfig(three);
510
+ config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: true};
511
+ config.responderGrant.grandParent = {order: 1};
512
+ config.responderMove.grandParent = {order: 2};
513
+ run(config, three, moveConfig(three.child, [three.child], [0]));
514
+ expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
515
+
516
+ config = oneEventLoopTestConfig(three);
517
+ config.responderEnd.grandParent = {order: 0};
518
+ config.responderRelease.grandParent = {order: 1};
519
+ run(config, three, endConfig(three.child, [three.child], [0]));
520
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
521
+ });
522
+
523
+ it('should grant responder parent while capturing move', () => {
524
+ var config = oneEventLoopTestConfig(three);
525
+
526
+ config.startShouldSetResponder.captured.grandParent = {order: 0};
527
+ config.startShouldSetResponder.captured.parent = {order: 1};
528
+ config.startShouldSetResponder.captured.child = {order: 2};
529
+ config.startShouldSetResponder.bubbled.child = {order: 3};
530
+ config.startShouldSetResponder.bubbled.parent = {order: 4};
531
+ config.startShouldSetResponder.bubbled.grandParent = {order: 5};
532
+ run(config, three, startConfig(three.child, [three.child], [0]));
533
+
534
+ config = oneEventLoopTestConfig(three);
535
+ config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
536
+ config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: true};
537
+ config.responderGrant.parent = {order: 2};
538
+ config.responderMove.parent = {order: 3};
539
+ run(config, three, moveConfig(three.child, [three.child], [0]));
540
+ expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
541
+
542
+ config = oneEventLoopTestConfig(three);
543
+ config.responderEnd.parent = {order: 0};
544
+ config.responderRelease.parent = {order: 1};
545
+ run(config, three, endConfig(three.child, [three.child], [0]));
546
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
547
+ });
548
+
549
+ it('should grant responder child while capturing move', () => {
550
+ var config = oneEventLoopTestConfig(three);
551
+
552
+ config.startShouldSetResponder.captured.grandParent = {order: 0};
553
+ config.startShouldSetResponder.captured.parent = {order: 1};
554
+ config.startShouldSetResponder.captured.child = {order: 2};
555
+ config.startShouldSetResponder.bubbled.child = {order: 3};
556
+ config.startShouldSetResponder.bubbled.parent = {order: 4};
557
+ config.startShouldSetResponder.bubbled.grandParent = {order: 5};
558
+ run(config, three, startConfig(three.child, [three.child], [0]));
559
+
560
+ config = oneEventLoopTestConfig(three);
561
+ config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
562
+ config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
563
+ config.moveShouldSetResponder.captured.child = {order: 2, returnVal: true};
564
+ config.responderGrant.child = {order: 3};
565
+ config.responderMove.child = {order: 4};
566
+ run(config, three, moveConfig(three.child, [three.child], [0]));
567
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
568
+
569
+ config = oneEventLoopTestConfig(three);
570
+ config.responderEnd.child = {order: 0};
571
+ config.responderRelease.child = {order: 1};
572
+ run(config, three, endConfig(three.child, [three.child], [0]));
573
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
574
+ });
575
+
576
+ it('should grant responder child while bubbling move', () => {
577
+ var config = oneEventLoopTestConfig(three);
578
+
579
+ config.startShouldSetResponder.captured.grandParent = {order: 0};
580
+ config.startShouldSetResponder.captured.parent = {order: 1};
581
+ config.startShouldSetResponder.captured.child = {order: 2};
582
+ config.startShouldSetResponder.bubbled.child = {order: 3};
583
+ config.startShouldSetResponder.bubbled.parent = {order: 4};
584
+ config.startShouldSetResponder.bubbled.grandParent = {order: 5};
585
+ run(config, three, startConfig(three.child, [three.child], [0]));
586
+
587
+ config = oneEventLoopTestConfig(three);
588
+ config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
589
+ config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
590
+ config.moveShouldSetResponder.captured.child = {order: 2, returnVal: false};
591
+ config.moveShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
592
+ config.responderGrant.child = {order: 4};
593
+ config.responderMove.child = {order: 5};
594
+ run(config, three, moveConfig(three.child, [three.child], [0]));
595
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
596
+
597
+ config = oneEventLoopTestConfig(three);
598
+ config.responderEnd.child = {order: 0};
599
+ config.responderRelease.child = {order: 1};
600
+ run(config, three, endConfig(three.child, [three.child], [0]));
601
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
602
+ });
603
+
604
+ it('should grant responder parent while bubbling move', () => {
605
+ var config = oneEventLoopTestConfig(three);
606
+
607
+ config.startShouldSetResponder.captured.grandParent = {order: 0};
608
+ config.startShouldSetResponder.captured.parent = {order: 1};
609
+ config.startShouldSetResponder.captured.child = {order: 2};
610
+ config.startShouldSetResponder.bubbled.child = {order: 3};
611
+ config.startShouldSetResponder.bubbled.parent = {order: 4};
612
+ config.startShouldSetResponder.bubbled.grandParent = {order: 5};
613
+ run(config, three, startConfig(three.child, [three.child], [0]));
614
+
615
+ config = oneEventLoopTestConfig(three);
616
+ config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
617
+ config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
618
+ config.moveShouldSetResponder.captured.child = {order: 2, returnVal: false};
619
+ config.moveShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
620
+ config.moveShouldSetResponder.bubbled.parent = {order: 4, returnVal: true};
621
+ config.responderGrant.parent = {order: 5};
622
+ config.responderMove.parent = {order: 6};
623
+ run(config, three, moveConfig(three.child, [three.child], [0]));
624
+ expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
625
+
626
+ config = oneEventLoopTestConfig(three);
627
+ config.responderEnd.parent = {order: 0};
628
+ config.responderRelease.parent = {order: 1};
629
+ run(config, three, endConfig(three.child, [three.child], [0]));
630
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
631
+ });
632
+
633
+ it('should grant responder grandParent while bubbling move', () => {
634
+ var config = oneEventLoopTestConfig(three);
635
+
636
+ config.startShouldSetResponder.captured.grandParent = {order: 0};
637
+ config.startShouldSetResponder.captured.parent = {order: 1};
638
+ config.startShouldSetResponder.captured.child = {order: 2};
639
+ config.startShouldSetResponder.bubbled.child = {order: 3};
640
+ config.startShouldSetResponder.bubbled.parent = {order: 4};
641
+ config.startShouldSetResponder.bubbled.grandParent = {order: 5};
642
+ run(config, three, startConfig(three.child, [three.child], [0]));
643
+
644
+ config = oneEventLoopTestConfig(three);
645
+ config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
646
+ config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
647
+ config.moveShouldSetResponder.captured.child = {order: 2, returnVal: false};
648
+ config.moveShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
649
+ config.moveShouldSetResponder.bubbled.parent = {order: 4, returnVal: false};
650
+ config.moveShouldSetResponder.bubbled.grandParent = {order: 5, returnVal: true};
651
+ config.responderGrant.grandParent = {order: 6};
652
+ config.responderMove.grandParent = {order: 7};
653
+ run(config, three, moveConfig(three.child, [three.child], [0]));
654
+ expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
655
+
656
+ config = oneEventLoopTestConfig(three);
657
+ config.responderEnd.grandParent = {order: 0};
658
+ config.responderRelease.grandParent = {order: 1};
659
+ run(config, three, endConfig(three.child, [three.child], [0]));
660
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
661
+ });
662
+
663
+
664
+ /**
665
+ * Common ancestor tests
666
+ * ---------------------
667
+ */
668
+
669
+ it('should bubble negotiation to first common ancestor of responder', () => {
670
+ var config = oneEventLoopTestConfig(three);
671
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
672
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: true};
673
+ config.responderGrant.parent = {order: 2};
674
+ config.responderStart.parent = {order: 3};
675
+ run(config, three, startConfig(three.child, [three.child], [0]));
676
+ expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
677
+
678
+ // While `PARENT_ID` is still responder, we create new handlers that verify
679
+ // the ordering of propagation, restarting the count at `0`.
680
+ config = oneEventLoopTestConfig(three);
681
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
682
+
683
+ config.startShouldSetResponder.bubbled.grandParent = {order: 1, returnVal: false};
684
+ config.responderStart.parent = {order: 2};
685
+ run(config, three, startConfig(three.child, [three.child], [0]));
686
+ expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
687
+
688
+ config = oneEventLoopTestConfig(three);
689
+ config.responderEnd.parent = {order: 0};
690
+ config.responderRelease.parent = {order: 1};
691
+ run(config, three, endConfig(three.child, [three.child], [0]));
692
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
693
+ });
694
+
695
+ it('should bubble negotiation to first common ancestor of responder then transfer', () => {
696
+ var config = oneEventLoopTestConfig(three);
697
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
698
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: true};
699
+ config.responderGrant.parent = {order: 2};
700
+ config.responderStart.parent = {order: 3};
701
+ run(config, three, startConfig(three.child, [three.child], [0]));
702
+ expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
703
+
704
+ config = oneEventLoopTestConfig(three);
705
+
706
+ // Parent is responder, and responder is transfered by a second touch start
707
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: true};
708
+ config.responderTerminationRequest.parent = {order: 1, returnVal: true};
709
+ config.responderGrant.grandParent = {order: 2};
710
+ config.responderTerminate.parent = {order: 3};
711
+ config.responderStart.grandParent = {order: 4};
712
+ run(config, three, startConfig(three.child, [three.child, three.child], [1]));
713
+ expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
714
+
715
+ config = oneEventLoopTestConfig(three);
716
+ config.responderEnd.grandParent = {order: 0};
717
+ // one remains\ /one ended \
718
+ run(config, three, endConfig(three.child, [three.child, three.child], [1]));
719
+ expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
720
+
721
+ config = oneEventLoopTestConfig(three);
722
+ config.responderEnd.grandParent = {order: 0};
723
+ config.responderRelease.grandParent = {order: 1};
724
+ run(config, three, endConfig(three.child, [three.child], [0]));
725
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
726
+ });
727
+
728
+ /**
729
+ * If nothing is responder, then the negotiation should propagate directly to
730
+ * the deepest target in the second touch.
731
+ */
732
+ it('should negotiate with deepest target on second touch if nothing is responder', () => {
733
+ // Initially nothing wants to become the responder
734
+ var config = oneEventLoopTestConfig(three);
735
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
736
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
737
+ config.startShouldSetResponder.bubbled.parent = {order: 2, returnVal: false};
738
+ config.startShouldSetResponder.bubbled.grandParent = {order: 3, returnVal: false};
739
+
740
+ run(config, three, startConfig(three.parent, [three.parent], [0]));
741
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
742
+
743
+ config = oneEventLoopTestConfig(three);
744
+
745
+ // Now child wants to become responder. Negotiation should bubble as deep
746
+ // as the target is because we don't find first common ancestor (with
747
+ // current responder) because there is no current responder.
748
+ // (Even if this is the second active touch).
749
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
750
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
751
+ config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
752
+ config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
753
+ config.responderGrant.child = {order: 4};
754
+ config.responderStart.child = {order: 5};
755
+ // / Two active touches \ /one of them new\
756
+ run(config, three, startConfig(three.child, [three.parent, three.child], [1]));
757
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
758
+
759
+
760
+ // Now we remove the original first touch, keeping the second touch that
761
+ // started within the current responder (child). Nothing changes because
762
+ // there's still touches that started inside of the current responder.
763
+ config = oneEventLoopTestConfig(three);
764
+ config.responderEnd.child = {order: 0};
765
+ // / one ended\ /one remains \
766
+ run(config, three, endConfig(three.child, [three.parent, three.child], [0]));
767
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
768
+
769
+ // Okay, now let's add back that first touch (nothing should change) and
770
+ // then we'll try peeling back the touches in the opposite order to make
771
+ // sure that first removing the second touch instantly causes responder to
772
+ // be released.
773
+ config = oneEventLoopTestConfig(three);
774
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
775
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
776
+ config.startShouldSetResponder.bubbled.parent = {order: 2, returnVal: false};
777
+ config.startShouldSetResponder.bubbled.grandParent = {order: 3, returnVal: false};
778
+ // Interesting: child still gets moves even though touch target is parent!
779
+ // Current responder gets a `responderStart` for any touch while responder.
780
+ config.responderStart.child = {order: 4};
781
+ // / Two active touches \ /one of them new\
782
+ run(config, three, startConfig(three.parent, [three.child, three.parent], [1]));
783
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
784
+
785
+
786
+ // Now, move that new touch that had no effect, and did not start within
787
+ // the current responder.
788
+ config = oneEventLoopTestConfig(three);
789
+ config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
790
+ config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
791
+ config.moveShouldSetResponder.bubbled.parent = {order: 2, returnVal: false};
792
+ config.moveShouldSetResponder.bubbled.grandParent = {order: 3, returnVal: false};
793
+ // Interesting: child still gets moves even though touch target is parent!
794
+ // Current responder gets a `responderMove` for any touch while responder.
795
+ config.responderMove.child = {order: 4};
796
+ // / Two active touches \ /one of them moved\
797
+ run(config, three, moveConfig(three.parent, [three.child, three.parent], [1]));
798
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
799
+
800
+
801
+ config = oneEventLoopTestConfig(three);
802
+ config.responderEnd.child = {order: 0};
803
+ config.responderRelease.child = {order: 1};
804
+ // /child end \ /parent remain\
805
+ run(config, three, endConfig(three.child, [three.child, three.parent], [0]));
806
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
807
+ });
808
+
809
+
810
+ /**
811
+ * If nothing is responder, then the negotiation should propagate directly to
812
+ * the deepest target in the second touch.
813
+ */
814
+ it('should negotiate until first common ancestor when there are siblings', () => {
815
+ // Initially nothing wants to become the responder
816
+ var config = oneEventLoopTestConfig(siblings);
817
+ config.startShouldSetResponder.captured.parent = {order: 0, returnVal: false};
818
+ config.startShouldSetResponder.captured.childOne = {order: 1, returnVal: false};
819
+ config.startShouldSetResponder.bubbled.childOne = {order: 2, returnVal: true};
820
+ config.responderGrant.childOne = {order: 3};
821
+ config.responderStart.childOne = {order: 4};
822
+
823
+ run(config, siblings, startConfig(siblings.childOne, [siblings.childOne], [0]));
824
+ expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne);
825
+
826
+ // If the touch target is the sibling item, the negotiation should only
827
+ // propagate to first common ancestor of current responder and sibling (so
828
+ // the parent).
829
+ config = oneEventLoopTestConfig(siblings);
830
+ config.startShouldSetResponder.captured.parent = {order: 0, returnVal: false};
831
+ config.startShouldSetResponder.bubbled.parent = {order: 1, returnVal: false};
832
+ config.responderStart.childOne = {order: 2};
833
+
834
+ var touchConfig =
835
+ startConfig(siblings.childTwo, [siblings.childOne, siblings.childTwo], [1]);
836
+ run(config, siblings, touchConfig);
837
+ expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne);
838
+
839
+
840
+ // move childOne
841
+ config = oneEventLoopTestConfig(siblings);
842
+ config.moveShouldSetResponder.captured.parent = {order: 0, returnVal: false};
843
+ config.moveShouldSetResponder.bubbled.parent = {order: 1, returnVal: false};
844
+ config.responderMove.childOne = {order: 2};
845
+ run(config, siblings, moveConfig(siblings.childOne, [siblings.childOne, siblings.childTwo], [0]));
846
+ expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne);
847
+
848
+ // move childTwo: Only negotiates to `parent`.
849
+ config = oneEventLoopTestConfig(siblings);
850
+ config.moveShouldSetResponder.captured.parent = {order: 0, returnVal: false};
851
+ config.moveShouldSetResponder.bubbled.parent = {order: 1, returnVal: false};
852
+ config.responderMove.childOne = {order: 2};
853
+ run(config, siblings, moveConfig(siblings.childTwo, [siblings.childOne, siblings.childTwo], [1]));
854
+ expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne);
855
+
856
+ });
857
+
858
+
859
+ it('should notify of being rejected. responderStart/Move happens on current responder', () => {
860
+ // Initially nothing wants to become the responder
861
+ var config = oneEventLoopTestConfig(three);
862
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
863
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
864
+ config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
865
+ config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
866
+ config.responderGrant.child = {order: 4};
867
+ config.responderStart.child = {order: 5};
868
+
869
+ run(config, three, startConfig(three.child, [three.child], [0]));
870
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
871
+
872
+ // Suppose parent wants to become responder on move, and is rejected
873
+ config = oneEventLoopTestConfig(three);
874
+ config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
875
+ config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
876
+ config.moveShouldSetResponder.bubbled.parent = {order: 2, returnVal: true};
877
+ config.responderTerminationRequest.child = {order: 3, returnVal: false};
878
+ config.responderReject.parent = {order: 4};
879
+ // The start/move should occur on the original responder if new one is rejected
880
+ config.responderMove.child = {order: 5};
881
+
882
+ var touchConfig =
883
+ moveConfig(three.child, [three.child], [0]);
884
+ run(config, three, touchConfig);
885
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
886
+
887
+ config = oneEventLoopTestConfig(three);
888
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
889
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
890
+ config.startShouldSetResponder.bubbled.parent = {order: 2, returnVal: true};
891
+ config.responderTerminationRequest.child = {order: 3, returnVal: false};
892
+ config.responderReject.parent = {order: 4};
893
+ // The start/move should occur on the original responder if new one is rejected
894
+ config.responderStart.child = {order: 5};
895
+
896
+ touchConfig =
897
+ startConfig(three.child, [three.child, three.child], [1]);
898
+ run(config, three, touchConfig);
899
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
900
+
901
+ });
902
+
903
+
904
+ it('should negotiate scroll', () => {
905
+ // Initially nothing wants to become the responder
906
+ var config = oneEventLoopTestConfig(three);
907
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
908
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
909
+ config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
910
+ config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
911
+ config.responderGrant.child = {order: 4};
912
+ config.responderStart.child = {order: 5};
913
+
914
+ run(config, three, startConfig(three.child, [three.child], [0]));
915
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
916
+
917
+ // If the touch target is the sibling item, the negotiation should only
918
+ // propagate to first common ancestor of current responder and sibling (so
919
+ // the parent).
920
+ config = oneEventLoopTestConfig(three);
921
+ config.scrollShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
922
+ config.scrollShouldSetResponder.captured.parent = {order: 1, returnVal: false};
923
+ config.scrollShouldSetResponder.bubbled.parent = {order: 2, returnVal: true};
924
+ config.responderTerminationRequest.child = {order: 3, returnVal: false};
925
+ config.responderReject.parent = {order: 4};
926
+
927
+ run(config, three, {
928
+ topLevelType: topLevelTypes.topScroll,
929
+ target: three.parent,
930
+ targetID: three.parent,
931
+ nativeEvent: {}
932
+ });
933
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
934
+
935
+
936
+ // Now lets let the scroll take control this time.
937
+ config = oneEventLoopTestConfig(three);
938
+ config.scrollShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
939
+ config.scrollShouldSetResponder.captured.parent = {order: 1, returnVal: false};
940
+ config.scrollShouldSetResponder.bubbled.parent = {order: 2, returnVal: true};
941
+ config.responderTerminationRequest.child = {order: 3, returnVal: true};
942
+ config.responderGrant.parent = {order: 4};
943
+ config.responderTerminate.child = {order: 5};
944
+
945
+ run(config, three, {
946
+ topLevelType: topLevelTypes.topScroll,
947
+ target: three.parent,
948
+ targetID: three.parent,
949
+ nativeEvent: {}
950
+ });
951
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.parent);
952
+
953
+
954
+ });
955
+
956
+ it('should cancel correctly', () => {
957
+ // Initially our child becomes responder
958
+ var config = oneEventLoopTestConfig(three);
959
+ config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
960
+ config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
961
+ config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
962
+ config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
963
+ config.responderGrant.child = {order: 4};
964
+ config.responderStart.child = {order: 5};
965
+
966
+ run(config, three, startConfig(three.child, [three.child], [0]));
967
+ expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
968
+
969
+ config = oneEventLoopTestConfig(three);
970
+ config.responderEnd.child = {order: 0};
971
+ config.responderTerminate.child = {order: 1};
972
+
973
+ var nativeEvent = _touchConfig(
974
+ topLevelTypes.topTouchCancel,
975
+ three.child,
976
+ [three.child],
977
+ [0]
978
+ );
979
+ run(config, three, nativeEvent);
980
+ expect(ResponderEventPlugin.getResponderID()).toBe(null);
981
+ });
982
+});