Skip to content
This repository was archived by the owner on Nov 11, 2024. It is now read-only.

Commit 830777c

Browse files
committed
fix: reimplement native "hitSlop" property
1 parent 675ba5e commit 830777c

File tree

3 files changed

+74
-7
lines changed

3 files changed

+74
-7
lines changed

React/Views/RCTView.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,9 @@
128128
*/
129129
@property (nonatomic, assign) RCTBorderStyle borderStyle;
130130

131+
/**
132+
* Insets used when hit testing inside this view.
133+
*/
134+
@property (nonatomic, assign) NSEdgeInsets hitTestEdgeInsets;
131135

132136
@end

React/Views/RCTView.m

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ - (instancetype)initWithFrame:(CGRect)frame
124124
_borderBottomStartRadius = -1;
125125
_borderBottomEndRadius = -1;
126126
_borderStyle = RCTBorderStyleSolid;
127+
_hitTestEdgeInsets = NSEdgeInsetsZero;
127128
self.clipsToBounds = NO;
128129
}
129130

@@ -186,21 +187,71 @@ - (void)setTransform:(CATransform3D)transform
186187

187188
- (NSView *)hitTest:(CGPoint)point
188189
{
189-
// TODO: implement pointerEvents
190+
// TODO: implement "isUserInteractionEnabled"
191+
// BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]);
192+
// if(!canReceiveTouchEvents) {
193+
// return nil;
194+
// }
195+
196+
if (self.isHidden) {
197+
return nil;
198+
}
199+
200+
// `hitSubview` is the topmost subview which was hit. The hit point can
201+
// be outside the bounds of `view` (e.g., if -clipsToBounds is NO).
202+
NSView *hitSubview = nil;
203+
BOOL isPointInside = [self pointInside:point];
204+
BOOL needsHitSubview = !(_pointerEvents == RCTPointerEventsNone || _pointerEvents == RCTPointerEventsBoxOnly);
205+
if (needsHitSubview && (![self clipsToBounds] || isPointInside)) {
206+
// Take z-index into account when calculating the touch target.
207+
NSArray<NSView *> *sortedSubviews = [self reactZIndexSortedSubviews];
208+
209+
// The default behaviour of UIKit is that if a view does not contain a point,
210+
// then no subviews will be returned from hit testing, even if they contain
211+
// the hit point. By doing hit testing directly on the subviews, we bypass
212+
// the strict containment policy (i.e., UIKit guarantees that every ancestor
213+
// of the hit view will return YES from -pointInside:withEvent:). See:
214+
// - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html
215+
for (NSView *subview in [sortedSubviews reverseObjectEnumerator]) {
216+
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
217+
hitSubview = [subview hitTest:convertedPoint];
218+
if (hitSubview != nil) {
219+
break;
220+
}
221+
}
222+
}
223+
224+
NSView *hitView = (isPointInside ? self : nil);
225+
return hitSubview ?: hitView;
226+
227+
// TODO: implement "pointerEvents"
190228
// switch (_pointerEvents) {
191229
// case RCTPointerEventsNone:
192230
// return nil;
193231
// case RCTPointerEventsUnspecified:
194-
// return RCTViewHitTest(self, point, event) ?: [super hitTest:point withEvent:event];
232+
// return hitSubview ?: hitView;
195233
// case RCTPointerEventsBoxOnly:
196-
// return [super hitTest:point withEvent:event] ? self: nil;
234+
// return hitView;
197235
// case RCTPointerEventsBoxNone:
198-
// return RCTViewHitTest(self, point, event);
236+
// return hitSubview;
199237
// default:
200-
// RCTLogError(@"Invalid pointer-events specified %zd on %@", _pointerEvents, self);
201-
// return [super hitTest:point withEvent:event];
238+
// RCTLogError(@"Invalid pointer-events specified %lld on %@", (long long)_pointerEvents, self);
239+
// return hitSubview ?: hitView;
202240
// }
203-
return [super hitTest:point];
241+
}
242+
243+
static inline CGRect NSEdgeInsetsInsetRect(CGRect rect, NSEdgeInsets insets) {
244+
rect.origin.x += insets.left;
245+
rect.origin.y += insets.top;
246+
rect.size.width -= (insets.left + insets.right);
247+
rect.size.height -= (insets.top + insets.bottom);
248+
return rect;
249+
}
250+
251+
- (BOOL)pointInside:(CGPoint)point
252+
{
253+
CGRect hitFrame = NSEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
254+
return CGRectContainsPoint(hitFrame, point);
204255
}
205256

206257
- (NSView *)reactAccessibilityElement

React/Views/RCTViewManager.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,18 @@ - (void)checkLayerExists:(NSView *)view
215215
view.borderStyle = json ? [RCTConvert RCTBorderStyle:json] : defaultView.borderStyle;
216216
}
217217
}
218+
RCT_CUSTOM_VIEW_PROPERTY(hitSlop, UIEdgeInsets, RCTView)
219+
{
220+
if ([view respondsToSelector:@selector(setHitTestEdgeInsets:)]) {
221+
if (json) {
222+
NSEdgeInsets hitSlopInsets = [RCTConvert NSEdgeInsets:json];
223+
view.hitTestEdgeInsets = NSEdgeInsetsMake(-hitSlopInsets.top, -hitSlopInsets.left, -hitSlopInsets.bottom, -hitSlopInsets.right);
224+
} else {
225+
view.hitTestEdgeInsets = defaultView.hitTestEdgeInsets;
226+
}
227+
}
228+
}
229+
218230
// RCT_EXPORT_VIEW_PROPERTY(onAccessibilityTap, RCTDirectEventBlock)
219231
// RCT_EXPORT_VIEW_PROPERTY(onMagicTap, RCTDirectEventBlock)
220232
RCT_EXPORT_VIEW_PROPERTY(onDragEnter, RCTDirectEventBlock)

0 commit comments

Comments
 (0)