Skip to content

Commit dec475f

Browse files
authored
[iOS] Activate buttons on pointer up (#4129)
## Description Changes the moment the `ACTIVE` event is dispatched on iOS from pointer down to pointer up, aligning it with Android and Web implementations. Updates existing checks to account for this change. Updates the v3 button implementation to do cleanup in `onFinalize` instead of `onDeactivate` - setup happens in `onBegin`, so there's no guarantee that `onDeactivate` will trigger. ## Test plan Tested existing examples, including legacy ones
1 parent 358b57c commit dec475f

5 files changed

Lines changed: 23 additions & 29 deletions

File tree

packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -253,11 +253,6 @@ - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event
253253
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
254254
withNumberOfTouches:event.allTouches.count
255255
withPointerType:_pointerType]];
256-
257-
[self sendActiveStateEventIfChangedForView:sender
258-
extraData:[RNGestureHandlerEventExtraData forPointerInside:YES
259-
withNumberOfTouches:event.allTouches.count
260-
withPointerType:_pointerType]];
261256
}
262257

263258
- (void)handleTouchUpOutside:(UIView *)sender forEvent:(UIEvent *)event
@@ -275,11 +270,12 @@ - (void)handleTouchUpOutside:(UIView *)sender forEvent:(UIEvent *)event
275270

276271
- (void)handleTouchUpInside:(UIView *)sender forEvent:(UIEvent *)event
277272
{
278-
[self sendEventsInState:RNGestureHandlerStateEnd
279-
forViewWithTag:sender.reactTag
280-
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
281-
withNumberOfTouches:event.allTouches.count
282-
withPointerType:_pointerType]];
273+
RNGestureHandlerEventExtraData *extraData = [RNGestureHandlerEventExtraData forPointerInside:YES
274+
withNumberOfTouches:event.allTouches.count
275+
withPointerType:_pointerType];
276+
277+
[self sendActiveStateEventIfChangedForView:sender extraData:extraData];
278+
[self sendEventsInState:RNGestureHandlerStateEnd forViewWithTag:sender.reactTag withExtraData:extraData];
283279
}
284280

285281
- (void)handleDragExit:(UIView *)sender forEvent:(UIEvent *)event

packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ describe('[API v3] Components', () => {
134134
>;
135135
const { jsEventHandler } = gesture.detectorCallbacks;
136136

137-
// Fire BEGAN
137+
// Fire BEGAN — long press timer starts here
138138
act(() => {
139139
jsEventHandler?.({
140140
oldState: State.UNDETERMINED,
@@ -145,7 +145,7 @@ describe('[API v3] Components', () => {
145145
});
146146
});
147147

148-
// Fire ACTIVE — long press timer starts here (on iOS / non-Android)
148+
// Fire ACTIVE
149149
act(() => {
150150
jsEventHandler?.({
151151
oldState: State.BEGAN,

packages/react-native-gesture-handler/src/components/GestureButtons.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,37 +36,38 @@ class InnerBaseButton extends React.Component<BaseButtonWithRefProps> {
3636
delayLongPress: 600,
3737
};
3838

39-
private lastActive: boolean;
39+
private lastIsPressed: boolean;
4040
private longPressTimeout: ReturnType<typeof setTimeout> | undefined;
4141
private longPressDetected: boolean;
4242

4343
constructor(props: BaseButtonWithRefProps) {
4444
super(props);
45-
this.lastActive = false;
45+
this.lastIsPressed = false;
4646
this.longPressDetected = false;
4747
}
4848

4949
private handleEvent = ({
5050
nativeEvent,
5151
}: HandlerStateChangeEvent<NativeViewGestureHandlerPayload>) => {
5252
const { state, oldState, pointerInside } = nativeEvent;
53-
const active = pointerInside && state === State.ACTIVE;
53+
const isPressed =
54+
pointerInside && (state === State.BEGAN || state === State.ACTIVE);
5455

55-
if (active !== this.lastActive && this.props.onActiveStateChange) {
56-
this.props.onActiveStateChange(active);
56+
if (isPressed !== this.lastIsPressed && this.props.onActiveStateChange) {
57+
this.props.onActiveStateChange(isPressed);
5758
}
5859

5960
if (
6061
!this.longPressDetected &&
6162
oldState === State.ACTIVE &&
6263
state !== State.CANCELLED &&
63-
this.lastActive &&
64+
this.lastIsPressed &&
6465
this.props.onPress
6566
) {
6667
this.props.onPress(pointerInside);
6768
}
6869

69-
if (!this.lastActive && state === State.BEGAN && pointerInside) {
70+
if (!this.lastIsPressed && state === State.BEGAN && pointerInside) {
7071
this.longPressDetected = false;
7172
if (this.props.onLongPress) {
7273
this.longPressTimeout = setTimeout(
@@ -93,7 +94,7 @@ class InnerBaseButton extends React.Component<BaseButtonWithRefProps> {
9394
this.longPressTimeout = undefined;
9495
}
9596

96-
this.lastActive = active;
97+
this.lastIsPressed = isPressed;
9798
};
9899

99100
private onLongPress = () => {

packages/react-native-gesture-handler/src/components/touchables/GenericTouchable.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { Component } from 'react';
3-
import { Animated, Platform } from 'react-native';
3+
import { Animated } from 'react-native';
44

55
import type {
66
GestureEvent,
@@ -174,10 +174,7 @@ export default class GenericTouchable extends Component<
174174
// Need to handle case with external cancellation (e.g. by ScrollView)
175175
this.moveToState(TOUCHABLE_STATE.UNDETERMINED);
176176
} else if (
177-
// This platform check is an implication of slightly different behavior of handlers on different platform.
178-
// And Android "Active" state is achieving on first move of a finger, not on press in.
179-
// On iOS event on "Began" is not delivered.
180-
state === (Platform.OS !== 'android' ? State.ACTIVE : State.BEGAN) &&
177+
state === State.BEGAN &&
181178
this.STATE === TOUCHABLE_STATE.UNDETERMINED
182179
) {
183180
// Moving inside requires

packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,16 @@ export const BaseButton = (props: BaseButtonProps) => {
6969
};
7070

7171
const onDeactivate = (e: EndCallbackEventType) => {
72+
props.onDeactivate?.(e);
73+
};
74+
75+
const onFinalize = (e: EndCallbackEventType) => {
7276
onActiveStateChange?.(false);
7377

7478
if (!e.canceled && !longPressDetected.current) {
7579
onPress?.(e.pointerInside);
7680
}
7781

78-
props.onDeactivate?.(e);
79-
};
80-
81-
const onFinalize = (e: EndCallbackEventType) => {
8282
if (longPressTimeout.current !== undefined) {
8383
clearTimeout(longPressTimeout.current);
8484
longPressTimeout.current = undefined;

0 commit comments

Comments
 (0)