From 3233d0ffe99a568de3b76272926c7083e083f83e Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Mon, 10 Aug 2020 13:26:13 -0700 Subject: [PATCH] [fix] Responder negotiation between siblings There should be responder negotiation between siblings if there is no common ancestor connected to the responder system. Instead the current responder should continue to receive events. This was only occuring for mouse events during mousemove, as the target can change during the course of the movement. --- .../useResponderEvents/ResponderSystem.js | 35 +++--- .../__tests__/index-test.js | 100 ++++++++++++++++++ 2 files changed, 121 insertions(+), 14 deletions(-) diff --git a/packages/react-native-web/src/hooks/useResponderEvents/ResponderSystem.js b/packages/react-native-web/src/hooks/useResponderEvents/ResponderSystem.js index 7493f299..a61d0449 100644 --- a/packages/react-native-web/src/hooks/useResponderEvents/ResponderSystem.js +++ b/packages/react-native-web/src/hooks/useResponderEvents/ResponderSystem.js @@ -329,21 +329,28 @@ function eventListener(domEvent: any) { if (currentResponderIdPath != null && eventIdPath != null) { const lowestCommonAncestor = getLowestCommonAncestor(currentResponderIdPath, eventIdPath); - const indexOfLowestCommonAncestor = eventIdPath.indexOf(lowestCommonAncestor); - // Skip the current responder so it doesn't receive unexpected "shouldSet" events. - const index = - indexOfLowestCommonAncestor + (lowestCommonAncestor === currentResponder.id ? 1 : 0); - eventPaths = { - idPath: eventIdPath.slice(index), - nodePath: eventPaths.nodePath.slice(index) - }; + if (lowestCommonAncestor != null) { + const indexOfLowestCommonAncestor = eventIdPath.indexOf(lowestCommonAncestor); + // Skip the current responder so it doesn't receive unexpected "shouldSet" events. + const index = + indexOfLowestCommonAncestor + (lowestCommonAncestor === currentResponder.id ? 1 : 0); + eventPaths = { + idPath: eventIdPath.slice(index), + nodePath: eventPaths.nodePath.slice(index) + }; + } else { + eventPaths = null; + } } - // If a node wants to become the responder, attempt to transfer. - wantsResponder = findWantsResponder(eventPaths, domEvent, responderEvent); - if (wantsResponder != null) { - // Sets responder if none exists, or negotates with existing responder. - attemptTransfer(responderEvent, wantsResponder); - wasNegotiated = true; + + if (eventPaths != null) { + // If a node wants to become the responder, attempt to transfer. + wantsResponder = findWantsResponder(eventPaths, domEvent, responderEvent); + if (wantsResponder != null) { + // Sets responder if none exists, or negotates with existing responder. + attemptTransfer(responderEvent, wantsResponder); + wasNegotiated = true; + } } } diff --git a/packages/react-native-web/src/hooks/useResponderEvents/__tests__/index-test.js b/packages/react-native-web/src/hooks/useResponderEvents/__tests__/index-test.js index 9c29f45d..ebc9afac 100644 --- a/packages/react-native-web/src/hooks/useResponderEvents/__tests__/index-test.js +++ b/packages/react-native-web/src/hooks/useResponderEvents/__tests__/index-test.js @@ -2154,6 +2154,106 @@ describe('useResponderEvents', () => { ]); }); + /** + * If siblings are connected to the responder system but have no ancestors + * connected, there should be no negotiation between siblings after one + * becomes the active responder. + */ + test('no negotation between siblings with no responder ancestors', () => { + const pointerType = 'mouse'; + const eventLog = []; + + const targetConfig = { + onStartShouldSetResponderCapture(e) { + eventLog.push('target: onStartShouldSetResponderCapture'); + return false; + }, + onStartShouldSetResponder(e) { + eventLog.push('target: onStartShouldSetResponder'); + return true; + }, + onMoveShouldSetResponderCapture(e) { + eventLog.push('target: onMoveShouldSetResponderCapture'); + return false; + }, + onMoveShouldSetResponder(e) { + eventLog.push('target: onMoveShouldSetResponder'); + return false; + }, + onResponderGrant(e) { + eventLog.push('target: onResponderGrant'); + }, + onResponderStart(e) { + eventLog.push('target: onResponderStart'); + }, + onResponderMove(e) { + eventLog.push('target: onResponderMove'); + } + }; + const siblingConfig = { + onStartShouldSetResponderCapture(e) { + eventLog.push('sibling: onStartShouldSetResponderCapture'); + return true; + }, + onStartShouldSetResponder(e) { + eventLog.push('sibling: onStartShouldSetResponder'); + return true; + }, + onMoveShouldSetResponderCapture(e) { + eventLog.push('sibling: onMoveShouldSetResponderCapture'); + return true; + }, + onMoveShouldSetResponder(e) { + eventLog.push('sibling: onMoveShouldSetResponder'); + return true; + }, + onResponderGrant(e) { + eventLog.push('sibling: onResponderGrant'); + }, + onResponderStart(e) { + eventLog.push('sibling: onResponderStart'); + }, + onResponderMove(e) { + eventLog.push('sibling: onResponderMove'); + } + }; + + const Component = () => { + useResponderEvents(targetRef, targetConfig); + useResponderEvents(siblingRef, siblingConfig); + return ( +
+
+
+
+
+
+ ); + }; + + // render + act(() => { + render(); + }); + const target = createEventTarget(targetRef.current); + const sibling = createEventTarget(siblingRef.current); + // gesture start and move on target + act(() => { + target.pointerdown({ pointerType }); + target.pointermove({ pointerType }); + sibling.pointermove({ pointerType }); + }); + // target remains responder, no negotation occurs + expect(eventLog).toEqual([ + 'target: onStartShouldSetResponderCapture', + 'target: onStartShouldSetResponder', + 'target: onResponderGrant', + 'target: onResponderStart', + 'target: onResponderMove', + 'target: onResponderMove' + ]); + }); + /** * If a node is responder and it rejects a termination request, it * should continue to receive responder events.