Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/react-native/Libraries/Pressability/Pressability.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ export default class Pressability {
pageX: number,
pageY: number,
}>;
_touchGrantPosition: ?Readonly<{
pageX: number,
pageY: number,
}>;
_touchActivateTime: ?number;
_touchState: TouchState = 'NOT_RESPONDER';

Expand Down Expand Up @@ -710,6 +714,7 @@ export default class Pressability {
): void {
if (isTerminalSignal(signal)) {
this._touchActivatePosition = null;
this._touchGrantPosition = null;
this._cancelLongPressDelayTimeout();
}

Expand All @@ -720,6 +725,12 @@ export default class Pressability {
const isActivationTransition =
!isActivationSignal(prevState) && isActivationSignal(nextState);

if (isInitialTransition) {
const touch = getTouchFromPressEvent(event);
this._touchGrantPosition =
touch == null ? null : {pageX: touch.pageX, pageY: touch.pageY};
}

if (isInitialTransition || isActivationTransition) {
this._measureResponderRegion();
}
Expand Down Expand Up @@ -826,6 +837,23 @@ export default class Pressability {
right: pageX + width,
top: pageY,
};

// The responder was granted by a native hit test, so the touch that
// granted it must lie inside the view's true on-screen region. If the
// measured region excludes that touch, the measurement is stale — e.g. the
// view was repositioned by native code (sheet footers, modals) after
// layout, and `measure` still reports the pre-move frame. Using such a
// region would cancel the press on the first touch move, so discard it.
const grantPosition = this._touchGrantPosition;
if (
grantPosition != null &&
!this._isTouchWithinResponderRegion(
grantPosition as $FlowFixMe,
this._responderRegion,
)
) {
this._responderRegion = null;
}
};

_isTouchWithinResponderRegion(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,70 @@ describe('Pressability', () => {
jest.advanceTimersByTime(CONFIGURED_DEFAULT_MIN_PRESS_DURATION);
expect(config.onPressOut).toBeCalled();
});

it('is called when the measured region excludes the grant touch', () => {
// A view repositioned by native code after layout (e.g. a native sheet
// footer) measures at its stale layout position, far from where the
// press actually landed. Such a measurement must not be used to cancel
// the press on the first touch move.
mockUIManagerMeasure();
const {config, handlers} = createMockPressability();

handlers.onStartShouldSetResponder();
handlers.onResponderGrant(
createMockPressEvent({
registrationName: 'onResponderGrant',
pageX: 276,
pageY: 467,
}),
);
handlers.onResponderMove(
createMockPressEvent({
registrationName: 'onResponderMove',
pageX: 276,
pageY: 467,
}),
);
handlers.onResponderRelease(
createMockPressEvent({
registrationName: 'onResponderRelease',
pageX: 276,
pageY: 467,
}),
);

expect(config.onPress).toBeCalled();
});

it('is not called when the touch leaves a region containing the grant touch', () => {
mockUIManagerMeasure();
const {config, handlers} = createMockPressability();

handlers.onStartShouldSetResponder();
handlers.onResponderGrant(
createMockPressEvent({
registrationName: 'onResponderGrant',
pageX: 10,
pageY: 10,
}),
);
handlers.onResponderMove(
createMockPressEvent({
registrationName: 'onResponderMove',
pageX: 500,
pageY: 500,
}),
);
handlers.onResponderRelease(
createMockPressEvent({
registrationName: 'onResponderRelease',
pageX: 500,
pageY: 500,
}),
);

expect(config.onPress).not.toBeCalled();
});
});

describe('onPressIn', () => {
Expand Down