Merge pull request #3555 from spicyj/native-overrides

Import ResponderEventPlugin from react-native
This commit is contained in:
Ben Alpert 2015-04-02 14:13:35 -07:00
commit dea7efbe16
10 changed files with 1823 additions and 811 deletions

View File

@ -1,309 +0,0 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ResponderEventPlugin
*/
'use strict';
var EventConstants = require('EventConstants');
var EventPluginUtils = require('EventPluginUtils');
var EventPropagators = require('EventPropagators');
var SyntheticEvent = require('SyntheticEvent');
var accumulateInto = require('accumulateInto');
var keyOf = require('keyOf');
var isStartish = EventPluginUtils.isStartish;
var isMoveish = EventPluginUtils.isMoveish;
var isEndish = EventPluginUtils.isEndish;
var executeDirectDispatch = EventPluginUtils.executeDirectDispatch;
var hasDispatches = EventPluginUtils.hasDispatches;
var executeDispatchesInOrderStopAtTrue =
EventPluginUtils.executeDispatchesInOrderStopAtTrue;
/**
* ID of element that should respond to touch/move types of interactions, as
* indicated explicitly by relevant callbacks.
*/
var responderID = null;
var isPressing = false;
var eventTypes = {
/**
* On a `touchStart`/`mouseDown`, is it desired that this element become the
* responder?
*/
startShouldSetResponder: {
phasedRegistrationNames: {
bubbled: keyOf({onStartShouldSetResponder: null}),
captured: keyOf({onStartShouldSetResponderCapture: null})
}
},
/**
* On a `scroll`, is it desired that this element become the responder? This
* is usually not needed, but should be used to retroactively infer that a
* `touchStart` had occured during momentum scroll. During a momentum scroll,
* a touch start will be immediately followed by a scroll event if the view is
* currently scrolling.
*/
scrollShouldSetResponder: {
phasedRegistrationNames: {
bubbled: keyOf({onScrollShouldSetResponder: null}),
captured: keyOf({onScrollShouldSetResponderCapture: null})
}
},
/**
* On a `touchMove`/`mouseMove`, is it desired that this element become the
* responder?
*/
moveShouldSetResponder: {
phasedRegistrationNames: {
bubbled: keyOf({onMoveShouldSetResponder: null}),
captured: keyOf({onMoveShouldSetResponderCapture: null})
}
},
/**
* Direct responder events dispatched directly to responder. Do not bubble.
*/
responderMove: {registrationName: keyOf({onResponderMove: null})},
responderRelease: {registrationName: keyOf({onResponderRelease: null})},
responderTerminationRequest: {
registrationName: keyOf({onResponderTerminationRequest: null})
},
responderGrant: {registrationName: keyOf({onResponderGrant: null})},
responderReject: {registrationName: keyOf({onResponderReject: null})},
responderTerminate: {registrationName: keyOf({onResponderTerminate: null})}
};
/**
* Performs negotiation between any existing/current responder, checks to see if
* any new entity is interested in becoming responder, performs that handshake
* and returns any events that must be emitted to notify the relevant parties.
*
* A note about event ordering in the `EventPluginHub`.
*
* Suppose plugins are injected in the following order:
*
* `[R, S, C]`
*
* To help illustrate the example, assume `S` is `SimpleEventPlugin` (for
* `onClick` etc) and `R` is `ResponderEventPlugin`.
*
* "Deferred-Dispatched Events":
*
* - The current event plugin system will traverse the list of injected plugins,
* in order, and extract events by collecting the plugin's return value of
* `extractEvents()`.
* - These events that are returned from `extractEvents` are "deferred
* dispatched events".
* - When returned from `extractEvents`, deferred-dispatched events contain an
* "accumulation" of deferred dispatches.
* - These deferred dispatches are accumulated/collected before they are
* returned, but processed at a later time by the `EventPluginHub` (hence the
* name deferred).
*
* In the process of returning their deferred-dispatched events, event plugins
* themselves can dispatch events on-demand without returning them from
* `extractEvents`. Plugins might want to do this, so that they can use event
* dispatching as a tool that helps them decide which events should be extracted
* in the first place.
*
* "On-Demand-Dispatched Events":
*
* - On-demand-dispatched events are not returned from `extractEvents`.
* - On-demand-dispatched events are dispatched during the process of returning
* the deferred-dispatched events.
* - They should not have side effects.
* - They should be avoided, and/or eventually be replaced with another
* abstraction that allows event plugins to perform multiple "rounds" of event
* extraction.
*
* Therefore, the sequence of event dispatches becomes:
*
* - `R`s on-demand events (if any) (dispatched by `R` on-demand)
* - `S`s on-demand events (if any) (dispatched by `S` on-demand)
* - `C`s on-demand events (if any) (dispatched by `C` on-demand)
* - `R`s extracted events (if any) (dispatched by `EventPluginHub`)
* - `S`s extracted events (if any) (dispatched by `EventPluginHub`)
* - `C`s extracted events (if any) (dispatched by `EventPluginHub`)
*
* In the case of `ResponderEventPlugin`: If the `startShouldSetResponder`
* on-demand dispatch returns `true` (and some other details are satisfied) the
* `onResponderGrant` deferred dispatched event is returned from
* `extractEvents`. The sequence of dispatch executions in this case
* will appear as follows:
*
* - `startShouldSetResponder` (`ResponderEventPlugin` dispatches on-demand)
* - `touchStartCapture` (`EventPluginHub` dispatches as usual)
* - `touchStart` (`EventPluginHub` dispatches as usual)
* - `responderGrant/Reject` (`EventPluginHub` dispatches as usual)
*
* @param {string} topLevelType Record from `EventConstants`.
* @param {string} topLevelTargetID ID of deepest React rendered element.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of synthetic events.
*/
function setResponderAndExtractTransfer(
topLevelType,
topLevelTargetID,
nativeEvent) {
var shouldSetEventType =
isStartish(topLevelType) ? eventTypes.startShouldSetResponder :
isMoveish(topLevelType) ? eventTypes.moveShouldSetResponder :
eventTypes.scrollShouldSetResponder;
var bubbleShouldSetFrom = responderID || topLevelTargetID;
var shouldSetEvent = SyntheticEvent.getPooled(
shouldSetEventType,
bubbleShouldSetFrom,
nativeEvent
);
EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent);
var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
if (!shouldSetEvent.isPersistent()) {
shouldSetEvent.constructor.release(shouldSetEvent);
}
if (!wantsResponderID || wantsResponderID === responderID) {
return null;
}
var extracted;
var grantEvent = SyntheticEvent.getPooled(
eventTypes.responderGrant,
wantsResponderID,
nativeEvent
);
EventPropagators.accumulateDirectDispatches(grantEvent);
if (responderID) {
var terminationRequestEvent = SyntheticEvent.getPooled(
eventTypes.responderTerminationRequest,
responderID,
nativeEvent
);
EventPropagators.accumulateDirectDispatches(terminationRequestEvent);
var shouldSwitch = !hasDispatches(terminationRequestEvent) ||
executeDirectDispatch(terminationRequestEvent);
if (!terminationRequestEvent.isPersistent()) {
terminationRequestEvent.constructor.release(terminationRequestEvent);
}
if (shouldSwitch) {
var terminateType = eventTypes.responderTerminate;
var terminateEvent = SyntheticEvent.getPooled(
terminateType,
responderID,
nativeEvent
);
EventPropagators.accumulateDirectDispatches(terminateEvent);
extracted = accumulateInto(extracted, [grantEvent, terminateEvent]);
responderID = wantsResponderID;
} else {
var rejectEvent = SyntheticEvent.getPooled(
eventTypes.responderReject,
wantsResponderID,
nativeEvent
);
EventPropagators.accumulateDirectDispatches(rejectEvent);
extracted = accumulateInto(extracted, rejectEvent);
}
} else {
extracted = accumulateInto(extracted, grantEvent);
responderID = wantsResponderID;
}
return extracted;
}
/**
* A transfer is a negotiation between a currently set responder and the next
* element to claim responder status. Any start event could trigger a transfer
* of responderID. Any move event could trigger a transfer, so long as there is
* currently a responder set (in other words as long as the user is pressing
* down).
*
* @param {string} topLevelType Record from `EventConstants`.
* @return {boolean} True if a transfer of responder could possibly occur.
*/
function canTriggerTransfer(topLevelType) {
return topLevelType === EventConstants.topLevelTypes.topScroll ||
isStartish(topLevelType) ||
(isPressing && isMoveish(topLevelType));
}
/**
* Event plugin for formalizing the negotiation between claiming locks on
* receiving touches.
*/
var ResponderEventPlugin = {
getResponderID: function() {
return responderID;
},
eventTypes: eventTypes,
/**
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of synthetic events.
* @see {EventPluginHub.extractEvents}
*/
extractEvents: function(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent) {
var extracted;
// Must have missed an end event - reset the state here.
if (responderID && isStartish(topLevelType)) {
responderID = null;
}
if (isStartish(topLevelType)) {
isPressing = true;
} else if (isEndish(topLevelType)) {
isPressing = false;
}
if (canTriggerTransfer(topLevelType)) {
var transfer = setResponderAndExtractTransfer(
topLevelType,
topLevelTargetID,
nativeEvent
);
if (transfer) {
extracted = accumulateInto(extracted, transfer);
}
}
// Now that we know the responder is set correctly, we can dispatch
// responder type events (directly to the responder).
var type = isMoveish(topLevelType) ? eventTypes.responderMove :
isEndish(topLevelType) ? eventTypes.responderRelease :
isStartish(topLevelType) ? eventTypes.responderStart : null;
if (type) {
var gesture = SyntheticEvent.getPooled(
type,
responderID || '',
nativeEvent
);
EventPropagators.accumulateDirectDispatches(gesture);
extracted = accumulateInto(extracted, gesture);
}
if (type === eventTypes.responderRelease) {
responderID = null;
}
return extracted;
}
};
module.exports = ResponderEventPlugin;

View File

