Skip to content

Commit 59a5311

Browse files
huextratm-bert
andauthored
fix(iOS): handles pointerEvents for Pressable component (#3925)
## Description fixes: #3891 fixes: #3904 This PR implements `pointerEvents` support for `Pressable` component from `react-native-gesture-handler` on iOS. Previously, setting `pointerEvents="box-none"` (or other modes) had no effect on iOS, while it worked correctly <s>on Android</s> and with React Native's `Pressable` on iOS. Android PR: #3927 ### Implementation Details The implementation follows React Native's `hitTest` logic for `pointerEvents`: - For `box-only`: Returns `self` if point is inside (respecting `hitSlop`), `nil` otherwise - For `box-none`: Checks subviews only, returns the hit subview or `nil` - For `none`: Always returns `nil` - For `auto`: Uses standard hit testing with `shouldHandleTouch` logic The implementation respects `hitTestEdgeInsets` (hitSlop) for all modes, ensuring consistent behavior with React Native's `Pressable`. ## Test plan Tested all `pointerEvents` modes on iOS: - ✅ `pointerEvents="none"` - View and subviews don't receive touches - ✅ `pointerEvents="box-none"` - View doesn't receive touches, subviews do - ✅ `pointerEvents="box-only"` - View receives touches, subviews don't - ✅ `pointerEvents="auto"` - Default behavior works as expected I've used https://github.com/huextrat/repro-pressable-gh to test scenarios Tested on both old architecture (Paper) and new architecture (Fabric). edit: `pointerEvents` with RNGH Pressable is not working on Android --------- Co-authored-by: Michał <[email protected]>
1 parent 49a2267 commit 59a5311

File tree

6 files changed

+101
-0
lines changed

6 files changed

+101
-0
lines changed

packages/react-native-gesture-handler/apple/RNGestureHandler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#import "RNGestureHandlerActionType.h"
33
#import "RNGestureHandlerDirection.h"
44
#import "RNGestureHandlerEvents.h"
5+
#import "RNGestureHandlerPointerEvents.h"
56
#import "RNGestureHandlerPointerTracker.h"
67
#import "RNGestureHandlerPointerType.h"
78
#import "RNGestureHandlerState.h"

packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;
2929
@property (nonatomic, assign) CGFloat borderRadius;
3030
@property (nonatomic) BOOL userEnabled;
31+
@property (nonatomic, assign) RNGestureHandlerPointerEvents pointerEvents;
3132

3233
#if TARGET_OS_OSX && RCT_NEW_ARCH_ENABLED
3334
- (void)mountChildComponentView:(RNGHUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index;

packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ - (instancetype)init
5050
if (self) {
5151
_hitTestEdgeInsets = UIEdgeInsetsZero;
5252
_userEnabled = YES;
53+
_pointerEvents = RNGestureHandlerPointerEventsAuto;
5354
#if !TARGET_OS_TV && !TARGET_OS_OSX
5455
[self setExclusiveTouch:YES];
5556
#endif
@@ -93,6 +94,29 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
9394

9495
- (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
9596
{
97+
RNGestureHandlerPointerEvents pointerEvents = _pointerEvents;
98+
99+
if (pointerEvents == RNGestureHandlerPointerEventsNone) {
100+
return nil;
101+
}
102+
103+
if (pointerEvents == RNGestureHandlerPointerEventsBoxNone) {
104+
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
105+
if (!subview.isHidden && subview.alpha > 0) {
106+
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
107+
UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
108+
if (hitView != nil && [self shouldHandleTouch:hitView]) {
109+
return hitView;
110+
}
111+
}
112+
}
113+
return nil;
114+
}
115+
116+
if (pointerEvents == RNGestureHandlerPointerEventsBoxOnly) {
117+
return [self pointInside:point withEvent:event] ? self : nil;
118+
}
119+
96120
RNGHUIView *inner = [super hitTest:point withEvent:event];
97121
while (inner && ![self shouldHandleTouch:inner]) {
98122
inner = inner.superview;

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,27 @@
99
#import <react/renderer/components/rngesturehandler_codegen/EventEmitters.h>
1010
#import <react/renderer/components/rngesturehandler_codegen/Props.h>
1111
#import <react/renderer/components/rngesturehandler_codegen/RCTComponentViewHelpers.h>
12+
#import <react/renderer/components/view/ViewProps.h>
1213

1314
#import "RNGestureHandlerButton.h"
1415

1516
using namespace facebook::react;
1617

18+
static RNGestureHandlerPointerEvents RCTPointerEventsToEnum(facebook::react::PointerEventsMode pointerEvents)
19+
{
20+
switch (pointerEvents) {
21+
case facebook::react::PointerEventsMode::None:
22+
return RNGestureHandlerPointerEventsNone;
23+
case facebook::react::PointerEventsMode::BoxNone:
24+
return RNGestureHandlerPointerEventsBoxNone;
25+
case facebook::react::PointerEventsMode::BoxOnly:
26+
return RNGestureHandlerPointerEventsBoxOnly;
27+
case facebook::react::PointerEventsMode::Auto:
28+
default:
29+
return RNGestureHandlerPointerEventsAuto;
30+
}
31+
}
32+
1733
@interface RNGestureHandlerButtonComponentView () <RCTRNGestureHandlerButtonViewProtocol>
1834
@end
1935

@@ -207,8 +223,35 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
207223
_buttonView.hitTestEdgeInsets = UIEdgeInsetsMake(
208224
-newProps.hitSlop.top, -newProps.hitSlop.left, -newProps.hitSlop.bottom, -newProps.hitSlop.right);
209225

226+
if (!oldProps) {
227+
_buttonView.pointerEvents = RCTPointerEventsToEnum(newProps.pointerEvents);
228+
} else {
229+
const auto &oldButtonProps = *std::static_pointer_cast<const RNGestureHandlerButtonProps>(oldProps);
230+
if (oldButtonProps.pointerEvents != newProps.pointerEvents) {
231+
_buttonView.pointerEvents = RCTPointerEventsToEnum(newProps.pointerEvents);
232+
}
233+
}
234+
210235
[super updateProps:props oldProps:oldProps];
211236
}
237+
238+
#if !TARGET_OS_OSX
239+
// Override hitTest to forward touches to _buttonView
240+
// This is necessary because RCTViewComponentView's hitTest might handle pointerEvents
241+
// from ViewProps and prevent touches from reaching _buttonView (which is the contentView).
242+
// Since _buttonView has its own pointerEvents handling, we always forward to it.
243+
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
244+
{
245+
if (![self pointInside:point withEvent:event]) {
246+
return nil;
247+
}
248+
249+
CGPoint buttonPoint = [self convertPoint:point toView:_buttonView];
250+
251+
return [_buttonView hitTest:buttonPoint withEvent:event];
252+
}
253+
#endif
254+
212255
@end
213256

214257
Class<RCTComponentViewProtocol> RNGestureHandlerButtonCls(void)

packages/react-native-gesture-handler/apple/RNGestureHandlerButtonManager.mm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
#import "RNGestureHandlerButtonManager.h"
22
#import "RNGestureHandlerButton.h"
33

4+
static RNGestureHandlerPointerEvents RCTPointerEventsToEnum(RCTPointerEvents pointerEvents)
5+
{
6+
switch (pointerEvents) {
7+
case RCTPointerEventsNone:
8+
return RNGestureHandlerPointerEventsNone;
9+
case RCTPointerEventsBoxNone:
10+
return RNGestureHandlerPointerEventsBoxNone;
11+
case RCTPointerEventsBoxOnly:
12+
return RNGestureHandlerPointerEventsBoxOnly;
13+
default:
14+
return RNGestureHandlerPointerEventsAuto;
15+
}
16+
}
17+
418
@implementation RNGestureHandlerButtonManager
519

620
RCT_EXPORT_MODULE(RNGestureHandlerButton)
@@ -28,6 +42,16 @@ @implementation RNGestureHandlerButtonManager
2842
}
2943
}
3044

45+
RCT_CUSTOM_VIEW_PROPERTY(pointerEvents, RCTPointerEvents, RNGestureHandlerButton)
46+
{
47+
if (json) {
48+
RCTPointerEvents pointerEvents = [RCTConvert RCTPointerEvents:json];
49+
view.pointerEvents = RCTPointerEventsToEnum(pointerEvents);
50+
} else {
51+
view.pointerEvents = RNGestureHandlerPointerEventsAuto;
52+
}
53+
}
54+
3155
- (RNGHUIView *)view
3256
{
3357
return (RNGHUIView *)[RNGestureHandlerButton new];
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#import <Foundation/Foundation.h>
2+
3+
typedef NS_ENUM(NSInteger, RNGestureHandlerPointerEvents) {
4+
RNGestureHandlerPointerEventsNone,
5+
RNGestureHandlerPointerEventsBoxNone,
6+
RNGestureHandlerPointerEventsBoxOnly,
7+
RNGestureHandlerPointerEventsAuto
8+
};

0 commit comments

Comments
 (0)