Skip to content

Commit a313557

Browse files
huextratm-bert
authored andcommitted
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ł <michal.bert@swmansion.com>
1 parent 9aec429 commit a313557

6 files changed

Lines changed: 101 additions & 0 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#import "RNGestureHandlerDirection.h"
44
#import "RNGestureHandlerEventHandlerType.h"
55
#import "RNGestureHandlerEvents.h"
6+
#import "RNGestureHandlerPointerEvents.h"
67
#import "RNGestureHandlerPointerTracker.h"
78
#import "RNGestureHandlerPointerType.h"
89
#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
@@ -26,6 +26,7 @@
2626
@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;
2727
@property (nonatomic, assign) CGFloat borderRadius;
2828
@property (nonatomic) BOOL userEnabled;
29+
@property (nonatomic, assign) RNGestureHandlerPointerEvents pointerEvents;
2930

3031
#if TARGET_OS_OSX
3132
- (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
@@ -46,6 +46,7 @@ - (instancetype)init
4646
if (self) {
4747
_hitTestEdgeInsets = UIEdgeInsetsZero;
4848
_userEnabled = YES;
49+
_pointerEvents = RNGestureHandlerPointerEventsAuto;
4950
#if !TARGET_OS_TV && !TARGET_OS_OSX
5051
[self setExclusiveTouch:YES];
5152
#endif
@@ -89,6 +90,29 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
8990

9091
- (RNGHUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
9192
{
93+
RNGestureHandlerPointerEvents pointerEvents = _pointerEvents;
94+
95+
if (pointerEvents == RNGestureHandlerPointerEventsNone) {
96+
return nil;
97+
}
98+
99+
if (pointerEvents == RNGestureHandlerPointerEventsBoxNone) {
100+
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
101+
if (!subview.isHidden && subview.alpha > 0) {
102+
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
103+
UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
104+
if (hitView != nil && [self shouldHandleTouch:hitView]) {
105+
return hitView;
106+
}
107+
}
108+
}
109+
return nil;
110+
}
111+
112+
if (pointerEvents == RNGestureHandlerPointerEventsBoxOnly) {
113+
return [self pointInside:point withEvent:event] ? self : nil;
114+
}
115+
92116
RNGHUIView *inner = [super hitTest:point withEvent:event];
93117
while (inner && ![self shouldHandleTouch:inner]) {
94118
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
@@ -7,11 +7,27 @@
77
#import <react/renderer/components/rngesturehandler_codegen/EventEmitters.h>
88
#import <react/renderer/components/rngesturehandler_codegen/Props.h>
99
#import <react/renderer/components/rngesturehandler_codegen/RCTComponentViewHelpers.h>
10+
#import <react/renderer/components/view/ViewProps.h>
1011

1112
#import "RNGestureHandlerButton.h"
1213

1314
using namespace facebook::react;
1415

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

@@ -205,8 +221,35 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
205221
_buttonView.hitTestEdgeInsets = UIEdgeInsetsMake(
206222
-newProps.hitSlop.top, -newProps.hitSlop.left, -newProps.hitSlop.bottom, -newProps.hitSlop.right);
207223

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

212255
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)