Open History to see the last commit
1diff --git a/apple/Handlers/RNPanHandler.m b/apple/Handlers/RNPanHandler.m
2index cb0666b9e35dc2b76ff0cb592e0a867e1a28b3dd..7f481d90f9993dbe874676c7393f90e266bffcbf 100644
3--- a/apple/Handlers/RNPanHandler.m
4+++ b/apple/Handlers/RNPanHandler.m
5@@ -31,6 +31,7 @@ @interface RNBetterPanGestureRecognizer : UIPanGestureRecognizer
6 @property (nonatomic) CGFloat failOffsetYStart;
7 @property (nonatomic) CGFloat failOffsetYEnd;
8 @property (nonatomic) CGFloat activateAfterLongPress;
9+@property (nonatomic) BOOL handlingTouchStart;
11 #if !TARGET_OS_OSX && !TARGET_OS_TV
12 @property (atomic, readonly, strong) RNGHStylusData *stylusData;
13@@ -133,9 +134,14 @@ - (void)activateAfterLongPress
15 - (void)interactionsBegan:(NSSet *)touches withEvent:(UIEvent *)event
16 {
17- if (self.state == UIGestureRecognizerStatePossible && ![self.delegate gestureRecognizerShouldBegin:self]) {
18- self.state = UIGestureRecognizerStateFailed;
19- return;
20+ if (self.state == UIGestureRecognizerStatePossible) {
21+ self.handlingTouchStart = touches.count > 0;
22+ BOOL shouldBegin = [self.delegate gestureRecognizerShouldBegin:self];
23+ self.handlingTouchStart = NO;
24+ if (!shouldBegin) {
25+ self.state = UIGestureRecognizerStateFailed;
26+ return;
27+ }
28 }
30 if (touches.count == 0) {
31@@ -501,6 +507,23 @@ - (void)updateConfig:(NSDictionary *)config
33 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
34 {
35+#if !TARGET_OS_OSX && !TARGET_OS_TV
36+ RNBetterPanGestureRecognizer *recognizer = (RNBetterPanGestureRecognizer *)gestureRecognizer;
37+ if (!recognizer.handlingTouchStart && recognizer.allowedScrollTypesMask != 0 && recognizer.numberOfTouches == 0) {
38+ // Trackpad scroll events can begin without touchesMoved enforcing failure
39+ // offsets. Reject vertical scrolling before a horizontal row pan cancels
40+ // the enclosing UIScrollView. The manual touch-start preflight above has
41+ // no current translation yet, so leave that path to the touch callbacks.
42+ CGPoint translation = [recognizer translationInView:recognizer.view.window];
43+ BOOL horizontalOnly = (!isnan(recognizer.activeOffsetXStart) || !isnan(recognizer.activeOffsetXEnd)) &&
44+ isnan(recognizer.activeOffsetYStart) && isnan(recognizer.activeOffsetYEnd);
45+ if ([recognizer shouldFailUnderCustomCriteria] ||
46+ (horizontalOnly && fabs(translation.y) > fabs(translation.x))) {
47+ return NO;
48+ }
49+ }
50+#endif
51+
52 RNGestureHandlerState savedState = _lastState;
53 BOOL shouldBegin = [super gestureRecognizerShouldBegin:gestureRecognizer];
54 _lastState = savedState;
55diff --git a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js
56index 30e6601d1dd2dbb8fa43b077feee374e0af4e509..5e6e99cee482c78aac71fe5ed1dc19e4ccf05ae4 100644
57--- a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js
58+++ b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js
59@@ -42,6 +42,8 @@ const Swipeable = props => {
60 enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE,
61 dragOffsetFromLeft = DEFAULT_DRAG_OFFSET,
62 dragOffsetFromRight = -DEFAULT_DRAG_OFFSET,
63+ failOffsetY,
64+ onSwipeableRelease,
65 friction = DEFAULT_FRICTION,
66 overshootFriction = DEFAULT_OVERSHOOT_FRICTION,
67 onSwipeableOpenStartDrag: onSwipeableOpenStartDragProp,
68@@ -284,6 +286,10 @@ const Swipeable = props => {
69 if (event.canceled) {
70 // Restore the row to where it rested before the gesture.
71 const restingValue = rowState.value === 1 ? leftWidth.value : rowState.value === -1 ? -rightWidth.value : 0;
72+ // A caller animating translation (e.g. a dismissal) keeps it.
73+ if (onSwipeableRelease?.(appliedTranslation, true)) {
74+ return;
75+ }
76 animateRow(restingValue, 0, true);
77 return;
78 }
79@@ -312,8 +318,12 @@ const Swipeable = props => {
80 toValue = -rightWidth.value;
81 }
82 }
83+ // Let a UI-thread full-swipe commit take over before the return spring.
84+ if (onSwipeableRelease?.(appliedTranslation, false)) {
85+ return;
86+ }
87 animateRow(toValue, velocityX / friction);
88- }, [animateRow, friction, leftThreshold, leftWidth, rightThreshold, rightWidth, rowState, userDrag]);
89+ }, [animateRow, friction, leftThreshold, leftWidth, rightThreshold, rightWidth, rowState, userDrag, onSwipeableRelease, appliedTranslation]);
90 const close = useCallback(() => {
91 'worklet';
93@@ -359,6 +369,8 @@ const Swipeable = props => {
94 enabled: enabled ?? true,
95 enableTrackpadTwoFingerGesture: enableTrackpadTwoFingerGesture,
96 activeOffsetX: [dragOffsetFromRight, dragOffsetFromLeft],
97+ failOffsetY,
98+ failOffsetY,
99 simultaneousWith,
100 requireToFail,
101 block,
102diff --git a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts
103index e97b6dbbced1bcbd359cf35293b7f10cbf13b0e7..b7d03718c4ce50a0a69f0f478ae5114d9310d9f3 100644
104--- a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts
105+++ b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts
106@@ -59,6 +59,13 @@ export type SwipeableProps = {
107 * Called when action panel is closed.
108 */
109 onSwipeableClose?: (direction: SwipeDirection.LEFT | SwipeDirection.RIGHT) => void;
110+ /**
111+ * UI-thread worklet called on release before the built-in spring starts,
112+ * including a canceled pan (`canceled`), which must not commit an action.
113+ * Return true to consume the release and take ownership of translation.
114+ * The caller must reset or remove the row after its custom animation.
115+ */
116+ onSwipeableRelease?: (translation: SharedValue<number>, canceled: boolean) => boolean;
117 /**
118 * Called when action panel starts animating on open (either right or left).
119 */
120@@ -150,6 +157,13 @@ export type SwipeableProps = {
121 * The horizontal offset from the starting point required to trigger a left-swipe gesture. Defaults to -10.
122 */
123 dragOffsetFromRight?: number;
124+ /**
125+ * Range along the Y axis (in points) that, once exceeded before the swipe
126+ * activates, makes the pan fail. Lets vertically-dominant pans (list
127+ * scrolling, especially trackpad two-finger pans) skip the swipe entirely.
128+ * Passed straight to the underlying Pan gesture's `failOffsetY`.
129+ */
130+ failOffsetY?: number | [start: number, end: number];
131 }>;
132 export interface SwipeableMethods {
133 close: () => void;
134diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx
135index 2b3789a5277404e57607c05db96dc062d6db1b10..0c23c2ce6b0a07540c68204a8528b655b5624f84 100644
136--- a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx
137+++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx
138@@ -80,6 +80,8 @@ const Swipeable = (props: SwipeableProps) => {
139 enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE,
140 dragOffsetFromLeft = DEFAULT_DRAG_OFFSET,
141 dragOffsetFromRight = -DEFAULT_DRAG_OFFSET,
142+ failOffsetY,
143+ onSwipeableRelease,
144 friction = DEFAULT_FRICTION,
145 overshootFriction = DEFAULT_OVERSHOOT_FRICTION,
146 onSwipeableOpenStartDrag: onSwipeableOpenStartDragProp,
147@@ -520,6 +522,10 @@ const Swipeable = (props: SwipeableProps) => {
148 : rowState.value === -1
149 ? -rightWidth.value
150 : 0;
151+ // A caller animating translation (e.g. a dismissal) keeps it.
152+ if (onSwipeableRelease?.(appliedTranslation, true)) {
153+ return;
154+ }
155 animateRow(restingValue, 0, true);
156 return;
157 }
158@@ -552,6 +558,11 @@ const Swipeable = (props: SwipeableProps) => {
159 }
160 }
162+ // Let a UI-thread full-swipe commit take over before the return spring.
163+ if (onSwipeableRelease?.(appliedTranslation, false)) {
164+ return;
165+ }
166+
167 animateRow(toValue, velocityX / friction);
168 },
169 [
170@@ -563,6 +574,8 @@ const Swipeable = (props: SwipeableProps) => {
171 rightWidth,
172 rowState,
173 userDrag,
174+ onSwipeableRelease,
175+ appliedTranslation,
176 ]
177 );
179@@ -640,6 +653,7 @@ const Swipeable = (props: SwipeableProps) => {
180 typeof dragOffsetFromRight,
181 typeof dragOffsetFromLeft,
182 ],
183+ failOffsetY,
184 simultaneousWith,
185 requireToFail,
186 block,
187@@ -653,6 +667,7 @@ const Swipeable = (props: SwipeableProps) => {
188 block,
189 dragOffsetFromLeft,
190 dragOffsetFromRight,
191+ failOffsetY,
192 enableTrackpadTwoFingerGesture,
193 enabled,
194 handleFinalize,
195diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts
196index 1e1246f709feb04c01cf3221472cc1219a8fffac..9099c53aa5e2ff164beeb0f43897fd56eef663fa 100644
197--- a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts
198+++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts
199@@ -77,6 +77,14 @@ export type SwipeableProps = {
200 direction: SwipeDirection.LEFT | SwipeDirection.RIGHT
201 ) => void;
203+ /**
204+ * UI-thread worklet called on release before the built-in spring starts,
205+ * including a canceled pan (`canceled`), which must not commit an action.
206+ * Return true to consume the release and take ownership of translation.
207+ * The caller must reset or remove the row after its custom animation.
208+ */
209+ onSwipeableRelease?: (translation: SharedValue<number>, canceled: boolean) => boolean;
210+
211 /**
212 * Called when action panel starts animating on open (either right or left).
213 */
214@@ -196,6 +204,14 @@ export type SwipeableProps = {
215 * The horizontal offset from the starting point required to trigger a left-swipe gesture. Defaults to -10.
216 */
217 dragOffsetFromRight?: number;
218+
219+ /**
220+ * Range along the Y axis (in points) that, once exceeded before the swipe
221+ * activates, makes the pan fail. Lets vertically-dominant pans (list
222+ * scrolling, especially trackpad two-finger pans) skip the swipe entirely.
223+ * Passed straight to the underlying Pan gesture's `failOffsetY`.
224+ */
225+ failOffsetY?: number | [start: number, end: number];
226 }>;
228 export interface SwipeableMethods {