@samitouri / QOS-React-2 / commits / 6ec3b65169

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 +});