Merge pull request #3555 from spicyj/native-overrides
Import ResponderEventPlugin from react-native
This commit is contained in:
commit
dea7efbe16
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
||||
});
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue