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.