Skip to content

Commit

Permalink
fix: reimplement native "hitSlop" property
Browse files Browse the repository at this point in the history
  • Loading branch information
aleclarson committed Mar 16, 2019
1 parent 675ba5e commit 830777c
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 7 deletions.
4 changes: 4 additions & 0 deletions React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,9 @@
*/
@property (nonatomic, assign) RCTBorderStyle borderStyle;

/**
* Insets used when hit testing inside this view.
*/
@property (nonatomic, assign) NSEdgeInsets hitTestEdgeInsets;

@end
65 changes: 58 additions & 7 deletions React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_borderBottomStartRadius = -1;
_borderBottomEndRadius = -1;
_borderStyle = RCTBorderStyleSolid;
_hitTestEdgeInsets = NSEdgeInsetsZero;
self.clipsToBounds = NO;
}

Expand Down Expand Up @@ -186,21 +187,71 @@ - (void)setTransform:(CATransform3D)transform

- (NSView *)hitTest:(CGPoint)point
{
// TODO: implement pointerEvents
// TODO: implement "isUserInteractionEnabled"
// BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]);
// if(!canReceiveTouchEvents) {
// return nil;
// }

if (self.isHidden) {
return nil;
}

// `hitSubview` is the topmost subview which was hit. The hit point can
// be outside the bounds of `view` (e.g., if -clipsToBounds is NO).
NSView *hitSubview = nil;
BOOL isPointInside = [self pointInside:point];
BOOL needsHitSubview = !(_pointerEvents == RCTPointerEventsNone || _pointerEvents == RCTPointerEventsBoxOnly);
if (needsHitSubview && (![self clipsToBounds] || isPointInside)) {
// Take z-index into account when calculating the touch target.
NSArray<NSView *> *sortedSubviews = [self reactZIndexSortedSubviews];

// The default behaviour of UIKit is that if a view does not contain a point,
// then no subviews will be returned from hit testing, even if they contain
// the hit point. By doing hit testing directly on the subviews, we bypass
// the strict containment policy (i.e., UIKit guarantees that every ancestor
// of the hit view will return YES from -pointInside:withEvent:). See:
// - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html
for (NSView *subview in [sortedSubviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
hitSubview = [subview hitTest:convertedPoint];
if (hitSubview != nil) {
break;
}
}
}

NSView *hitView = (isPointInside ? self : nil);
return hitSubview ?: hitView;

// TODO: implement "pointerEvents"
// switch (_pointerEvents) {
// case RCTPointerEventsNone:
// return nil;
// case RCTPointerEventsUnspecified:
// return RCTViewHitTest(self, point, event) ?: [super hitTest:point withEvent:event];
// return hitSubview ?: hitView;
// case RCTPointerEventsBoxOnly:
// return [super hitTest:point withEvent:event] ? self: nil;
// return hitView;
// case RCTPointerEventsBoxNone:
// return RCTViewHitTest(self, point, event);
// return hitSubview;
// default:
// RCTLogError(@"Invalid pointer-events specified %zd on %@", _pointerEvents, self);
// return [super hitTest:point withEvent:event];
// RCTLogError(@"Invalid pointer-events specified %lld on %@", (long long)_pointerEvents, self);
// return hitSubview ?: hitView;
// }
return [super hitTest:point];
}

static inline CGRect NSEdgeInsetsInsetRect(CGRect rect, NSEdgeInsets insets) {
rect.origin.x += insets.left;
rect.origin.y += insets.top;
rect.size.width -= (insets.left + insets.right);
rect.size.height -= (insets.top + insets.bottom);
return rect;
}

- (BOOL)pointInside:(CGPoint)point
{
CGRect hitFrame = NSEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}

- (NSView *)reactAccessibilityElement
Expand Down
12 changes: 12 additions & 0 deletions React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,18 @@ - (void)checkLayerExists:(NSView *)view
view.borderStyle = json ? [RCTConvert RCTBorderStyle:json] : defaultView.borderStyle;
}
}
RCT_CUSTOM_VIEW_PROPERTY(hitSlop, UIEdgeInsets, RCTView)
{
if ([view respondsToSelector:@selector(setHitTestEdgeInsets:)]) {
if (json) {
NSEdgeInsets hitSlopInsets = [RCTConvert NSEdgeInsets:json];
view.hitTestEdgeInsets = NSEdgeInsetsMake(-hitSlopInsets.top, -hitSlopInsets.left, -hitSlopInsets.bottom, -hitSlopInsets.right);
} else {
view.hitTestEdgeInsets = defaultView.hitTestEdgeInsets;
}
}
}

// RCT_EXPORT_VIEW_PROPERTY(onAccessibilityTap, RCTDirectEventBlock)
// RCT_EXPORT_VIEW_PROPERTY(onMagicTap, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDragEnter, RCTDirectEventBlock)
Expand Down

0 comments on commit 830777c

Please sign in to comment.