diff --git a/packages/react-native/Libraries/Pressability/Pressability.js b/packages/react-native/Libraries/Pressability/Pressability.js index 0be465bec546..ba7d858be895 100644 --- a/packages/react-native/Libraries/Pressability/Pressability.js +++ b/packages/react-native/Libraries/Pressability/Pressability.js @@ -385,6 +385,8 @@ export default class Pressability { top: number, }> = null; _touchActivatePosition: ?Readonly<{ + locationX: number, + locationY: number, pageX: number, pageY: number, }>; @@ -721,6 +723,7 @@ export default class Pressability { !isActivationSignal(prevState) && isActivationSignal(nextState); if (isInitialTransition || isActivationTransition) { + this._recordTouchActivatePosition(event); this._measureResponderRegion(); } @@ -764,14 +767,18 @@ export default class Pressability { _activate(event: GestureResponderEvent): void { const {onPressIn} = this._config; - const {pageX, pageY} = getTouchFromPressEvent(event); - this._touchActivatePosition = {pageX, pageY}; + this._recordTouchActivatePosition(event); this._touchActivateTime = Date.now(); if (onPressIn != null) { onPressIn(event); } } + _recordTouchActivatePosition(event: GestureResponderEvent): void { + const {locationX, locationY, pageX, pageY} = getTouchFromPressEvent(event); + this._touchActivatePosition = {locationX, locationY, pageX, pageY}; + } + _deactivate(event: GestureResponderEvent): void { const {onPressOut} = this._config; if (onPressOut != null) { @@ -820,16 +827,42 @@ export default class Pressability { if (!left && !top && !width && !height && !pageX && !pageY) { return; } - this._responderRegion = { + const responderRegion = { bottom: pageY + height, left: pageX, right: pageX + width, top: pageY, }; + + const touch = this._touchActivatePosition; + if ( + touch != null && + !this._isTouchWithinResponderRegion(touch, responderRegion) + ) { + const correctedLeft = touch.pageX - touch.locationX; + const correctedTop = touch.pageY - touch.locationY; + const correctedRegion = { + bottom: correctedTop + height, + left: correctedLeft, + right: correctedLeft + width, + top: correctedTop, + }; + + if (this._isTouchWithinResponderRegion(touch, correctedRegion)) { + this._responderRegion = correctedRegion; + return; + } + } + + this._responderRegion = responderRegion; }; _isTouchWithinResponderRegion( - touch: GestureResponderEvent['nativeEvent'], + touch: Readonly<{ + pageX: number, + pageY: number, + ... + }>, responderRegion: Readonly<{ bottom: number, left: number, diff --git a/packages/react-native/Libraries/Pressability/__tests__/Pressability-test.js b/packages/react-native/Libraries/Pressability/__tests__/Pressability-test.js index 05a46d31e9bd..1838e4cf0728 100644 --- a/packages/react-native/Libraries/Pressability/__tests__/Pressability-test.js +++ b/packages/react-native/Libraries/Pressability/__tests__/Pressability-test.js @@ -171,11 +171,15 @@ const createMockPressEvent = ( registrationName: string, pageX: number, pageY: number, + locationX?: number, + locationY?: number, }>, ): GestureResponderEvent => { let registrationName = ''; let pageX = 0; let pageY = 0; + let locationX = 0; + let locationY = 0; if (typeof nameOrOverrides === 'string') { registrationName = nameOrOverrides; @@ -183,14 +187,19 @@ const createMockPressEvent = ( registrationName = nameOrOverrides.registrationName; pageX = nameOrOverrides.pageX; pageY = nameOrOverrides.pageY; + locationX = nameOrOverrides.locationX ?? pageX; + locationY = nameOrOverrides.locationY ?? pageY; + } else { + locationX = pageX; + locationY = pageY; } const nativeEvent = { changedTouches: [] as Array, force: 1, identifier: 42, - locationX: pageX, - locationY: pageY, + locationX, + locationY, pageX, pageY, target: 42, @@ -980,6 +989,42 @@ describe('Pressability', () => { jest.advanceTimersByTime(630); // 1000 - 500 (onPressIn activation, already advanced before) + DEFAULT_MIN_PRESS_DURATION expect(config.onPressOut).toBeCalled(); }); + + it('uses touch location when measured region is shifted from visual position', () => { + getMock(UIManager.measure).mockImplementation((id, fn) => { + fn(0, 500, mockRegion.width, mockRegion.height, 0, 500); + }); + const {config, handlers} = createMockPressability({ + delayPressIn: 0, + }); + + handlers.onStartShouldSetResponder(); + handlers.onResponderGrant( + createMockPressEvent({ + registrationName: 'onResponderGrant', + pageX: 25, + pageY: 25, + locationX: 25, + locationY: 25, + }), + ); + + expect(UIManager.measure).toBeCalled(); + + handlers.onResponderMove( + createMockPressEvent({ + registrationName: 'onResponderMove', + pageX: 25, + pageY: 25, + locationX: 25, + locationY: 25, + }), + ); + handlers.onResponderRelease(createMockPressEvent('onResponderRelease')); + + expect(config.onPressIn).toBeCalled(); + expect(config.onPress).toBeCalled(); + }); }); });