@ -1,494 +0,0 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
'use strict';
var EventPluginHub;
var EventConstants;
var EventPropagators;
var ReactInstanceHandles;
var ResponderEventPlugin;
var SyntheticEvent;
var GRANDPARENT_ID = '.0';
var PARENT_ID = '.0.0';
var CHILD_ID = '.0.0.0';
var topLevelTypes;
var responderEventTypes;
var spies;
var DUMMY_NATIVE_EVENT = {};
var DUMMY_RENDERED_TARGET = {};
var onStartShouldSetResponder = function(id, cb, capture) {
var registrationNames = responderEventTypes
.startShouldSetResponder
.phasedRegistrationNames;
EventPluginHub.putListener(
id,
capture ? registrationNames.captured : registrationNames.bubbled,
cb
);
};
var onScrollShouldSetResponder = function(id, cb, capture) {
var registrationNames = responderEventTypes
.scrollShouldSetResponder
.phasedRegistrationNames;
EventPluginHub.putListener(
id,
capture ? registrationNames.captured : registrationNames.bubbled,
cb
);
};
var onMoveShouldSetResponder = function(id, cb, capture) {
var registrationNames = responderEventTypes
.moveShouldSetResponder
.phasedRegistrationNames;
EventPluginHub.putListener(
id,
capture ? registrationNames.captured : registrationNames.bubbled,
cb
);
};
var onResponderGrant = function(id, cb) {
EventPluginHub.putListener(
id,
responderEventTypes.responderGrant.registrationName,
cb
);
};
var extractForTouchStart = function(renderedTargetID) {
return ResponderEventPlugin.extractEvents(
topLevelTypes.topTouchStart,
DUMMY_NATIVE_EVENT,
renderedTargetID,
DUMMY_RENDERED_TARGET
);
};
var extractForTouchMove = function(renderedTargetID) {
return ResponderEventPlugin.extractEvents(
topLevelTypes.topTouchMove,
DUMMY_NATIVE_EVENT,
renderedTargetID,
DUMMY_RENDERED_TARGET
);
};
var extractForTouchEnd = function(renderedTargetID) {
return ResponderEventPlugin.extractEvents(
topLevelTypes.topTouchEnd,
DUMMY_NATIVE_EVENT,
renderedTargetID,
DUMMY_RENDERED_TARGET
);
};
var extractForMouseDown = function(renderedTargetID) {
return ResponderEventPlugin.extractEvents(
topLevelTypes.topMouseDown,
DUMMY_NATIVE_EVENT,
renderedTargetID,
DUMMY_RENDERED_TARGET
);
};
var extractForMouseMove = function(renderedTargetID) {
return ResponderEventPlugin.extractEvents(
topLevelTypes.topMouseMove,
DUMMY_NATIVE_EVENT,
renderedTargetID,
DUMMY_RENDERED_TARGET
);
};
var extractForMouseUp = function(renderedTargetID) {
return ResponderEventPlugin.extractEvents(
topLevelTypes.topMouseUp,
DUMMY_NATIVE_EVENT,
renderedTargetID,
DUMMY_RENDERED_TARGET
);
};
var extractForScroll = function(renderedTargetID) {
return ResponderEventPlugin.extractEvents(
topLevelTypes.topScroll,
DUMMY_NATIVE_EVENT,
renderedTargetID,
DUMMY_RENDERED_TARGET
);
};
var onGrantChild;
var onGrantParent;
var onGrantGrandParent;
var existsInExtraction = function(extracted, test) {
if (Array.isArray(extracted)) {
for (var i = 0; i < extracted.length; i++) {
if (test(extracted[i])) {
return true;
}
}
} else if (extracted) {
return test(extracted);
}
return false;
};
/**
* Helper validators.
*/
function assertGrantEvent(id, extracted) {
var test = function(event) {
return event instanceof SyntheticEvent &&
event.dispatchConfig === responderEventTypes.responderGrant &&
event.dispatchMarker === id;
};
expect(ResponderEventPlugin.getResponderID()).toBe(id);
expect(existsInExtraction(extracted, test)).toBe(true);
}
function assertResponderMoveEvent(id, extracted) {
var test = function(event) {
return event instanceof SyntheticEvent &&
event.dispatchConfig === responderEventTypes.responderMove &&
event.dispatchMarker === id;
};
expect(ResponderEventPlugin.getResponderID()).toBe(id);
expect(existsInExtraction(extracted, test)).toBe(true);
}
function assertTerminateEvent(id, extracted) {
var test = function(event) {
return event instanceof SyntheticEvent &&
event.dispatchConfig === responderEventTypes.responderTerminate &&
event.dispatchMarker === id;
};
expect(ResponderEventPlugin.getResponderID()).not.toBe(id);
expect(existsInExtraction(extracted, test)).toBe(true);
}
function assertRelease(id, extracted) {
var test = function(event) {
return event instanceof SyntheticEvent &&
event.dispatchConfig === responderEventTypes.responderRelease &&
event.dispatchMarker === id;
};
expect(ResponderEventPlugin.getResponderID()).toBe(null);
expect(existsInExtraction(extracted, test)).toBe(true);
}
function assertNothingExtracted(extracted) {
expect(Array.isArray(extracted)).toBe(false); // No grant events.
expect(Array.isArray(extracted)).toBeFalsy();
}
/**
* TODO:
* - Test that returning false from `responderTerminationRequest` will never
* cause the responder to be lost.
* - Automate some of this testing by providing config data - generalize.
*/
describe('ResponderEventPlugin', function() {
beforeEach(function() {
require('mock-modules').dumpCache();
EventPluginHub = require('EventPluginHub');
EventConstants = require('EventConstants');
EventPropagators = require('EventPropagators');
ReactInstanceHandles = require('ReactInstanceHandles');
ResponderEventPlugin = require('ResponderEventPlugin');
SyntheticEvent = require('SyntheticEvent');
EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles);
// dumpCache, in open-source tests, only resets existing mocks. It does not
// reset module-state though -- so we need to do this explicitly in the test
// for now. Once that's no longer the case, we can delete this line.
EventPluginHub.__purge();
topLevelTypes = EventConstants.topLevelTypes;
responderEventTypes = ResponderEventPlugin.eventTypes;
spies = {
onStartShouldSetResponderChild: function() {},
onStartShouldSetResponderParent: function() {},
onStartShouldSetResponderParentCapture: function() {},
onStartShouldSetResponderGrandParent: function() {},
onMoveShouldSetResponderParent: function() {},
onScrollShouldSetResponderParent: function() {}
};
onGrantChild = function() {};
onGrantParent = function() {};
onGrantGrandParent = function() {};
});
it('should not auto-set responder on touch start', function() {
// Notice we're not registering the startShould* handler.
var extracted = extractForTouchStart(CHILD_ID);
assertNothingExtracted(extracted);
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should not auto-set responder on mouse down', function() {
// Notice we're not registering the startShould* handler.
var extracted = extractForMouseDown(CHILD_ID);
assertNothingExtracted(extracted);
expect(ResponderEventPlugin.getResponderID()).toBe(null);
extractForMouseUp(CHILD_ID); // Let up!
expect(ResponderEventPlugin.getResponderID()).toBe(null);
// Register `onMoveShould*` handler.
spyOn(spies, 'onMoveShouldSetResponderParent').andReturn(true);
onMoveShouldSetResponder(PARENT_ID, spies.onMoveShouldSetResponderParent);
onResponderGrant(PARENT_ID, onGrantParent);
// Move mouse while not pressing down
extracted = extractForMouseMove(CHILD_ID);
assertNothingExtracted(extracted);
// Not going to call `onMoveShould`* if not touching.
expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(0);
expect(ResponderEventPlugin.getResponderID()).toBe(null);
// Now try the move extraction again, this time while holding down, and not
// letting up.
extracted = extractForMouseDown(CHILD_ID);
assertNothingExtracted(extracted);
expect(ResponderEventPlugin.getResponderID()).toBe(null);
// Now moving can set the responder, if pressing down, even if there is no
// current responder.
extracted = extractForMouseMove(CHILD_ID);
expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(1);
expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
assertGrantEvent(PARENT_ID, extracted);
extractForMouseUp(CHILD_ID);
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should not extract a grant/release event if double start', function() {
// Return true - we should become the responder.
var extracted;
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
onResponderGrant(CHILD_ID, onGrantChild);
extracted = extractForTouchStart(CHILD_ID);
assertGrantEvent(CHILD_ID, extracted);
expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
// Now we do *not* clear out the touch via a simulated touch end. This mocks
// out an environment that likely will never happen, but could in some odd
// error state so it's nice to make sure we recover gracefully.
// extractForTouchEnd(CHILD_ID); // Clear the responder
extracted = extractForTouchStart(CHILD_ID);
assertNothingExtracted();
expect(spies.onStartShouldSetResponderChild.calls.length).toBe(2);
});
it('should bubble/capture responder on start', function() {
// Return true - we should become the responder.
var extracted;
spyOn(spies, 'onStartShouldSetResponderParent').andReturn(true);
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
onStartShouldSetResponder(PARENT_ID, spies.onStartShouldSetResponderParent);
onResponderGrant(CHILD_ID, onGrantChild);
onResponderGrant(PARENT_ID, onGrantParent);
// Nothing extracted if no responder.
extracted = extractForTouchMove(GRANDPARENT_ID);
assertNothingExtracted(extracted);
extracted = extractForTouchStart(CHILD_ID);
assertGrantEvent(CHILD_ID, extracted);
expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
expect(spies.onStartShouldSetResponderParent.calls.length).toBe(0);
// Even if moving on the grandparent, the child will receive responder moves
// (This is even true for mouse interactions - which we should absolutely
// test)
extracted = extractForTouchMove(GRANDPARENT_ID);
assertResponderMoveEvent(CHILD_ID, extracted);
extracted = extractForTouchMove(CHILD_ID); // Test move on child node too.
assertResponderMoveEvent(CHILD_ID, extracted);
// Reset the responder - id passed here shouldn't matter:
// TODO: Test varying the id here.
extracted = extractForTouchEnd(GRANDPARENT_ID); // Clear the responder
assertRelease(CHILD_ID, extracted);
// Now make sure the parent requests responder on capture.
spyOn(spies, 'onStartShouldSetResponderParentCapture').andReturn(true);
onStartShouldSetResponder(
PARENT_ID,
spies.onStartShouldSetResponderParent,
true // Capture
);
onResponderGrant(PARENT_ID, onGrantGrandParent);
extracted = extractForTouchStart(PARENT_ID);
expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
assertGrantEvent(PARENT_ID, extracted);
// Now move on various nodes, ensuring that the responder move is emitted to
// the parent node.
extracted = extractForTouchMove(GRANDPARENT_ID);
assertResponderMoveEvent(PARENT_ID, extracted);
extracted = extractForTouchMove(CHILD_ID); // Test move on child node too.
assertResponderMoveEvent(PARENT_ID, extracted);
// Reset the responder - id passed here shouldn't matter:
// TODO: Test varying the id here.
extracted = extractForTouchEnd(GRANDPARENT_ID); // Clear the responder
assertRelease(PARENT_ID, extracted);
});
it('should invoke callback to ask if responder is desired', function() {
// Return true - we should become the responder.
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
var extracted = extractForTouchStart(CHILD_ID);
assertNothingExtracted(extracted);
expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
expect(ResponderEventPlugin.getResponderID()).toBe(CHILD_ID);
extractForTouchEnd(CHILD_ID); // Clear the responder
// Now try returning false - we should not become the responder.
spies.onStartShouldSetResponderChild.andReturn(false);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
extracted = extractForTouchStart(CHILD_ID);
assertNothingExtracted(extracted);
expect(spies.onStartShouldSetResponderChild.calls.length).toBe(2);
expect(ResponderEventPlugin.getResponderID()).toBe(null);
extractForTouchEnd(CHILD_ID);
expect(ResponderEventPlugin.getResponderID()).toBe(null); // Still null
// Same thing as before but return true from "shouldSet".
spies.onStartShouldSetResponderChild.andReturn(true);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
onResponderGrant(CHILD_ID, onGrantChild);
extracted = extractForTouchStart(CHILD_ID);
expect(spies.onStartShouldSetResponderChild.calls.length).toBe(3);
assertGrantEvent(CHILD_ID, extracted);
extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
assertRelease(CHILD_ID, extracted);
});
it('should give up responder to parent on move iff allowed', function() {
// Return true - we should become the responder.
var extracted;
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
spyOn(spies, 'onMoveShouldSetResponderParent').andReturn(true);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
onMoveShouldSetResponder(PARENT_ID, spies.onMoveShouldSetResponderParent);
onResponderGrant(CHILD_ID, onGrantChild);
onResponderGrant(PARENT_ID, onGrantParent);
spies.onStartShouldSetResponderChild.andReturn(true);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
extracted = extractForTouchStart(CHILD_ID);
expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(0); // none yet
assertGrantEvent(CHILD_ID, extracted); // Child is the current responder
extracted = extractForTouchMove(CHILD_ID);
expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(1);
assertGrantEvent(PARENT_ID, extracted);
assertTerminateEvent(CHILD_ID, extracted);
extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
assertRelease(PARENT_ID, extracted);
});
it('should responder move only on direct responder', function() {
// Return true - we should become the responder.
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
var extracted = extractForTouchStart(CHILD_ID);
assertNothingExtracted(extracted);
expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
expect(ResponderEventPlugin.getResponderID()).toBe(CHILD_ID);
extractForTouchEnd(CHILD_ID); // Clear the responder
expect(ResponderEventPlugin.getResponderID()).toBe(null);
// Now try returning false - we should not become the responder.
spies.onStartShouldSetResponderChild.andReturn(false);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
extracted = extractForTouchStart(CHILD_ID);
assertNothingExtracted(extracted);
expect(spies.onStartShouldSetResponderChild.calls.length).toBe(2);
expect(ResponderEventPlugin.getResponderID()).toBe(null);
extractForTouchEnd(CHILD_ID); // Clear the responder
// Same thing as before but return true from "shouldSet".
spies.onStartShouldSetResponderChild.andReturn(true);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
onResponderGrant(CHILD_ID, onGrantChild);
extracted = extractForTouchStart(CHILD_ID);
expect(spies.onStartShouldSetResponderChild.calls.length).toBe(3);
assertGrantEvent(CHILD_ID, extracted);
extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
assertRelease(CHILD_ID, extracted);
});
it('should give up responder to parent on scroll iff allowed', function() {
// Return true - we should become the responder.
var extracted;
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
spyOn(spies, 'onMoveShouldSetResponderParent').andReturn(false);
spyOn(spies, 'onScrollShouldSetResponderParent').andReturn(true);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
onMoveShouldSetResponder(PARENT_ID, spies.onMoveShouldSetResponderParent);
onScrollShouldSetResponder(
PARENT_ID,
spies.onScrollShouldSetResponderParent
);
onResponderGrant(CHILD_ID, onGrantChild);
onResponderGrant(PARENT_ID, onGrantParent);
spies.onStartShouldSetResponderChild.andReturn(true);
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
extracted = extractForTouchStart(CHILD_ID);
expect(spies.onStartShouldSetResponderChild.calls.length).toBe(1);
expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(0); // none yet
assertGrantEvent(CHILD_ID, extracted); // Child is the current responder
extracted = extractForTouchMove(CHILD_ID);
expect(spies.onMoveShouldSetResponderParent.calls.length).toBe(1);
assertNothingExtracted(extracted);
extracted = extractForScroll(CHILD_ID); // Could have been parent here too.
expect(spies.onScrollShouldSetResponderParent.calls.length).toBe(1);
assertGrantEvent(PARENT_ID, extracted);
assertTerminateEvent(CHILD_ID, extracted);
extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
assertRelease(PARENT_ID, extracted);
});
});

