Skip to content

Commit b06b3ef

Browse files
m-bertclaude
andauthored
[Release] 2.33.1 Cherry-pick thread (#4597)
## Description Cherry pick thread for release 2.33.1 ## List of PRs | PR | Original commit | Picked commit | Without conflict | Notes | |:----------:|:-------------:|:------:|:--------:|:------:| | [[iOS] Skip hidden and transparent buttons in hit testing](#4512) | [162c21d](162c21d) | [720a9b3](720a9b3) | ✅ | | | [[Android] Align the alpha traversal gate with iOS](#4513) | [a2e0430](a2e0430) | [aad1f45](aad1f45) | ❌ | `RNGestureHandlerRootHelper` hunk only; dropped the v3 detector box-none hunk. Behavior change: Android alpha gate lowered from 0.1 to 0.01 | | [[iOS] Fix `manualActivation` being ignored after a config update mid-gesture](#4520) | [186eb6b](186eb6b) | [96f5112](96f5112) | ❌ | `syncManualActivationRecognizer` called at the end of 2.x `configure:` (no `setConfig:` / `updateConfig:` split); dropped `testID` reset | | [[Web] Introduce key pointer in KeyboardEventManager](#4519) | [5d1c171](5d1c171) | [f0004f0](f0004f0) | ✅ | | | [Scan mount relations directly instead of rebuilding tag arrays](#4546) | [590518c](590518c) | [774b459](774b459) | ❌ | Import-order conflict only; dropped absent test file | | [[Web] Fix view relative coordinates of trackpad pan events](#4531) | [bb62b06](bb62b06) | [7adbee7](7adbee7) | ❌ | `getBoundingClientRect` in place of v3 `getEffectiveBoundingRect`; dropped absent test file | | [[Web] Register the wheel listener as passive and only for Pan](#4583) | [117c98a](117c98a) | [05e06ca](05e06ca) | ❌ | `instanceof PanGestureHandler` in place of `handler.name` (absent on 2.x); dropped `ScrollEventManager` context and absent test file | | [[Android] Fix NPE when a touch cancel callback finishes the handler mid-transition](#4584) | [52a9c06](52a9c06) | [e53f926](e53f926) | ✅ | | | [[General] Link late-mounted gesture objects and skip mount reactions without external relations](#4562) | [34efb3d](34efb3d) | [c424f80](c424f80) | ❌ | Import conflicts only; dropped absent test file | | [fix(android): respect pointer events on gesture root views](#4565) | [d30f463](d30f463) | [c3b7245](c3b7245) | ❌ | Dropped `moduleId` from the spec | | [Expose animatable ref from legacy `GestureDetector` `Wrap`](#4554) | [b5730d0](b5730d0) | [6f72f2e](6f72f2e) | ❌ | Import/export conflicts only (2.x exports no `Worklets`); existing test file updated | | [[Android] Clear the pending HOVER_EXIT finish when the hover handler is reset](#4593) | [cdb3c9d](cdb3c9d) | [4ab9fb5](4ab9fb5) | ✅ | Amended: safe call, 2.x `handler` is nullable | | [[Android] Stop nested scroll when the root starts intercepting](#4592) | [6c2a8df](6c2a8df) | [558bcaa](558bcaa) | ✅ | | | [Fix gesture relations after `freeze`](#4244) | [6ba9d63](6ba9d63) | [5ed131a](5ed131a) | ✅ | Missed in the 2.32/2.33 threads; required for #4546/#4562 mount reactions on 2.x (CodeRabbit finding), also fixes #4238 | ## Test plan Tested that example apps are built correctly --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 88fb470 commit b06b3ef

24 files changed

Lines changed: 324 additions & 60 deletions

‎packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerDelegate.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public RNGestureHandlerRootViewManagerDelegate(U viewManager) {
2323
@Override
2424
public void setProperty(T view, String propName, @Nullable Object value) {
2525
switch (propName) {
26+
case "pointerEvents":
27+
mViewManager.setPointerEvents(view, (String) value);
28+
break;
2629
case "unstable_forceActive":
2730
mViewManager.setUnstable_forceActive(view, value == null ? false : (boolean) value);
2831
break;

‎packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/RNGestureHandlerRootViewManagerInterface.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
package com.facebook.react.viewmanagers;
1111

1212
import android.view.View;
13+
import androidx.annotation.Nullable;
1314
import com.facebook.react.uimanager.ViewManagerWithGeneratedInterface;
1415

1516
public interface RNGestureHandlerRootViewManagerInterface<T extends View> extends ViewManagerWithGeneratedInterface {
17+
void setPointerEvents(T view, @Nullable String value);
1618
void setUnstable_forceActive(T view, boolean value);
1719
}

‎packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -568,10 +568,14 @@ open class GestureHandler {
568568
}
569569

570570
// if there are tracked pointers and the gesture is about to end, send event cancelling all pointers
571-
if (trackedPointersCount > 0 &&
572-
(newState == STATE_END || newState == STATE_CANCELLED || newState == STATE_FAILED)
573-
) {
571+
if (trackedPointersCount > 0 && isFinished(newState)) {
574572
cancelPointers()
573+
574+
// `cancelPointers` dispatches the event synchronously and its callback may change the state
575+
// (e.g. via `GestureStateManager.fail`), in which case the transition is already complete.
576+
if (isFinished(state)) {
577+
return
578+
}
575579
}
576580

577581
val oldState = state
@@ -951,6 +955,8 @@ open class GestureHandler {
951955
}
952956

953957
companion object {
958+
fun isFinished(state: Int) = state == STATE_CANCELLED || state == STATE_FAILED || state == STATE_END
959+
954960
private const val DEFAULT_NEEDS_POINTER_DATA = false
955961
private const val DEFAULT_MANUAL_ACTIVATION = false
956962
private const val DEFAULT_SHOULD_CANCEL_WHEN_OUTSIDE = false

‎packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class GestureHandlerOrchestrator(
9797

9898
private fun cleanupFinishedHandlers() {
9999
for (handler in gestureHandlers.asReversed()) {
100-
if (isFinished(handler.state) && !handler.isAwaiting) {
100+
if (GestureHandler.isFinished(handler.state) && !handler.isAwaiting) {
101101
handler.reset()
102102
handler.apply {
103103
isActive = false
@@ -107,13 +107,13 @@ class GestureHandlerOrchestrator(
107107
}
108108
}
109109

110-
gestureHandlers.removeAll { isFinished(it.state) && !it.isAwaiting }
110+
gestureHandlers.removeAll { GestureHandler.isFinished(it.state) && !it.isAwaiting }
111111

112112
finishedHandlersCleanupScheduled = false
113113
}
114114

115115
private fun hasOtherHandlerToWaitFor(handler: GestureHandler) =
116-
gestureHandlers.any { !isFinished(it.state) && shouldHandlerWaitForOther(handler, it) }
116+
gestureHandlers.any { !GestureHandler.isFinished(it.state) && shouldHandlerWaitForOther(handler, it) }
117117

118118
private fun shouldBeCancelledByFinishedHandler(handler: GestureHandler) =
119119
gestureHandlers.any { shouldHandlerWaitForOther(handler, it) && it.state == GestureHandler.STATE_END }
@@ -163,7 +163,7 @@ class GestureHandlerOrchestrator(
163163
handler.isAwaiting = false
164164
}
165165

166-
if (isFinished(newState)) {
166+
if (GestureHandler.isFinished(newState)) {
167167
// We have to loop through copy in order to avoid modifying collection
168168
// while iterating over its elements
169169
val currentlyAwaitingHandlers = awaitingHandlers.toList()
@@ -678,6 +678,7 @@ class GestureHandlerOrchestrator(
678678
continue
679679
}
680680
view.onTouchEvent(cancelEvent)
681+
view.stopNestedScroll()
681682
}
682683

683684
cancelEvent.recycle()
@@ -927,9 +928,5 @@ class GestureHandlerOrchestrator(
927928
true
928929
}
929930
}
930-
931-
private fun isFinished(state: Int) = state == GestureHandler.STATE_CANCELLED ||
932-
state == GestureHandler.STATE_FAILED ||
933-
state == GestureHandler.STATE_END
934931
}
935932
}

‎packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/HoverGestureHandler.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ class HoverGestureHandler : GestureHandler() {
124124
override fun onReset() {
125125
super.onReset()
126126
stylusData = StylusData()
127+
handler?.removeCallbacksAndMessages(null)
127128
}
128129

129130
private fun finish() {

‎packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
155155
}
156156

157157
companion object {
158-
private const val MIN_ALPHA_FOR_TOUCH = 0.1f
158+
// Same threshold as UIKit / RCTViewComponentView hit testing on iOS.
159+
private const val MIN_ALPHA_FOR_TOUCH = 0.01f
159160
private fun findRootViewTag(viewGroup: ViewGroup): ViewGroup {
160161
UiThreadUtil.assertOnUiThread()
161162
var parent: ViewParent? = viewGroup

‎packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootViewManager.kt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.swmansion.gesturehandler.react
22

33
import com.facebook.react.module.annotations.ReactModule
4+
import com.facebook.react.uimanager.PointerEvents.Companion.parsePointerEvents
45
import com.facebook.react.uimanager.ThemedReactContext
56
import com.facebook.react.uimanager.ViewGroupManager
67
import com.facebook.react.uimanager.ViewManagerDelegate
@@ -33,6 +34,11 @@ class RNGestureHandlerRootViewManager :
3334
view.tearDown()
3435
}
3536

37+
@ReactProp(name = "pointerEvents")
38+
override fun setPointerEvents(view: RNGestureHandlerRootView, pointerEventsStr: String?) {
39+
view.pointerEvents = parsePointerEvents(pointerEventsStr)
40+
}
41+
3642
@ReactProp(name = "unstable_forceActive")
3743
override fun setUnstable_forceActive(view: RNGestureHandlerRootView, active: Boolean) {
3844
view.setUnstableForceActive(active)

‎packages/react-native-gesture-handler/apple/RNGestureHandler.mm‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ - (instancetype)initWithTag:(NSNumber *)tag
105105
- (void)resetConfig
106106
{
107107
self.enabled = YES;
108-
self.manualActivation = NO;
108+
_manualActivation = NO;
109109
_shouldCancelWhenOutside = NO;
110110
_handlersToWaitFor = nil;
111111
_simultaneousHandlers = nil;
@@ -176,6 +176,8 @@ - (void)configure:(NSDictionary *)config
176176
RCTLogError(@"Cannot have all of top, bottom and height defined");
177177
}
178178
}
179+
180+
[self syncManualActivationRecognizer];
179181
}
180182

181183
- (void)setEnabled:(BOOL)enabled
@@ -480,14 +482,24 @@ - (void)stopActivationBlocker
480482
- (void)setManualActivation:(BOOL)manualActivation
481483
{
482484
_manualActivation = manualActivation;
485+
[self syncManualActivationRecognizer];
486+
}
487+
488+
- (void)syncManualActivationRecognizer
489+
{
490+
BOOL hasRecognizer = _manualActivationRecognizer != nil;
483491

484-
if (manualActivation) {
492+
if (hasRecognizer == _manualActivation) {
493+
return;
494+
}
495+
496+
if (_manualActivation) {
485497
_manualActivationRecognizer = [[RNManualActivationRecognizer alloc] initWithGestureHandler:self];
486498

487499
if (_recognizer.view != nil) {
488500
[_recognizer.view addGestureRecognizer:_manualActivationRecognizer];
489501
}
490-
} else if (_manualActivationRecognizer != nil) {
502+
} else {
491503
[_manualActivationRecognizer.view removeGestureRecognizer:_manualActivationRecognizer];
492504
_manualActivationRecognizer = nil;
493505
}

‎packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
259259
// This is necessary because RCTViewComponentView's hitTest might handle pointerEvents
260260
// from ViewProps and prevent touches from reaching _buttonView (which is the contentView).
261261
// Since _buttonView has its own pointerEvents handling, we always forward to it.
262+
// Keep the visibility checks RCTViewComponentView applies, so a hidden or
263+
// (nearly) transparent button is skipped like any other RN view.
262264
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
263265
{
266+
if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
267+
return nil;
268+
}
269+
264270
if (![self pointInside:point withEvent:event]) {
265271
return nil;
266272
}

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { cleanup, render } from '@testing-library/react-native';
22
import React from 'react';
33
import { findNodeHandle, View } from 'react-native';
44

5+
import { Wrap } from '../handlers/gestures/GestureDetector/Wrap';
6+
import * as reanimatedWrapper from '../handlers/gestures/reanimatedWrapper';
57
import { Gesture, GestureDetector, GestureHandlerRootView } from '../index';
68
import RNGestureHandlerModule from '../RNGestureHandlerModule';
79

@@ -132,6 +134,65 @@ describe('Legacy GestureDetector ref forwarding', () => {
132134
expect(secondRef).toHaveBeenCalledWith(childInstance);
133135
});
134136

137+
describe('animatable ref for Reanimated', () => {
138+
let refreshSpy: jest.SpyInstance;
139+
140+
beforeEach(() => {
141+
refreshSpy = jest.spyOn(
142+
reanimatedWrapper,
143+
'reanimatedRefreshesAnimatableRef'
144+
);
145+
});
146+
147+
afterEach(() => {
148+
refreshSpy.mockRestore();
149+
});
150+
151+
test('exposes the child host instance when Reanimated re-resolves it on update', () => {
152+
refreshSpy.mockReturnValue(true);
153+
const wrapRef = React.createRef<Wrap>();
154+
const childRef = React.createRef<ChildWithHostInstance>();
155+
156+
render(
157+
<Wrap ref={wrapRef}>
158+
<ChildWithHostInstance ref={childRef} />
159+
</Wrap>
160+
);
161+
162+
expect(childRef.current).not.toBeNull();
163+
expect(wrapRef.current?.getAnimatableRef()).toBe(childRef.current);
164+
});
165+
166+
test('falls back to itself without a host instance', () => {
167+
refreshSpy.mockReturnValue(true);
168+
const wrapRef = React.createRef<Wrap>();
169+
170+
render(
171+
<Wrap ref={wrapRef}>
172+
<ChildIgnoringRef />
173+
</Wrap>
174+
);
175+
176+
expect(wrapRef.current).not.toBeNull();
177+
expect(wrapRef.current?.getAnimatableRef()).toBe(wrapRef.current);
178+
});
179+
180+
test('returns itself when Reanimated caches the animatable ref', () => {
181+
refreshSpy.mockReturnValue(false);
182+
const wrapRef = React.createRef<Wrap>();
183+
const childRef = React.createRef<ChildWithHostInstance>();
184+
185+
render(
186+
<Wrap ref={wrapRef}>
187+
<ChildWithHostInstance ref={childRef} />
188+
</Wrap>
189+
);
190+
191+
expect(childRef.current).not.toBeNull();
192+
expect(wrapRef.current?.getAnimatableRef()).toBe(wrapRef.current);
193+
});
194+
});
195+
135196
test('does not reattach gestures on re-render', () => {
136197
const gesture = Gesture.Tap();
137198

0 commit comments

Comments
 (0)