@@ -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
0 commit comments