View File

@ -295,6 +295,16 @@ var ReactInstanceHandles = {
}
},
/**
* Same as `traverseTwoPhase` but skips the `targetID`.
*/
traverseTwoPhaseSkipTarget: function(targetID, cb, arg) {
if (targetID) {
traverseParentPath('', targetID, cb, arg, true, true);
traverseParentPath(targetID, '', cb, arg, true, true);
}
},
/**
* Traverse a node ID, calling the supplied `cb` for each ancestor ID. For
* example, passing `.0.$row-0.1` would result in `cb` getting called
@ -311,11 +321,7 @@ var ReactInstanceHandles = {
traverseParentPath('', targetID, cb, arg, true, false);
},
/**
* Exposed for unit testing.
* @private
*/
_getFirstCommonAncestorID: getFirstCommonAncestorID,
getFirstCommonAncestorID: getFirstCommonAncestorID,
/**
* Exposed for unit testing.

View File

@ -408,7 +408,7 @@ describe('ReactInstanceHandles', function() {
var i;
for (i = 0; i < ancestors.length; i++) {
var plan = ancestors[i];
var firstCommon = ReactInstanceHandles._getFirstCommonAncestorID(
var firstCommon = ReactInstanceHandles.getFirstCommonAncestorID(
getNodeID(plan.one),
getNodeID(plan.two)
);

View File

@ -30,9 +30,9 @@ var injection = {
injection.Mount = InjectedMount;
if (__DEV__) {
warning(
InjectedMount && InjectedMount.getNode,
InjectedMount && InjectedMount.getNode && InjectedMount.getID,
'EventPluginUtils.injection.injectMount(...): Injected Mount ' +
'module is missing getNode.'
'module is missing getNode or getID.'
);
}
}
@ -211,6 +211,14 @@ var EventPluginUtils = {
executeDispatchesInOrder: executeDispatchesInOrder,
executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,
hasDispatches: hasDispatches,
getNode: function(id) {
return injection.Mount.getNode(id);
},
getID: function(node) {
return injection.Mount.getID(node);
},
injection: injection
};

View File

@ -71,6 +71,19 @@ function accumulateTwoPhaseDispatchesSingle(event) {
}
}
/**
* Same as `accumulateTwoPhaseDispatchesSingle`, but skips over the targetID.
*/
function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
EventPluginHub.injection.getInstanceHandle().traverseTwoPhaseSkipTarget(
event.dispatchMarker,
accumulateDirectionalDispatches,
event
);
}
}
/**
* Accumulates without regard to direction, does not look for phased
@ -104,6 +117,10 @@ function accumulateTwoPhaseDispatches(events) {
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}
function accumulateTwoPhaseDispatchesSkipTarget(events) {
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget);
}
function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) {
EventPluginHub.injection.getInstanceHandle().traverseEnterLeave(
fromID,
@ -134,6 +151,7 @@ function accumulateDirectDispatches(events) {
*/
var EventPropagators = {
accumulateTwoPhaseDispatches: accumulateTwoPhaseDispatches,
accumulateTwoPhaseDispatchesSkipTarget: accumulateTwoPhaseDispatchesSkipTarget,
accumulateDirectDispatches: accumulateDirectDispatches,
accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches
};

View File

