Skip to content

Commit b18dbd1

Browse files
authored
[Web] Fix nested touchables and gestures (#4108)
## Description 1. Adds `stopPropagation` call to event handlers in `Touchable` so only the inner-most one triggers the active animation. 2. Changes when buttons without `shouldActivateOnStart` (so `Touchable`) activate - instead of doing it on pressDown, now it happens during pressUp. This ensures that all other handlers have been extracted and can be canceled/can cancel `Touchable` correctly. 3. Behavior for buttons with `shouldActivateOnStart` (other buttons) shouldn't change after this PR, but the proper `shouldActivateOnStart` implementation is still to be done. ## Test plan Tested on the example from #4106
1 parent 43b28f4 commit b18dbd1

3 files changed

Lines changed: 52 additions & 31 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const LegacyRawButton = createNativeWrapper<LegacyRawButtonProps>(
2727
GestureHandlerButton as unknown as HostComponent<LegacyRawButtonProps>,
2828
{
2929
shouldCancelWhenOutside: false,
30-
shouldActivateOnStart: false,
30+
shouldActivateOnStart: Platform.OS === 'web',
3131
}
3232
);
3333

packages/react-native-gesture-handler/src/components/GestureHandlerButton.web.tsx

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import type { ColorValue, ViewProps } from 'react-native';
2+
import type { ColorValue, NativeSyntheticEvent, ViewProps } from 'react-native';
33
import { View } from 'react-native';
44

55
type ButtonProps = ViewProps & {
@@ -55,42 +55,60 @@ export const ButtonComponent = ({
5555
};
5656
}, []);
5757

58-
const pressIn = React.useCallback(() => {
59-
if (enabled) {
58+
const pressIn = React.useCallback(
59+
(event: NativeSyntheticEvent<unknown>) => {
60+
if (!enabled) {
61+
return;
62+
}
63+
64+
event.stopPropagation();
6065
if (pressOutTimer.current != null) {
6166
clearTimeout(pressOutTimer.current);
6267
pressOutTimer.current = null;
6368
}
6469
pressInTimestamp.current = performance.now();
6570
setCurrentDuration(pressAndHoldAnimationDuration);
6671
setPressed(true);
67-
}
68-
}, [enabled, pressAndHoldAnimationDuration]);
72+
},
73+
[enabled, pressAndHoldAnimationDuration]
74+
);
6975

70-
const pressOut = React.useCallback(() => {
71-
if (pressOutTimer.current != null) {
72-
clearTimeout(pressOutTimer.current);
73-
pressOutTimer.current = null;
74-
}
75-
const elapsed = performance.now() - pressInTimestamp.current;
76+
const pressOut = React.useCallback(
77+
(event: NativeSyntheticEvent<unknown>) => {
78+
// Only release if a press-in was actually recorded — guards against
79+
// stray pointer events and lets us complete the release cycle even if
80+
// `enabled` flipped to false between press-in and press-out.
81+
if (pressInTimestamp.current === 0) {
82+
return;
83+
}
7684

77-
if (elapsed >= pressAndHoldAnimationDuration) {
78-
setCurrentDuration(pressAndHoldAnimationDuration);
79-
setPressed(false);
80-
// elapsed * 2 to ensure there is at least half of the tapAnimationDuration left for the animation to play
81-
} else if (elapsed * 2 >= tapAnimationDuration) {
82-
setCurrentDuration(elapsed);
83-
setPressed(false);
84-
} else {
85-
// Let the in-progress CSS press-in transition continue; schedule press-out after remaining time
86-
const remaining = tapAnimationDuration - elapsed;
87-
pressOutTimer.current = setTimeout(() => {
85+
event.stopPropagation();
86+
if (pressOutTimer.current != null) {
87+
clearTimeout(pressOutTimer.current);
8888
pressOutTimer.current = null;
89-
setCurrentDuration(tapAnimationDuration);
89+
}
90+
const elapsed = performance.now() - pressInTimestamp.current;
91+
pressInTimestamp.current = 0;
92+
93+
if (elapsed >= pressAndHoldAnimationDuration) {
94+
setCurrentDuration(pressAndHoldAnimationDuration);
9095
setPressed(false);
91-
}, remaining);
92-
}
93-
}, [pressAndHoldAnimationDuration, tapAnimationDuration]);
96+
// elapsed * 2 to ensure there is at least half of the tapAnimationDuration left for the animation to play
97+
} else if (elapsed * 2 >= tapAnimationDuration) {
98+
setCurrentDuration(elapsed);
99+
setPressed(false);
100+
} else {
101+
// Let the in-progress CSS press-in transition continue; schedule press-out after remaining time
102+
const remaining = tapAnimationDuration - elapsed;
103+
pressOutTimer.current = setTimeout(() => {
104+
pressOutTimer.current = null;
105+
setCurrentDuration(tapAnimationDuration);
106+
setPressed(false);
107+
}, remaining);
108+
}
109+
},
110+
[pressAndHoldAnimationDuration, tapAnimationDuration]
111+
);
94112

95113
const currentUnderlayOpacity = pressed
96114
? activeUnderlayOpacity

packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import type IGestureHandler from './IGestureHandler';
1515
export default class NativeViewGestureHandler extends GestureHandler {
1616
private buttonRole!: boolean;
1717

18-
// TODO: Implement logic for activation on start
19-
// @ts-ignore Logic yet to be implemented
18+
// TODO: Implement logic for activation on start properly
2019
private shouldActivateOnStart = false;
2120
private disallowInterruption = false;
2221

@@ -72,7 +71,7 @@ export default class NativeViewGestureHandler extends GestureHandler {
7271
}
7372

7473
view.style['touchAction'] = 'auto';
75-
// @ts-ignore Turns on defualt touch behavior on Safari
74+
// @ts-ignore Turns on default touch behavior on Safari
7675
view.style['WebkitTouchCallout'] = 'auto';
7776
}
7877

@@ -102,7 +101,7 @@ export default class NativeViewGestureHandler extends GestureHandler {
102101
const view = this.delegate.view as HTMLElement;
103102
const isRNGHText = view.hasAttribute('rnghtext');
104103

105-
if (this.buttonRole || isRNGHText) {
104+
if ((this.buttonRole && this.shouldActivateOnStart) || isRNGHText) {
106105
this.activate();
107106
}
108107
}
@@ -144,6 +143,10 @@ export default class NativeViewGestureHandler extends GestureHandler {
144143
this.tracker.removeFromTracker(event.pointerId);
145144

146145
if (this.tracker.trackedPointersCount === 0) {
146+
if (this.buttonRole && this.state === State.BEGAN) {
147+
this.activate();
148+
}
149+
147150
if (this.state === State.ACTIVE) {
148151
this.end();
149152
} else {

0 commit comments

Comments
 (0)