diff --git a/src/browser/eventPlugins/ResponderEventPlugin.js b/src/browser/eventPlugins/ResponderEventPlugin.js deleted file mode 100644 index dd055e4e04..0000000000 --- a/src/browser/eventPlugins/ResponderEventPlugin.js +++ /dev/null @@ -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; diff --git a/src/browser/eventPlugins/__tests__/ResponderEventPlugin-test.js b/src/browser/eventPlugins/__tests__/ResponderEventPlugin-test.js deleted file mode 100644 index 0154122ed0..0000000000 --- a/src/browser/eventPlugins/__tests__/ResponderEventPlugin-test.js +++ /dev/null @@ -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); - }); - - -}); diff --git a/src/core/ReactInstanceHandles.js b/src/core/ReactInstanceHandles.js index 37a96e251a..f8a30ec7a8 100644 --- a/src/core/ReactInstanceHandles.js +++ b/src/core/ReactInstanceHandles.js @@ -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. diff --git a/src/core/__tests__/ReactInstanceHandles-test.js b/src/core/__tests__/ReactInstanceHandles-test.js index 2ecf5edcaf..58a50cfc8e 100644 --- a/src/core/__tests__/ReactInstanceHandles-test.js +++ b/src/core/__tests__/ReactInstanceHandles-test.js @@ -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) ); diff --git a/src/event/EventPluginUtils.js b/src/event/EventPluginUtils.js index b04883b15f..3b5fc634ec 100644 --- a/src/event/EventPluginUtils.js +++ b/src/event/EventPluginUtils.js @@ -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 }; diff --git a/src/event/EventPropagators.js b/src/event/EventPropagators.js index 79dd2adb91..257266ab0e 100644 --- a/src/event/EventPropagators.js +++ b/src/event/EventPropagators.js @@ -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 }; diff --git a/src/event/eventPlugins/ResponderEventPlugin.js b/src/event/eventPlugins/ResponderEventPlugin.js new file mode 100644 index 0000000000..3dcaa5d5d2 --- /dev/null +++ b/src/event/eventPlugins/ResponderEventPlugin.js @@ -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; diff --git a/src/event/eventPlugins/ResponderSyntheticEvent.js b/src/event/eventPlugins/ResponderSyntheticEvent.js new file mode 100644 index 0000000000..6a8b0260ad --- /dev/null +++ b/src/event/eventPlugins/ResponderSyntheticEvent.js @@ -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; diff --git a/src/event/eventPlugins/ResponderTouchHistoryStore.js b/src/event/eventPlugins/ResponderTouchHistoryStore.js new file mode 100644 index 0000000000..00f99255db --- /dev/null +++ b/src/event/eventPlugins/ResponderTouchHistoryStore.js @@ -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; diff --git a/src/event/eventPlugins/__tests__/ResponderEventPlugin-test.js b/src/event/eventPlugins/__tests__/ResponderEventPlugin-test.js new file mode 100644 index 0000000000..57ffd5d323 --- /dev/null +++ b/src/event/eventPlugins/__tests__/ResponderEventPlugin-test.js @@ -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} touches All active touches. + * @param {Array} 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} 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} 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); + }); +});