@ -0,0 +1,591 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ResponderEventPlugin
*/
'use strict';
var EventConstants = require('EventConstants');
var EventPluginUtils = require('EventPluginUtils');
var EventPropagators = require('EventPropagators');
var ReactInstanceHandles = require('ReactInstanceHandles');
var ResponderSyntheticEvent = require('ResponderSyntheticEvent');
var ResponderTouchHistoryStore = require('ResponderTouchHistoryStore');
var accumulate = require('accumulate');
var invariant = require('invariant');
var keyOf = require('keyOf');
var isStartish = EventPluginUtils.isStartish;
var isMoveish = EventPluginUtils.isMoveish;
var isEndish = EventPluginUtils.isEndish;
var executeDirectDispatch = EventPluginUtils.executeDirectDispatch;
var hasDispatches = EventPluginUtils.hasDispatches;
var executeDispatchesInOrderStopAtTrue =
EventPluginUtils.executeDispatchesInOrderStopAtTrue;
/**
* ID of element that should respond to touch/move types of interactions, as
* indicated explicitly by relevant callbacks.
*/
var responderID = null;
/**
* Count of current touches. A textInput should become responder iff the
* the selection changes while there is a touch on the screen.
*/
var trackedTouchCount = 0;
/**
* Last reported number of active touches.
*/
var previousActiveTouches = 0;
var changeResponder = function(nextResponderID) {
var oldResponderID = responderID;
responderID = nextResponderID;
if (ResponderEventPlugin.GlobalResponderHandler !== null) {
ResponderEventPlugin.GlobalResponderHandler.onChange(
oldResponderID,
nextResponderID
);
}
};
var eventTypes = {
/**
* On a `touchStart`/`mouseDown`, is it desired that this element become the
* responder?
*/
startShouldSetResponder: {
phasedRegistrationNames: {
bubbled: keyOf({onStartShouldSetResponder: null}),
captured: keyOf({onStartShouldSetResponderCapture: null})
}
},
/**
* On a `scroll`, is it desired that this element become the responder? This
* is usually not needed, but should be used to retroactively infer that a
* `touchStart` had occured during momentum scroll. During a momentum scroll,
* a touch start will be immediately followed by a scroll event if the view is
* currently scrolling.
*
* TODO: This shouldn't bubble.
*/
scrollShouldSetResponder: {
phasedRegistrationNames: {
bubbled: keyOf({onScrollShouldSetResponder: null}),
captured: keyOf({onScrollShouldSetResponderCapture: null})
}
},
/**
* On text selection change, should this element become the responder? This
* is needed for text inputs or other views with native selection, so the
* JS view can claim the responder.
*
* TODO: This shouldn't bubble.
*/
selectionChangeShouldSetResponder: {
phasedRegistrationNames: {
bubbled: keyOf({onSelectionChangeShouldSetResponder: null}),
captured: keyOf({onSelectionChangeShouldSetResponderCapture: null})
}
},
/**
* On a `touchMove`/`mouseMove`, is it desired that this element become the
* responder?
*/
moveShouldSetResponder: {
phasedRegistrationNames: {
bubbled: keyOf({onMoveShouldSetResponder: null}),
captured: keyOf({onMoveShouldSetResponderCapture: null})
}
},
/**
* Direct responder events dispatched directly to responder. Do not bubble.
*/
responderStart: {registrationName: keyOf({onResponderStart: null})},
responderMove: {registrationName: keyOf({onResponderMove: null})},
responderEnd: {registrationName: keyOf({onResponderEnd: null})},
responderRelease: {registrationName: keyOf({onResponderRelease: null})},
responderTerminationRequest: {
registrationName: keyOf({onResponderTerminationRequest: null})
},
responderGrant: {registrationName: keyOf({onResponderGrant: null})},
responderReject: {registrationName: keyOf({onResponderReject: null})},
responderTerminate: {registrationName: keyOf({onResponderTerminate: null})}
};
/**
*
* Responder System:
* ----------------
*
* - A global, solitary "interaction lock" on a view.
* - If a node becomes the responder, it should convey visual feedback
* immediately to indicate so, either by highlighting or moving accordingly.
* - To be the responder means, that touches are exclusively important to that
* responder view, and no other view.
* - While touches are still occuring, the responder lock can be transfered to
* a new view, but only to increasingly "higher" views (meaning ancestors of
* the current responder).
*
* Responder being granted:
* ------------------------
*
* - Touch starts, moves, and scrolls can cause an ID to become the responder.
* - We capture/bubble `startShouldSetResponder`/`moveShouldSetResponder` to
* the "appropriate place".
* - If nothing is currently the responder, the "appropriate place" is the
* initiating event's `targetID`.
* - If something *is* already the responder, the "appropriate place" is the
* first common ancestor of the event target and the current `responderID`.
* - Some negotiation happens: See the timing diagram below.
* - Scrolled views automatically become responder. The reasoning is that a
* platform scroll view that isn't built on top of the responder system has
* began scrolling, and the active responder must now be notified that the
* interaction is no longer locked to it - the system has taken over.
*
* - Responder being released:
* As soon as no more touches that *started* inside of descendents of the
* *current* responderID, an `onResponderRelease` event is dispatched to the
* current responder, and the responder lock is released.
*
* TODO:
* - on "end", a callback hook for `onResponderEndShouldRemainResponder` that
* determines if the responder lock should remain.
* - If a view shouldn't "remain" the responder, any active touches should by
* default be considered "dead" and do not influence future negotiations or
* bubble paths. It should be as if those touches do not exist.
* -- For multitouch: Usually a translate-z will choose to "remain" responder
* after one out of many touches ended. For translate-y, usually the view
* doesn't wish to "remain" responder after one of many touches end.
* - Consider building this on top of a `stopPropagation` model similar to
* `W3C` events.
* - Ensure that `onResponderTerminate` is called on touch cancels, whether or
* not `onResponderTerminationRequest` returns `true` or `false`.
*
*/
/* Negotiation Performed
+-----------------------+
/ \
Process low level events to + Current Responder + wantsResponderID
determine who to perform negot-| (if any exists at all) |
iation/transition | Otherwise just pass through|
-------------------------------+----------------------------+------------------+
Bubble to find first ID | |
to return true:wantsResponderID| |
| |
+-------------+ | |
| onTouchStart| | |
+------+------+ none | |
| return| |
+-----------v-------------+true| +------------------------+ |
|onStartShouldSetResponder|----->|onResponderStart (cur) |<-----------+
+-----------+-------------+ | +------------------------+ | |
| | | +--------+-------+
| returned true for| false:REJECT +-------->|onResponderReject
| wantsResponderID | | | +----------------+
| (now attempt | +------------------+-----+ |
| handoff) | | onResponder | |
+------------------->| TerminationRequest| |
| +------------------+-----+ |
| | | +----------------+
| true:GRANT +-------->|onResponderGrant|
| | +--------+-------+
| +------------------------+ | |
| | onResponderTerminate |<-----------+
| +------------------+-----+ |
| | | +----------------+
| +-------->|onResponderStart|
| | +----------------+
Bubble to find first ID | |
to return true:wantsResponderID| |
| |
+-------------+ | |
| onTouchMove | | |
+------+------+ none | |
| return| |
+-----------v-------------+true| +------------------------+ |
|onMoveShouldSetResponder |----->|onResponderMove (cur) |<-----------+
+-----------+-------------+ | +------------------------+ | |
| | | +--------+-------+
| returned true for| false:REJECT +-------->|onResponderRejec|
| wantsResponderID | | | +----------------+
| (now attempt | +------------------+-----+ |
| handoff) | | onResponder | |
+------------------->| TerminationRequest| |
| +------------------+-----+ |
| | | +----------------+
| true:GRANT +-------->|onResponderGrant|
| | +--------+-------+
| +------------------------+ | |
| | onResponderTerminate |<-----------+
| +------------------+-----+ |
| | | +----------------+
| +-------->|onResponderMove |
| | +----------------+
| |
| |
Some active touch started| |
inside current responder | +------------------------+ |
+------------------------->| onResponderEnd | |
| | +------------------------+ |
+---+---------+ | |
| onTouchEnd | | |
+---+---------+ | |
| | +------------------------+ |
+------------------------->| onResponderEnd | |
No active touches started| +-----------+------------+ |
inside current responder | | |
| v |
| +------------------------+ |
| | onResponderRelease | |
| +------------------------+ |
| |
+ + */
/**
* A note about event ordering in the `EventPluginHub`.
*
* Suppose plugins are injected in the following order:
*
* `[R, S, C]`
*
* To help illustrate the example, assume `S` is `SimpleEventPlugin` (for
* `onClick` etc) and `R` is `ResponderEventPlugin`.
*
* "Deferred-Dispatched Events":
*
* - The current event plugin system will traverse the list of injected plugins,
* in order, and extract events by collecting the plugin's return value of
* `extractEvents()`.
* - These events that are returned from `extractEvents` are "deferred
* dispatched events".
* - When returned from `extractEvents`, deferred-dispatched events contain an
* "accumulation" of deferred dispatches.
* - These deferred dispatches are accumulated/collected before they are
* returned, but processed at a later time by the `EventPluginHub` (hence the
* name deferred).
*
* In the process of returning their deferred-dispatched events, event plugins
* themselves can dispatch events on-demand without returning them from
* `extractEvents`. Plugins might want to do this, so that they can use event
* dispatching as a tool that helps them decide which events should be extracted
* in the first place.
*
* "On-Demand-Dispatched Events":
*
* - On-demand-dispatched events are not returned from `extractEvents`.
* - On-demand-dispatched events are dispatched during the process of returning
* the deferred-dispatched events.
* - They should not have side effects.
* - They should be avoided, and/or eventually be replaced with another
* abstraction that allows event plugins to perform multiple "rounds" of event
* extraction.
*
* Therefore, the sequence of event dispatches becomes:
*
* - `R`s on-demand events (if any) (dispatched by `R` on-demand)
* - `S`s on-demand events (if any) (dispatched by `S` on-demand)
* - `C`s on-demand events (if any) (dispatched by `C` on-demand)
* - `R`s extracted events (if any) (dispatched by `EventPluginHub`)
* - `S`s extracted events (if any) (dispatched by `EventPluginHub`)
* - `C`s extracted events (if any) (dispatched by `EventPluginHub`)
*
* In the case of `ResponderEventPlugin`: If the `startShouldSetResponder`
* on-demand dispatch returns `true` (and some other details are satisfied) the
* `onResponderGrant` deferred dispatched event is returned from
* `extractEvents`. The sequence of dispatch executions in this case
* will appear as follows:
*
* - `startShouldSetResponder` (`ResponderEventPlugin` dispatches on-demand)
* - `touchStartCapture` (`EventPluginHub` dispatches as usual)
* - `touchStart` (`EventPluginHub` dispatches as usual)
* - `responderGrant/Reject` (`EventPluginHub` dispatches as usual)
*
* @param {string} topLevelType Record from `EventConstants`.
* @param {string} topLevelTargetID ID of deepest React rendered element.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of synthetic events.
*/
function setResponderAndExtractTransfer(
topLevelType,
topLevelTargetID,
nativeEvent) {
var shouldSetEventType =
isStartish(topLevelType) ? eventTypes.startShouldSetResponder :
isMoveish(topLevelType) ? eventTypes.moveShouldSetResponder :
topLevelType === EventConstants.topLevelTypes.topSelectionChange ?
eventTypes.selectionChangeShouldSetResponder :
eventTypes.scrollShouldSetResponder;
// TODO: stop one short of the the current responder.
var bubbleShouldSetFrom = !responderID ?
topLevelTargetID :
ReactInstanceHandles.getFirstCommonAncestorID(responderID, topLevelTargetID);
// When capturing/bubbling the "shouldSet" event, we want to skip the target
// (deepest ID) if it happens to be the current responder. The reasoning:
// It's strange to get an `onMoveShouldSetResponder` when you're *already*
// the responder.
var skipOverBubbleShouldSetFrom = bubbleShouldSetFrom === responderID;
var shouldSetEvent = ResponderSyntheticEvent.getPooled(
shouldSetEventType,
bubbleShouldSetFrom,
nativeEvent
);
shouldSetEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
if (skipOverBubbleShouldSetFrom) {
EventPropagators.accumulateTwoPhaseDispatchesSkipTarget(shouldSetEvent);
} else {
EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent);
}
var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
if (!shouldSetEvent.isPersistent()) {
shouldSetEvent.constructor.release(shouldSetEvent);
}
if (!wantsResponderID || wantsResponderID === responderID) {
return null;
}
var extracted;
var grantEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderGrant,
wantsResponderID,
nativeEvent
);
grantEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
EventPropagators.accumulateDirectDispatches(grantEvent);
if (responderID) {
var terminationRequestEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderTerminationRequest,
responderID,
nativeEvent
);
terminationRequestEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
EventPropagators.accumulateDirectDispatches(terminationRequestEvent);
var shouldSwitch = !hasDispatches(terminationRequestEvent) ||
executeDirectDispatch(terminationRequestEvent);
if (!terminationRequestEvent.isPersistent()) {
terminationRequestEvent.constructor.release(terminationRequestEvent);
}
if (shouldSwitch) {
var terminateType = eventTypes.responderTerminate;
var terminateEvent = ResponderSyntheticEvent.getPooled(
terminateType,
responderID,
nativeEvent
);
terminateEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
EventPropagators.accumulateDirectDispatches(terminateEvent);
extracted = accumulate(extracted, [grantEvent, terminateEvent]);
changeResponder(wantsResponderID);
} else {
var rejectEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderReject,
wantsResponderID,
nativeEvent
);
rejectEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
EventPropagators.accumulateDirectDispatches(rejectEvent);
extracted = accumulate(extracted, rejectEvent);
}
} else {
extracted = accumulate(extracted, grantEvent);
changeResponder(wantsResponderID);
}
return extracted;
}
/**
* A transfer is a negotiation between a currently set responder and the next
* element to claim responder status. Any start event could trigger a transfer
* of responderID. Any move event could trigger a transfer.
*
* @param {string} topLevelType Record from `EventConstants`.
* @return {boolean} True if a transfer of responder could possibly occur.
*/
function canTriggerTransfer(topLevelType, topLevelTargetID) {
return topLevelTargetID && (
topLevelType === EventConstants.topLevelTypes.topScroll ||
(trackedTouchCount > 0 &&
topLevelType === EventConstants.topLevelTypes.topSelectionChange) ||
isStartish(topLevelType) ||
isMoveish(topLevelType)
);
}
/**
* Returns whether or not this touch end event makes it such that there are no
* longer any touches that started inside of the current `responderID`.
*
* @param {NativeEvent} nativeEvent Native touch end event.
* @return {bool} Whether or not this touch end event ends the responder.
*/
function noResponderTouches(nativeEvent) {
var touches = nativeEvent.touches;
if (!touches || touches.length === 0) {
return true;
}
for (var i = 0; i < touches.length; i++) {
var activeTouch = touches[i];
var target = activeTouch.target;
if (target !== null && target !== undefined && target !== 0) {
// Is the original touch location inside of the current responder?
var isAncestor =
ReactInstanceHandles.isAncestorIDOf(
responderID,
EventPluginUtils.getID(target)
);
if (isAncestor) {
return false;
}
}
}
return true;
}
var ResponderEventPlugin = {
getResponderID: function() {
return responderID;
},
eventTypes: eventTypes,
/**
* We must be resilient to `topLevelTargetID` being `undefined` on
* `touchMove`, or `touchEnd`. On certain platforms, this means that a native
* scroll has assumed control and the original touch targets are destroyed.
*
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of synthetic events.
* @see {EventPluginHub.extractEvents}
*/
extractEvents: function(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent) {
if (isStartish(topLevelType)) {
trackedTouchCount += 1;
} else if (isEndish(topLevelType)) {
trackedTouchCount -= 1;
invariant(
trackedTouchCount >= 0,
'Ended a touch event which was not counted in trackedTouchCount.'
);
}
ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent);
var extracted = canTriggerTransfer(topLevelType, topLevelTargetID) ?
setResponderAndExtractTransfer(topLevelType, topLevelTargetID, nativeEvent) :
null;
// Responder may or may not have transfered on a new touch start/move.
// Regardless, whoever is the responder after any potential transfer, we
// direct all touch start/move/ends to them in the form of
// `onResponderMove/Start/End`. These will be called for *every* additional
// finger that move/start/end, dispatched directly to whoever is the
// current responder at that moment, until the responder is "released".
//
// These multiple individual change touch events are are always bookended
// by `onResponderGrant`, and one of
// (`onResponderRelease/onResponderTerminate`).
var isResponderTouchStart = responderID && isStartish(topLevelType);
var isResponderTouchMove = responderID && isMoveish(topLevelType);
var isResponderTouchEnd = responderID && isEndish(topLevelType);
var incrementalTouch =
isResponderTouchStart ? eventTypes.responderStart :
isResponderTouchMove ? eventTypes.responderMove :
isResponderTouchEnd ? eventTypes.responderEnd :
null;
if (incrementalTouch) {
var gesture =
ResponderSyntheticEvent.getPooled(incrementalTouch, responderID, nativeEvent);
gesture.touchHistory = ResponderTouchHistoryStore.touchHistory;
EventPropagators.accumulateDirectDispatches(gesture);
extracted = accumulate(extracted, gesture);
}
var isResponderTerminate =
responderID &&
topLevelType === EventConstants.topLevelTypes.topTouchCancel;
var isResponderRelease =
responderID &&
!isResponderTerminate &&
isEndish(topLevelType) &&
noResponderTouches(nativeEvent);
var finalTouch =
isResponderTerminate ? eventTypes.responderTerminate :
isResponderRelease ? eventTypes.responderRelease :
null;
if (finalTouch) {
var finalEvent =
ResponderSyntheticEvent.getPooled(finalTouch, responderID, nativeEvent);
finalEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
EventPropagators.accumulateDirectDispatches(finalEvent);
extracted = accumulate(extracted, finalEvent);
changeResponder(null);
}
var numberActiveTouches =
ResponderTouchHistoryStore.touchHistory.numberActiveTouches;
if (ResponderEventPlugin.GlobalInteractionHandler &&
numberActiveTouches !== previousActiveTouches) {
ResponderEventPlugin.GlobalInteractionHandler.onChange(
numberActiveTouches
);
}
previousActiveTouches = numberActiveTouches;
return extracted;
},
GlobalResponderHandler: null,
GlobalInteractionHandler: null,
injection: {
/**
* @param {{onChange: (ReactID, ReactID) => void} GlobalResponderHandler
* Object that handles any change in responder. Use this to inject
* integration with an existing touch handling system etc.
*/
injectGlobalResponderHandler: function(GlobalResponderHandler) {
ResponderEventPlugin.GlobalResponderHandler = GlobalResponderHandler;
},
/**
* @param {{onChange: (numberActiveTouches) => void} GlobalInteractionHandler
* Object that handles any change in the number of active touches.
*/
injectGlobalInteractionHandler: function(GlobalInteractionHandler) {
ResponderEventPlugin.GlobalInteractionHandler = GlobalInteractionHandler;
}
}
};
module.exports = ResponderEventPlugin;

View File

@ -0,0 +1,40 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ResponderSyntheticEvent
* @typechecks static-only
*/
'use strict';
var SyntheticEvent = require('SyntheticEvent');
/**
* `touchHistory` isn't actually on the native event, but putting it in the
* interface will ensure that it is cleaned up when pooled/destroyed. The
* `ResponderEventPlugin` will populate it appropriately.
*/
var ResponderEventInterface = {
touchHistory: function(nativeEvent) {
return null; // Actually doesn't even look at the native event.
}
};
/**
* @param {object} dispatchConfig Configuration used to dispatch this event.
* @param {string} dispatchMarker Marker identifying the event target.
* @param {object} nativeEvent Native event.
* @extends {SyntheticEvent}
*/
function ResponderSyntheticEvent(dispatchConfig, dispatchMarker, nativeEvent) {
SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);
}
SyntheticEvent.augmentClass(ResponderSyntheticEvent, ResponderEventInterface);
module.exports = ResponderSyntheticEvent;

View File

@ -0,0 +1,185 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ResponderTouchHistoryStore
*/
'use strict';
var EventPluginUtils = require('EventPluginUtils');
var invariant = require('invariant');
var isMoveish = EventPluginUtils.isMoveish;
var isStartish = EventPluginUtils.isStartish;
var isEndish = EventPluginUtils.isEndish;
var MAX_TOUCH_BANK = 20;
/**
* Touch position/time tracking information by touchID. Typically, we'll only
* see IDs with a range of 1-20 (they are recycled when touches end and then
* start again). This data is commonly needed by many different interaction
* logic modules so precomputing it is very helpful to do once.
* Each touch object in `touchBank` is of the following form:
* { touchActive: boolean,
* startTimeStamp: number,
* startPageX: number,
* startPageY: number,
* currentPageX: number,
* currentPageY: number,
* currentTimeStamp: number
* }
*/
var touchHistory = {
touchBank: [ ],
numberActiveTouches: 0,
// If there is only one active touch, we remember its location. This prevents
// us having to loop through all of the touches all the time in the most
// common case.
indexOfSingleActiveTouch: -1,
mostRecentTimeStamp: 0
};
var timestampForTouch = function(touch) {
// The legacy internal implementation provides "timeStamp", which has been
// renamed to "timestamp". Let both work for now while we iron it out
// TODO (evv): rename timeStamp to timestamp in internal code
return touch.timeStamp || touch.timestamp;
};
/**
* TODO: Instead of making gestures recompute filtered velocity, we could
* include a built in velocity computation that can be reused globally.
* @param {Touch} touch Native touch object.
*/
var initializeTouchData = function(touch) {
return {
touchActive: true,
startTimeStamp: timestampForTouch(touch),
startPageX: touch.pageX,
startPageY: touch.pageY,
currentPageX: touch.pageX,
currentPageY: touch.pageY,
currentTimeStamp: timestampForTouch(touch),
previousPageX: touch.pageX,
previousPageY: touch.pageY,
previousTimeStamp: timestampForTouch(touch)
};
};
var reinitializeTouchTrack = function(touchTrack, touch) {
touchTrack.touchActive = true;
touchTrack.startTimeStamp = timestampForTouch(touch);
touchTrack.startPageX = touch.pageX;
touchTrack.startPageY = touch.pageY;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
touchTrack.currentTimeStamp = timestampForTouch(touch);
touchTrack.previousPageX = touch.pageX;
touchTrack.previousPageY = touch.pageY;
touchTrack.previousTimeStamp = timestampForTouch(touch);
};
var validateTouch = function(touch) {
var identifier = touch.identifier;
invariant(identifier != null, 'Touch object is missing identifier');
if (identifier > MAX_TOUCH_BANK) {
console.warn(
'Touch identifier ' + identifier + ' is greater than maximum ' +
'supported ' + MAX_TOUCH_BANK + ' which causes performance issues ' +
'backfilling array locations for all of the indices.'
);
}
};
var recordStartTouchData = function(touch) {
var touchBank = touchHistory.touchBank;
var identifier = touch.identifier;
var touchTrack = touchBank[identifier];
if (__DEV__) {
validateTouch(touch);
}
if (!touchTrack) {
touchBank[touch.identifier] = initializeTouchData(touch);
} else {
reinitializeTouchTrack(touchTrack, touch);
}
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
var recordMoveTouchData = function(touch) {
var touchBank = touchHistory.touchBank;
var touchTrack = touchBank[touch.identifier];
if (__DEV__) {
validateTouch(touch);
invariant(touchTrack, 'Touch data should have been recorded on start');
}
touchTrack.touchActive = true;
touchTrack.previousPageX = touchTrack.currentPageX;
touchTrack.previousPageY = touchTrack.currentPageY;
touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
touchTrack.currentTimeStamp = timestampForTouch(touch);
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
var recordEndTouchData = function(touch) {
var touchBank = touchHistory.touchBank;
var touchTrack = touchBank[touch.identifier];
if (__DEV__) {
validateTouch(touch);
invariant(touchTrack, 'Touch data should have been recorded on start');
}
touchTrack.previousPageX = touchTrack.currentPageX;
touchTrack.previousPageY = touchTrack.currentPageY;
touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
touchTrack.currentTimeStamp = timestampForTouch(touch);
touchTrack.touchActive = false;
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
var ResponderTouchHistoryStore = {
recordTouchTrack: function(topLevelType, nativeEvent) {
var touchBank = touchHistory.touchBank;
if (isMoveish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordMoveTouchData);
} else if (isStartish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordStartTouchData);
touchHistory.numberActiveTouches = nativeEvent.touches.length;
if (touchHistory.numberActiveTouches === 1) {
touchHistory.indexOfSingleActiveTouch = nativeEvent.touches[0].identifier;
}
} else if (isEndish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordEndTouchData);
touchHistory.numberActiveTouches = nativeEvent.touches.length;
if (touchHistory.numberActiveTouches === 1) {
for (var i = 0; i < touchBank.length; i++) {
var touchTrackToCheck = touchBank[i];
if (touchTrackToCheck != null && touchTrackToCheck.touchActive) {
touchHistory.indexOfSingleActiveTouch = i;
break;
}
}
if (__DEV__) {
var activeTouchData = touchBank[touchHistory.indexOfSingleActiveTouch];
var foundActive = activeTouchData != null && !!activeTouchData.touchActive;
invariant(foundActive, 'Cannot find single active touch');
}
}
}
},
touchHistory: touchHistory
};
module.exports = ResponderTouchHistoryStore;

View File

@ -0,0 +1,967 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
'use strict';
var EventPluginHub;
var EventConstants;
var EventPropagators;
var ReactInstanceHandles;
var ResponderEventPlugin;
var SyntheticEvent;
var EventPluginUtils;
var GRANDPARENT_ID = '.0';
var PARENT_ID = '.0.0';
var CHILD_ID = '.0.0.0';
var CHILD_ID2 = '.0.0.1';
var topLevelTypes;
var responderEventTypes;
var touch = function(nodeHandle, i) {
return {target: nodeHandle, identifier: i};
};
/**
* @param {NodeHandle} nodeHandle @see NodeHandle. Handle of target.
* @param {Array<Touch>} touches All active touches.
* @param {Array<Touch>} changedTouches Only the touches that have changed.
* @return {TouchEvent} Model of a touch event that is compliant with responder
* system plugin.
*/
var touchEvent = function(nodeHandle, touches, changedTouches) {
return {
target: nodeHandle,
changedTouches: changedTouches,
touches: touches
};
};
var subsequence = function(arr, indices) {
var ret = [];
for (var i = 0; i < indices.length; i++) {
var index = indices[i];
ret.push(arr[index]);
}
return ret;
};
var antiSubsequence = function(arr, indices) {
var ret = [];
for (var i = 0; i < arr.length; i++) {
if (indices.indexOf(i) === -1) {
ret.push(arr[i]);
}
}
return ret;
};
/**
* Helper for creating touch test config data.
* @param allTouchHandles
*/
var _touchConfig =
function(topType, targetNodeHandle, allTouchHandles, changedIndices) {
var allTouchObjects = allTouchHandles.map(touch);
var changedTouchObjects = subsequence(allTouchObjects, changedIndices);
var activeTouchObjects =
topType === 'topTouchStart' ? allTouchObjects :
topType === 'topTouchMove' ? allTouchObjects :
topType === 'topTouchEnd' ? antiSubsequence(allTouchObjects, changedIndices) :
topType === 'topTouchCancel' ? antiSubsequence(allTouchObjects, changedIndices) :
null;
return {
nativeEvent: touchEvent(
targetNodeHandle,
activeTouchObjects,
changedTouchObjects
),
topLevelType: topType,
target: targetNodeHandle,
targetID: targetNodeHandle,
};
};
/**
* Creates test data for touch events using environment agnostic "node
* handles".
*
* @param {NodeHandle} nodeHandle Environment agnostic handle to DOM node.
* @param {Array<NodeHandle>} allTouchHandles Encoding of all "touches" in the
* form of a mapping from integer (touch `identifier`) to touch target. This is
* encoded in array form. Because of this, it is possible for two separate
* touches (meaning two separate indices) to have the same touch target ID -
* this corresponds to real world cases where two separate unique touches have
* the same target. These touches don't just represent all active touches,
* rather it also includes any touches that are not active, but are in the
* process of being removed.
* @param {Array<NodeHandle>} changedIndices Indices of `allTouchHandles` that
* have changed.
* @return {object} Config data used by test cases for extracting responder
* events.
*/
var startConfig = function(nodeHandle, allTouchHandles, changedIndices) {
return _touchConfig(
topLevelTypes.topTouchStart,
nodeHandle,
allTouchHandles,
changedIndices
);
};
/**
* @see `startConfig`
*/
var moveConfig = function(nodeHandle, allTouchHandles, changedIndices) {
return _touchConfig(
topLevelTypes.topTouchMove,
nodeHandle,
allTouchHandles,
changedIndices
);
};
/**
* @see `startConfig`
*/
var endConfig = function(nodeHandle, allTouchHandles, changedIndices) {
return _touchConfig(
topLevelTypes.topTouchEnd,
nodeHandle,
allTouchHandles,
changedIndices
);
};
/**
* Test config for events that aren't negotiation related, but rather result of
* a negotiation.
*
* Returns object of the form:
*
* {
* responderReject: {
* // Whatever "readableIDToID" was passed in.
* grandParent: {order: NA, assertEvent: null, returnVal: blah},
* ...
* child: {order: NA, assertEvent: null, returnVal: blah},
* }
* responderGrant: {
* grandParent: {order: NA, assertEvent: null, returnVal: blah},
* ...
* child: {order: NA, assertEvent: null, returnVal: blah}
* }
* ...
* }
*
* After this is created, a test case would configure specific event orderings
* and optional assertions. Anything left with an `order` of `NA` will be
* required to never be invoked (the test runner will make sure it throws if
* ever invoked).
*
*/
var NA = -1;
var oneEventLoopTestConfig = function(readableIDToID) {
var ret = {
// Negotiation
scrollShouldSetResponder: {bubbled: {}, captured: {}},
startShouldSetResponder: {bubbled: {}, captured: {}},
moveShouldSetResponder: {bubbled: {}, captured: {}},
responderTerminationRequest: {},
// Non-negotiation
responderReject: {}, // These do not bubble capture.
responderGrant: {},
responderStart: {},
responderMove: {},
responderTerminate: {},
responderEnd: {},
responderRelease: {},
};
for (var eventName in ret) {
for (var readableNodeName in readableIDToID) {
if (ret[eventName].bubbled) {
// Two phase
ret[eventName].bubbled[readableNodeName] =
{order: NA, assertEvent: null, returnVal: undefined};
ret[eventName].captured[readableNodeName] =
{order: NA, assertEvent: null, returnVal: undefined};
} else {
ret[eventName][readableNodeName] =
{order: NA, assertEvent: null, returnVal: undefined};
}
}
}
return ret;
};
/**
* @param {object} sequence See `oneEventLoopTestConfig`.
*/
var registerTestHandlers = function(eventTestConfig, readableIDToID) {
var runs = {dispatchCount: 0};
var neverFire = function (readableID, registrationName) {
runs.dispatchCount++;
expect('').toBe(
'Event type: ' + registrationName +
'\nShould never occur on:' + readableID +
'\nFor event test config:\n' + JSON.stringify(eventTestConfig) + '\n'
);
};
var registerOneEventType = function(registrationName, eventTypeTestConfig) {
for (var readableID in eventTypeTestConfig) {
var nodeConfig = eventTypeTestConfig[readableID];
var id = readableIDToID[readableID];
var handler = nodeConfig.order === NA ? neverFire.bind(null, readableID, registrationName) :
function(readableID, registrationName, nodeConfig, e) {
expect(
readableID + '->' + registrationName + ' index:' + runs.dispatchCount++
).toBe(
readableID + '->' + registrationName + ' index:' + nodeConfig.order
);
nodeConfig.assertEvent && nodeConfig.assertEvent(e);
return nodeConfig.returnVal;
}.bind(null, readableID, registrationName, nodeConfig);
EventPluginHub.putListener(id, registrationName, handler);
}
};
for (var eventName in eventTestConfig) {
var oneEventTypeTestConfig = eventTestConfig[eventName];
var hasTwoPhase = !!oneEventTypeTestConfig.bubbled;
if (hasTwoPhase) {
registerOneEventType(
ResponderEventPlugin.eventTypes[eventName].phasedRegistrationNames.bubbled,
oneEventTypeTestConfig.bubbled
);
registerOneEventType(
ResponderEventPlugin.eventTypes[eventName].phasedRegistrationNames.captured,
oneEventTypeTestConfig.captured
);
} else {
registerOneEventType(
ResponderEventPlugin.eventTypes[eventName].registrationName,
oneEventTypeTestConfig
);
}
}
return runs;
};
var run = function(config, hierarchyConfig, nativeEventConfig) {
var max = NA;
var searchForMax = function(nodeConfig) {
for (var readableID in nodeConfig) {
var order = nodeConfig[readableID].order;
max = order > max ? order : max;
}
};
for (var eventName in config) {
var eventConfig = config[eventName];
if (eventConfig.bubbled) {
searchForMax(eventConfig.bubbled);
searchForMax(eventConfig.captured);
} else {
searchForMax(eventConfig);
}
}
// Register the handlers
var runData = registerTestHandlers(config, hierarchyConfig);
// Trigger the event
var extractedEvents = ResponderEventPlugin.extractEvents(
nativeEventConfig.topLevelType,
nativeEventConfig.target,
nativeEventConfig.targetID,
nativeEventConfig.nativeEvent
);
// At this point the negotiation events have been dispatched as part of the
// extraction process, but not the side effectful events. Below, we dispatch
// side effectful events.
EventPluginHub.enqueueEvents(extractedEvents);
EventPluginHub.processEventQueue();
// Ensure that every event that declared an `order`, was actually dispatched.
expect(
'number of events dispatched:' + runData.dispatchCount
).toBe(
'number of events dispatched:' + (max + 1)
); // +1 for extra ++
};
var three = {
grandParent: GRANDPARENT_ID,
parent: PARENT_ID,
child: CHILD_ID,
};
var siblings = {
parent: PARENT_ID,
childOne: CHILD_ID,
childTwo: CHILD_ID2,
};
describe('ResponderEventPlugin', function() {
beforeEach(function() {
require('mock-modules').dumpCache();
EventConstants = require('EventConstants');
EventPluginHub = require('EventPluginHub');
EventPluginUtils = require('EventPluginUtils');
EventPropagators = require('EventPropagators');
ReactInstanceHandles = require('ReactInstanceHandles');
ResponderEventPlugin = require('ResponderEventPlugin');
SyntheticEvent = require('SyntheticEvent');
EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles);
// Only needed because SyntheticEvent supports the `currentTarget`
// property.
EventPluginUtils.injection.injectMount({
getNode: function(id) {
return id;
},
getID: function(nodeHandle) {
return nodeHandle;
}
});
topLevelTypes = EventConstants.topLevelTypes;
responderEventTypes = ResponderEventPlugin.eventTypes;
});
it('should do nothing when no one wants to respond', function() {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
config.startShouldSetResponder.bubbled.parent = {order: 4, returnVal: false};
config.startShouldSetResponder.bubbled.grandParent = {order: 5, returnVal: false};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
// Now no handlers should be called on `touchEnd`.
config = oneEventLoopTestConfig(three);
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
/**
* Simple Start Granting
* --------------------
*/
it('should grant responder grandParent while capturing', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: true};
config.responderGrant.grandParent = {order: 1};
config.responderStart.grandParent = {order: 2};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
config = oneEventLoopTestConfig(three);
config.responderEnd.grandParent = {order: 0};
config.responderRelease.grandParent = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should grant responder parent while capturing', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: true};
config.responderGrant.parent = {order: 2};
config.responderStart.parent = {order: 3};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
config = oneEventLoopTestConfig(three);
config.responderEnd.parent = {order: 0};
config.responderRelease.parent = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should grant responder child while capturing', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.captured.child = {order: 2, returnVal: true};
config.responderGrant.child = {order: 3};
config.responderStart.child = {order: 4};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
config = oneEventLoopTestConfig(three);
config.responderEnd.child = {order: 0};
config.responderRelease.child = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should grant responder child while bubbling', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
config.responderGrant.child = {order: 4};
config.responderStart.child = {order: 5};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
config = oneEventLoopTestConfig(three);
config.responderEnd.child = {order: 0};
config.responderRelease.child = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should grant responder parent while bubbling', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
config.startShouldSetResponder.bubbled.parent = {order: 4, returnVal: true};
config.responderGrant.parent = {order: 5};
config.responderStart.parent = {order: 6};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
config = oneEventLoopTestConfig(three);
config.responderEnd.parent = {order: 0};
config.responderRelease.parent = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should grant responder grandParent while bubbling', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
config.startShouldSetResponder.bubbled.parent = {order: 4, returnVal: false};
config.startShouldSetResponder.bubbled.grandParent = {order: 5, returnVal: true};
config.responderGrant.grandParent = {order: 6};
config.responderStart.grandParent = {order: 7};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
config = oneEventLoopTestConfig(three);
config.responderEnd.grandParent = {order: 0};
config.responderRelease.grandParent = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
/**
* Simple Move Granting
* --------------------
*/
it('should grant responder grandParent while capturing move', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0};
config.startShouldSetResponder.captured.parent = {order: 1};
config.startShouldSetResponder.captured.child = {order: 2};
config.startShouldSetResponder.bubbled.child = {order: 3};
config.startShouldSetResponder.bubbled.parent = {order: 4};
config.startShouldSetResponder.bubbled.grandParent = {order: 5};
run(config, three, startConfig(three.child, [three.child], [0]));
config = oneEventLoopTestConfig(three);
config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: true};
config.responderGrant.grandParent = {order: 1};
config.responderMove.grandParent = {order: 2};
run(config, three, moveConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
config = oneEventLoopTestConfig(three);
config.responderEnd.grandParent = {order: 0};
config.responderRelease.grandParent = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should grant responder parent while capturing move', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0};
config.startShouldSetResponder.captured.parent = {order: 1};
config.startShouldSetResponder.captured.child = {order: 2};
config.startShouldSetResponder.bubbled.child = {order: 3};
config.startShouldSetResponder.bubbled.parent = {order: 4};
config.startShouldSetResponder.bubbled.grandParent = {order: 5};
run(config, three, startConfig(three.child, [three.child], [0]));
config = oneEventLoopTestConfig(three);
config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: true};
config.responderGrant.parent = {order: 2};
config.responderMove.parent = {order: 3};
run(config, three, moveConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
config = oneEventLoopTestConfig(three);
config.responderEnd.parent = {order: 0};
config.responderRelease.parent = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should grant responder child while capturing move', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0};
config.startShouldSetResponder.captured.parent = {order: 1};
config.startShouldSetResponder.captured.child = {order: 2};
config.startShouldSetResponder.bubbled.child = {order: 3};
config.startShouldSetResponder.bubbled.parent = {order: 4};
config.startShouldSetResponder.bubbled.grandParent = {order: 5};
run(config, three, startConfig(three.child, [three.child], [0]));
config = oneEventLoopTestConfig(three);
config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.moveShouldSetResponder.captured.child = {order: 2, returnVal: true};
config.responderGrant.child = {order: 3};
config.responderMove.child = {order: 4};
run(config, three, moveConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
config = oneEventLoopTestConfig(three);
config.responderEnd.child = {order: 0};
config.responderRelease.child = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should grant responder child while bubbling move', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0};
config.startShouldSetResponder.captured.parent = {order: 1};
config.startShouldSetResponder.captured.child = {order: 2};
config.startShouldSetResponder.bubbled.child = {order: 3};
config.startShouldSetResponder.bubbled.parent = {order: 4};
config.startShouldSetResponder.bubbled.grandParent = {order: 5};
run(config, three, startConfig(three.child, [three.child], [0]));
config = oneEventLoopTestConfig(three);
config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.moveShouldSetResponder.captured.child = {order: 2, returnVal: false};
config.moveShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
config.responderGrant.child = {order: 4};
config.responderMove.child = {order: 5};
run(config, three, moveConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
config = oneEventLoopTestConfig(three);
config.responderEnd.child = {order: 0};
config.responderRelease.child = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should grant responder parent while bubbling move', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0};
config.startShouldSetResponder.captured.parent = {order: 1};
config.startShouldSetResponder.captured.child = {order: 2};
config.startShouldSetResponder.bubbled.child = {order: 3};
config.startShouldSetResponder.bubbled.parent = {order: 4};
config.startShouldSetResponder.bubbled.grandParent = {order: 5};
run(config, three, startConfig(three.child, [three.child], [0]));
config = oneEventLoopTestConfig(three);
config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.moveShouldSetResponder.captured.child = {order: 2, returnVal: false};
config.moveShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
config.moveShouldSetResponder.bubbled.parent = {order: 4, returnVal: true};
config.responderGrant.parent = {order: 5};
config.responderMove.parent = {order: 6};
run(config, three, moveConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
config = oneEventLoopTestConfig(three);
config.responderEnd.parent = {order: 0};
config.responderRelease.parent = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should grant responder grandParent while bubbling move', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0};
config.startShouldSetResponder.captured.parent = {order: 1};
config.startShouldSetResponder.captured.child = {order: 2};
config.startShouldSetResponder.bubbled.child = {order: 3};
config.startShouldSetResponder.bubbled.parent = {order: 4};
config.startShouldSetResponder.bubbled.grandParent = {order: 5};
run(config, three, startConfig(three.child, [three.child], [0]));
config = oneEventLoopTestConfig(three);
config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.moveShouldSetResponder.captured.child = {order: 2, returnVal: false};
config.moveShouldSetResponder.bubbled.child = {order: 3, returnVal: false};
config.moveShouldSetResponder.bubbled.parent = {order: 4, returnVal: false};
config.moveShouldSetResponder.bubbled.grandParent = {order: 5, returnVal: true};
config.responderGrant.grandParent = {order: 6};
config.responderMove.grandParent = {order: 7};
run(config, three, moveConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
config = oneEventLoopTestConfig(three);
config.responderEnd.grandParent = {order: 0};
config.responderRelease.grandParent = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
/**
* Common ancestor tests
* ---------------------
*/
it('should bubble negotiation to first common ancestor of responder', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: true};
config.responderGrant.parent = {order: 2};
config.responderStart.parent = {order: 3};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
// While `PARENT_ID` is still responder, we create new handlers that verify
// the ordering of propagation, restarting the count at `0`.
config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.bubbled.grandParent = {order: 1, returnVal: false};
config.responderStart.parent = {order: 2};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
config = oneEventLoopTestConfig(three);
config.responderEnd.parent = {order: 0};
config.responderRelease.parent = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
it('should bubble negotiation to first common ancestor of responder then transfer', () => {
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: true};
config.responderGrant.parent = {order: 2};
config.responderStart.parent = {order: 3};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
config = oneEventLoopTestConfig(three);
// Parent is responder, and responder is transfered by a second touch start
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: true};
config.responderTerminationRequest.parent = {order: 1, returnVal: true};
config.responderGrant.grandParent = {order: 2};
config.responderTerminate.parent = {order: 3};
config.responderStart.grandParent = {order: 4};
run(config, three, startConfig(three.child, [three.child, three.child], [1]));
expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
config = oneEventLoopTestConfig(three);
config.responderEnd.grandParent = {order: 0};
// one remains\ /one ended \
run(config, three, endConfig(three.child, [three.child, three.child], [1]));
expect(ResponderEventPlugin.getResponderID()).toBe(GRANDPARENT_ID);
config = oneEventLoopTestConfig(three);
config.responderEnd.grandParent = {order: 0};
config.responderRelease.grandParent = {order: 1};
run(config, three, endConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
/**
* If nothing is responder, then the negotiation should propagate directly to
* the deepest target in the second touch.
*/
it('should negotiate with deepest target on second touch if nothing is responder', () => {
// Initially nothing wants to become the responder
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.bubbled.parent = {order: 2, returnVal: false};
config.startShouldSetResponder.bubbled.grandParent = {order: 3, returnVal: false};
run(config, three, startConfig(three.parent, [three.parent], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
config = oneEventLoopTestConfig(three);
// Now child wants to become responder. Negotiation should bubble as deep
// as the target is because we don't find first common ancestor (with
// current responder) because there is no current responder.
// (Even if this is the second active touch).
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
config.responderGrant.child = {order: 4};
config.responderStart.child = {order: 5};
// / Two active touches \ /one of them new\
run(config, three, startConfig(three.child, [three.parent, three.child], [1]));
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
// Now we remove the original first touch, keeping the second touch that
// started within the current responder (child). Nothing changes because
// there's still touches that started inside of the current responder.
config = oneEventLoopTestConfig(three);
config.responderEnd.child = {order: 0};
// / one ended\ /one remains \
run(config, three, endConfig(three.child, [three.parent, three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
// Okay, now let's add back that first touch (nothing should change) and
// then we'll try peeling back the touches in the opposite order to make
// sure that first removing the second touch instantly causes responder to
// be released.
config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.bubbled.parent = {order: 2, returnVal: false};
config.startShouldSetResponder.bubbled.grandParent = {order: 3, returnVal: false};
// Interesting: child still gets moves even though touch target is parent!
// Current responder gets a `responderStart` for any touch while responder.
config.responderStart.child = {order: 4};
// / Two active touches \ /one of them new\
run(config, three, startConfig(three.parent, [three.child, three.parent], [1]));
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
// Now, move that new touch that had no effect, and did not start within
// the current responder.
config = oneEventLoopTestConfig(three);
config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.moveShouldSetResponder.bubbled.parent = {order: 2, returnVal: false};
config.moveShouldSetResponder.bubbled.grandParent = {order: 3, returnVal: false};
// Interesting: child still gets moves even though touch target is parent!
// Current responder gets a `responderMove` for any touch while responder.
config.responderMove.child = {order: 4};
// / Two active touches \ /one of them moved\
run(config, three, moveConfig(three.parent, [three.child, three.parent], [1]));
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
config = oneEventLoopTestConfig(three);
config.responderEnd.child = {order: 0};
config.responderRelease.child = {order: 1};
// /child end \ /parent remain\
run(config, three, endConfig(three.child, [three.child, three.parent], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
/**
* If nothing is responder, then the negotiation should propagate directly to
* the deepest target in the second touch.
*/
it('should negotiate until first common ancestor when there are siblings', () => {
// Initially nothing wants to become the responder
var config = oneEventLoopTestConfig(siblings);
config.startShouldSetResponder.captured.parent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.childOne = {order: 1, returnVal: false};
config.startShouldSetResponder.bubbled.childOne = {order: 2, returnVal: true};
config.responderGrant.childOne = {order: 3};
config.responderStart.childOne = {order: 4};
run(config, siblings, startConfig(siblings.childOne, [siblings.childOne], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne);
// If the touch target is the sibling item, the negotiation should only
// propagate to first common ancestor of current responder and sibling (so
// the parent).
config = oneEventLoopTestConfig(siblings);
config.startShouldSetResponder.captured.parent = {order: 0, returnVal: false};
config.startShouldSetResponder.bubbled.parent = {order: 1, returnVal: false};
config.responderStart.childOne = {order: 2};
var touchConfig =
startConfig(siblings.childTwo, [siblings.childOne, siblings.childTwo], [1]);
run(config, siblings, touchConfig);
expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne);
// move childOne
config = oneEventLoopTestConfig(siblings);
config.moveShouldSetResponder.captured.parent = {order: 0, returnVal: false};
config.moveShouldSetResponder.bubbled.parent = {order: 1, returnVal: false};
config.responderMove.childOne = {order: 2};
run(config, siblings, moveConfig(siblings.childOne, [siblings.childOne, siblings.childTwo], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne);
// move childTwo: Only negotiates to `parent`.
config = oneEventLoopTestConfig(siblings);
config.moveShouldSetResponder.captured.parent = {order: 0, returnVal: false};
config.moveShouldSetResponder.bubbled.parent = {order: 1, returnVal: false};
config.responderMove.childOne = {order: 2};
run(config, siblings, moveConfig(siblings.childTwo, [siblings.childOne, siblings.childTwo], [1]));
expect(ResponderEventPlugin.getResponderID()).toBe(siblings.childOne);
});
it('should notify of being rejected. responderStart/Move happens on current responder', () => {
// Initially nothing wants to become the responder
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
config.responderGrant.child = {order: 4};
config.responderStart.child = {order: 5};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
// Suppose parent wants to become responder on move, and is rejected
config = oneEventLoopTestConfig(three);
config.moveShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.moveShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.moveShouldSetResponder.bubbled.parent = {order: 2, returnVal: true};
config.responderTerminationRequest.child = {order: 3, returnVal: false};
config.responderReject.parent = {order: 4};
// The start/move should occur on the original responder if new one is rejected
config.responderMove.child = {order: 5};
var touchConfig =
moveConfig(three.child, [three.child], [0]);
run(config, three, touchConfig);
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.bubbled.parent = {order: 2, returnVal: true};
config.responderTerminationRequest.child = {order: 3, returnVal: false};
config.responderReject.parent = {order: 4};
// The start/move should occur on the original responder if new one is rejected
config.responderStart.child = {order: 5};
touchConfig =
startConfig(three.child, [three.child, three.child], [1]);
run(config, three, touchConfig);
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
});
it('should negotiate scroll', () => {
// Initially nothing wants to become the responder
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
config.responderGrant.child = {order: 4};
config.responderStart.child = {order: 5};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
// If the touch target is the sibling item, the negotiation should only
// propagate to first common ancestor of current responder and sibling (so
// the parent).
config = oneEventLoopTestConfig(three);
config.scrollShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.scrollShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.scrollShouldSetResponder.bubbled.parent = {order: 2, returnVal: true};
config.responderTerminationRequest.child = {order: 3, returnVal: false};
config.responderReject.parent = {order: 4};
run(config, three, {
topLevelType: topLevelTypes.topScroll,
target: three.parent,
targetID: three.parent,
nativeEvent: {}
});
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
// Now lets let the scroll take control this time.
config = oneEventLoopTestConfig(three);
config.scrollShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.scrollShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.scrollShouldSetResponder.bubbled.parent = {order: 2, returnVal: true};
config.responderTerminationRequest.child = {order: 3, returnVal: true};
config.responderGrant.parent = {order: 4};
config.responderTerminate.child = {order: 5};
run(config, three, {
topLevelType: topLevelTypes.topScroll,
target: three.parent,
targetID: three.parent,
nativeEvent: {}
});
expect(ResponderEventPlugin.getResponderID()).toBe(three.parent);
});
it('should cancel correctly', () => {
// Initially our child becomes responder
var config = oneEventLoopTestConfig(three);
config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false};
config.startShouldSetResponder.captured.parent = {order: 1, returnVal: false};
config.startShouldSetResponder.captured.child = {order: 2, returnVal: false};
config.startShouldSetResponder.bubbled.child = {order: 3, returnVal: true};
config.responderGrant.child = {order: 4};
config.responderStart.child = {order: 5};
run(config, three, startConfig(three.child, [three.child], [0]));
expect(ResponderEventPlugin.getResponderID()).toBe(three.child);
config = oneEventLoopTestConfig(three);
config.responderEnd.child = {order: 0};
config.responderTerminate.child = {order: 1};
var nativeEvent = _touchConfig(
topLevelTypes.topTouchCancel,
three.child,
[three.child],
[0]
);
run(config, three, nativeEvent);
expect(ResponderEventPlugin.getResponderID()).toBe(null);
});
});