diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index 637b3917cf6d68..a994ffb7f306b4 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -10,9 +10,8 @@ #if !TARGET_OS_OSX // [macOS] #import -#endif // [macOS] -#if TARGET_OS_OSX // [macOS -#import +#else // [macOS +#import #endif // macOS] #import @@ -28,16 +27,29 @@ #import "RCTConversions.h" #import "RCTFabricComponentsPlugins.h" +#import // [macOS] + using namespace facebook::react; +#if !TARGET_OS_OSX // [macOS] // ParagraphTextView is an auxiliary view we set as contentView so the drawing // can happen on top of the layers manipulated by RCTViewComponentView (the parent view) @interface RCTParagraphTextView : RCTUIView // [macOS] +#else // [macOS +// On macOS, we also defer drawing to an NSTextView, +// in order to get more native behaviors like text selection. +@interface RCTParagraphTextView : NSTextView // [macOS] +#endif // macOS] @property (nonatomic) ParagraphShadowNode::ConcreteState::Shared state; @property (nonatomic) ParagraphAttributes paragraphAttributes; @property (nonatomic) LayoutMetrics layoutMetrics; +#if TARGET_OS_OSX // [macOS +/// UIKit compatibility shim that simply calls `[self setNeedsDisplay:YES]` +- (void)setNeedsDisplay; +#endif // macOS] + @end #if !TARGET_OS_OSX // [macOS] @@ -47,7 +59,7 @@ @interface RCTParagraphComponentView () @end #else // [macOS -@interface RCTParagraphComponentView () +@interface RCTParagraphComponentView () @end #endif // [macOS] @@ -57,7 +69,7 @@ @implementation RCTParagraphComponentView { RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; #if !TARGET_OS_OSX // [macOS] UILongPressGestureRecognizer *_longPressGestureRecognizer; -#endif // [macOS] +#endif // macOS] RCTParagraphTextView *_textView; } @@ -66,11 +78,29 @@ - (instancetype)initWithFrame:(CGRect)frame if (self = [super initWithFrame:frame]) { _props = ParagraphShadowNode::defaultSharedProps(); -#if !TARGET_OS_OSX // [macOS] +#if !TARGET_OS_OSX // [macOS] self.opaque = NO; -#endif // [macOS] _textView = [RCTParagraphTextView new]; _textView.backgroundColor = RCTUIColor.clearColor; // [macOS] +#else // [macOS + // Make the RCTParagraphComponentView accessible and available in the a11y hierarchy. + self.accessibilityElement = YES; + self.accessibilityRole = NSAccessibilityStaticTextRole; + // Fix blurry text on non-retina displays. + self.canDrawSubviewsIntoLayer = YES; + _textView = [[RCTParagraphTextView alloc] initWithFrame:self.bounds]; + _textView.delegate = self; + _textView.accessibilityElement = NO; + _textView.usesFontPanel = NO; + _textView.drawsBackground = NO; + _textView.linkTextAttributes = @{}; + _textView.editable = NO; + _textView.selectable = NO; + _textView.verticallyResizable = NO; + _textView.layoutManager.usesFontLeading = NO; + self.contentView = _textView; + self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; +#endif // macOS] self.contentView = _textView; } @@ -127,7 +157,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & } else { [self disableContextMenu]; } -#endif // [macOS] +#else // [macOS + _textView.selectable = newParagraphProps.isSelectable; +#endif // macOS] } [super updateProps:props oldProps:oldProps]; @@ -185,7 +217,7 @@ - (BOOL)isAccessibilityElement return NO; } -#if !TARGET_OS_OSX // [macOS +#if !TARGET_OS_OSX // [macOS] - (NSArray *)accessibilityElements { const auto ¶graphProps = static_cast(*_props); @@ -221,12 +253,7 @@ - (UIAccessibilityTraits)accessibilityTraits { return [super accessibilityTraits] | UIAccessibilityTraitStaticText; } -#else // [macOS -- (NSAccessibilityRole)accessibilityRole -{ - return [super accessibilityRole] ?: NSAccessibilityStaticTextRole; -} -#endif // macOS] +#endif // [macOS] #pragma mark - RCTTouchableComponentViewProtocol @@ -302,13 +329,87 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture [menuController showMenuFromView:self rect:self.bounds]; } } -#endif // [macOS] +#else // [macOS +- (NSView *)hitTest:(NSPoint)point +{ + // We will forward mouse click events to the NSTextView ourselves to prevent NSTextView from swallowing events that may be handled in JS (e.g. long press). + NSView *hitView = [super hitTest:point]; + + NSEventType eventType = NSApp.currentEvent.type; + BOOL isMouseClickEvent = NSEvent.pressedMouseButtons > 0; + BOOL isMouseMoveEventType = eventType == NSEventTypeMouseMoved || eventType == NSEventTypeMouseEntered || eventType == NSEventTypeMouseExited || eventType == NSEventTypeCursorUpdate; + BOOL isMouseMoveEvent = !isMouseClickEvent && isMouseMoveEventType; + BOOL isTextViewClick = (hitView && hitView == _textView) && !isMouseMoveEvent; + + return isTextViewClick ? self : hitView; +} + +- (void)mouseDown:(NSEvent *)event +{ + if (!_textView.selectable) { + [super mouseDown:event]; + return; + } + + // Double/triple-clicks should be forwarded to the NSTextView. + BOOL shouldForward = event.clickCount > 1; + + if (!shouldForward) { + // Peek at next event to know if a selection should begin. + NSEvent *nextEvent = [self.window nextEventMatchingMask:NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged + untilDate:[NSDate distantFuture] + inMode:NSEventTrackingRunLoopMode + dequeue:NO]; + shouldForward = nextEvent.type == NSEventTypeLeftMouseDragged; + } + + if (shouldForward) { + NSView *contentView = self.window.contentView; + // -[NSView hitTest:] takes coordinates in a view's superview coordinate system. + NSPoint point = [contentView.superview convertPoint:event.locationInWindow fromView:nil]; + + // Start selection if we're still selectable and hit-testable. + if (_textView.selectable && [contentView hitTest:point] == self) { + [[RCTSurfaceTouchHandler surfaceTouchHandlerForView:self] cancelTouchWithEvent:event]; + [self.window makeFirstResponder:_textView]; + [_textView mouseDown:event]; + } + } else { + // Clear selection for single clicks. + _textView.selectedRange = NSMakeRange(NSNotFound, 0); + } +} + +#pragma mark - Selection +- (void)textDidEndEditing:(NSNotification *)notification +{ + _textView.selectedRange = NSMakeRange(NSNotFound, 0); +} + +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] - (BOOL)canBecomeFirstResponder { const auto ¶graphProps = static_cast(*_props); return paragraphProps.isSelectable; } +#else +- (BOOL)becomeFirstResponder +{ + if (![super becomeFirstResponder]) { + return NO; + } + + return YES; +} + +- (BOOL)canBecomeFirstResponder +{ + return self.focusable; +} +#endif // macOS] #if !TARGET_OS_OSX // [macOS] - (BOOL)canPerformAction:(SEL)action withSender:(id)sender @@ -319,7 +420,11 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return YES; } +#if !TARGET_OS_OSX // [macOS] return [self.nextResponder canPerformAction:action withSender:sender]; +#else // [macOS + return NO; +#endif // macOS] } #endif // [macOS] @@ -357,10 +462,12 @@ - (void)copy:(id)sender } @implementation RCTParagraphTextView { +#if !TARGET_OS_OSX // [macOS] CAShapeLayer *_highlightLayer; +#endif // macOS] } -- (RCTUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +- (RCTUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS] { return nil; } @@ -382,6 +489,7 @@ - (void)drawRect:(CGRect)rect CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); +#if !TARGET_OS_OSX // [macOS] [nativeTextLayoutManager drawAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes frame:frame @@ -393,70 +501,54 @@ - (void)drawRect:(CGRect)rect [self.layer addSublayer:self->_highlightLayer]; } self->_highlightLayer.position = frame.origin; - -#if !TARGET_OS_OSX // [macOS] self->_highlightLayer.path = highlightPath.CGPath; -#else // [macOS Update once our minimum is macOS 14 - self->_highlightLayer.path = UIBezierPathCreateCGPathRef(highlightPath); -#endif // macOS] } else { [self->_highlightLayer removeFromSuperlayer]; self->_highlightLayer = nil; } }]; -} +#else // [macOS + NSTextStorage *textStorage = [nativeTextLayoutManager getTextStorageForAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes size:frame.size]; -@end + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; + + [self replaceTextContainer:textContainer]; + + NSArray *managers = [[textStorage layoutManagers] copy]; + for (NSLayoutManager *manager in managers) { + [textStorage removeLayoutManager:manager]; + } + + self.minSize = frame.size; + self.maxSize = frame.size; + self.frame = frame; + [[self textStorage] setAttributedString:textStorage]; + + [super drawRect:rect]; +#endif +} #if TARGET_OS_OSX // [macOS -// Copied from RCTUIKit -CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *bezierPath) +- (void)setNeedsDisplay { - CGPathRef immutablePath = NULL; - - // Draw the path elements. - NSInteger numElements = [bezierPath elementCount]; - if (numElements > 0) - { - CGMutablePathRef path = CGPathCreateMutable(); - NSPoint points[3]; - BOOL didClosePath = YES; - - for (NSInteger i = 0; i < numElements; i++) - { - switch ([bezierPath elementAtIndex:i associatedPoints:points]) - { - case NSBezierPathElementMoveTo: - CGPathMoveToPoint(path, NULL, points[0].x, points[0].y); - break; - - case NSBezierPathElementLineTo: - CGPathAddLineToPoint(path, NULL, points[0].x, points[0].y); - didClosePath = NO; - break; - - case NSBezierPathElementCurveTo: - CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y, - points[1].x, points[1].y, - points[2].x, points[2].y); - didClosePath = NO; - break; - - case NSBezierPathElementClosePath: - CGPathCloseSubpath(path); - didClosePath = YES; - break; - } - } - - // Be sure the path is closed or Quartz may not do valid hit detection. - if (!didClosePath) - CGPathCloseSubpath(path); - - immutablePath = CGPathCreateCopy(path); - CGPathRelease(path); + [self setNeedsDisplay:YES]; +} + +- (BOOL)canBecomeKeyView +{ + return NO; +} + +- (BOOL)resignFirstResponder +{ + // Don't relinquish first responder while selecting text. + if (self.selectable && NSRunLoop.currentRunLoop.currentMode == NSEventTrackingRunLoopMode) { + return NO; } - - return immutablePath; + + return [super resignFirstResponder]; } #endif // macOS] + +@end diff --git a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h index f3f802d256592d..cebbb1c2872ecb 100644 --- a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h +++ b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h @@ -9,6 +9,10 @@ NS_ASSUME_NONNULL_BEGIN +#if TARGET_OS_OSX // [macOS +static NSString *const RCTSurfaceTouchHandlerOutsideViewMouseUpNotification = @"RCTSurfaceTouchHandlerOutsideViewMouseUpNotification"; +#endif // macOS] + @interface RCTSurfaceTouchHandler : UIGestureRecognizer /* @@ -23,6 +27,15 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) CGPoint viewOriginOffset; +#if TARGET_OS_OSX // [macOS ++ (instancetype)surfaceTouchHandlerForEvent:(NSEvent *)event; ++ (instancetype)surfaceTouchHandlerForView:(NSView *)view; ++ (void)notifyOutsideViewMouseUp:(NSEvent *)event; + +- (void)cancelTouchWithEvent:(NSEvent *)event; +- (void)reset; +#endif // macOS] + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm index fc83e173dc51ae..a9b8ff3bf9ab3d 100644 --- a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm +++ b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm @@ -11,11 +11,65 @@ #import #import #import +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] + #import "RCTConversions.h" #import "RCTSurfacePointerHandler.h" #import "RCTTouchableComponentViewProtocol.h" + +#if TARGET_OS_OSX // [macOS +@interface RCTSurfaceTouchHandler (Private) +- (void)endFromEventTrackingLeftMouseUp:(NSEvent *)event; +- (void)endFromEventTrackingRightMouseUp:(NSEvent *)event; +@end + +@interface NSApplication (RCTSurfaceTouchHandlerOverride) +- (NSEvent*)override_surface_nextEventMatchingMask:(NSEventMask)mask + untilDate:(NSDate*)expiration + inMode:(NSRunLoopMode)mode + dequeue:(BOOL)dequeue; +@end + +@implementation NSApplication (RCTSurfaceTouchHandlerOverride) + ++ (void)load +{ + RCTSwapInstanceMethods(self, @selector(nextEventMatchingMask:untilDate:inMode:dequeue:), @selector(override_surface_nextEventMatchingMask:untilDate:inMode:dequeue:)); +} + +- (NSEvent*)override_surface_nextEventMatchingMask:(NSEventMask)mask + untilDate:(NSDate*)expiration + inMode:(NSRunLoopMode)mode + dequeue:(BOOL)dequeue +{ + NSEvent* event = [self override_surface_nextEventMatchingMask:mask + untilDate:expiration + inMode:mode + dequeue:dequeue]; + if (dequeue && (event.type == NSEventTypeLeftMouseUp || event.type == NSEventTypeRightMouseUp || event.type == NSEventTypeOtherMouseUp)) { + RCTSurfaceTouchHandler *targetSurfaceTouchHandler = [RCTSurfaceTouchHandler surfaceTouchHandlerForEvent:event]; + if (!targetSurfaceTouchHandler) { + [RCTSurfaceTouchHandler notifyOutsideViewMouseUp:event]; + } else if (event.type == NSEventTypeRightMouseUp && [mode isEqualTo:NSEventTrackingRunLoopMode]) { + // If the event is consumed by an event tracking loop, we won't get the mouse up event + if (event.type == NSEventTypeLeftMouseUp) { + [targetSurfaceTouchHandler endFromEventTrackingLeftMouseUp:event]; + } else if (event.type == NSEventTypeRightMouseUp) { + [targetSurfaceTouchHandler endFromEventTrackingRightMouseUp:event]; + } + } + } + + return event; +} + +@end +#endif // macOS] + using namespace facebook::react; typedef NS_ENUM(NSInteger, RCTTouchEventType) { @@ -207,7 +261,13 @@ - (instancetype)init self.cancelsTouchesInView = NO; self.delaysTouchesBegan = NO; // This is default value. self.delaysTouchesEnded = NO; -#endif // [macOS] +#else // [macOS + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(endOutsideViewMouseUp:) + name:RCTSurfaceTouchHandlerOutsideViewMouseUpNotification + object:[RCTSurfaceTouchHandler class]]; +#endif // macOS] + self.delegate = self; @@ -586,4 +646,224 @@ - (void)_cancelTouches [self setEnabled:YES]; } + +#if TARGET_OS_OSX // [macOS + +#pragma mark - macOS + ++ (instancetype)surfaceTouchHandlerForEvent:(NSEvent *)event { + RCTPlatformView *hitView = [event.window.contentView.superview hitTest:event.locationInWindow]; + return [self surfaceTouchHandlerForView:hitView]; +} + ++ (instancetype)surfaceTouchHandlerForView:(RCTPlatformView *)view { + if ([view isKindOfClass:[RCTSurfaceHostingView class]]) { + // The RCTSurfaceTouchHandler is attached to surface's view. + view = (RCTPlatformView *)(((RCTSurfaceHostingView *)view).surface.view); + } + + while (view) { + for (NSGestureRecognizer *gestureRecognizer in view.gestureRecognizers) { + if ([gestureRecognizer isKindOfClass:[self class]]) { + return (RCTSurfaceTouchHandler *)gestureRecognizer; + } + } + + view = view.superview; + } + + return nil; +} + ++ (void)notifyOutsideViewMouseUp:(NSEvent *)event { + [[NSNotificationCenter defaultCenter] postNotificationName:RCTSurfaceTouchHandlerOutsideViewMouseUpNotification + object:self + userInfo:@{@"event": event}]; +} + +- (void)endOutsideViewMouseUp:(NSNotification *)notification { + NSEvent *event = notification.userInfo[@"event"]; + + auto iterator = _activeTouches.find(event.eventNumber); + if (iterator == _activeTouches.end()) { + // A contextual menu click would generate a mouse up with a diffrent event + // and leave a touchable/pressable session open. This would cause touch end + // events from a modal window to end the touchable/pressable session and + // potentially trigger an onPress event. Hence the need to reset and cancel + // that session when a mouse up event was detected outside the touch handler + // view bounds. + [self reset]; + return; + } + + [self cancelTouchWithEvent:event]; +} + +- (void)endFromEventTrackingRightMouseUp:(NSEvent *)event +{ + auto iterator = _activeTouches.find(event.eventNumber); + if (iterator == _activeTouches.end()) { + return; + } + + [self cancelTouchWithEvent:event]; +} + +- (void)cancelTouchWithEvent:(NSEvent *)event +{ + NSSet *touches = [NSSet setWithObject:event]; + [self _updateTouches:touches]; + [self _dispatchActiveTouches:[self _activeTouchesFromTouches:touches] eventType:RCTTouchEventTypeTouchCancel]; + [self _unregisterTouches:touches]; + + self.state = NSGestureRecognizerStateCancelled; +} +#endif // macOS] + +#if !TARGET_OS_OSX +- (void)hovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0)) +{ + RCTUIView *listenerView = recognizer.view; // [macOS] + CGPoint clientLocation = [recognizer locationInView:listenerView]; + CGPoint screenLocation = [listenerView convertPoint:clientLocation + toCoordinateSpace:listenerView.window.screen.coordinateSpace]; + + RCTUIView *targetView = [listenerView hitTest:clientLocation withEvent:nil]; // [macOS] + targetView = FindClosestFabricManagedTouchableView(targetView); + + CGPoint offsetLocation = [recognizer locationInView:targetView]; + + UIKeyModifierFlags modifierFlags; + if (@available(iOS 13.4, *)) { + modifierFlags = recognizer.modifierFlags; + } else { + modifierFlags = 0; + } + + PointerEvent event = + CreatePointerEventFromIncompleteHoverData(clientLocation, screenLocation, offsetLocation, modifierFlags); + + NSOrderedSet *eventPathViews = [self handleIncomingPointerEvent:event onView:targetView]; + SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(targetView, offsetLocation); + bool hasMoveEventListeners = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMove) || + IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMoveCapture); + if (eventEmitter != nil && hasMoveEventListeners) { + eventEmitter->onPointerMove(event); + } +} +#endif + +/** + * Private method which is used for tracking the location of pointer events to manage the entering/leaving events. + * The primary idea is that a pointer's presence & movement is dicated by a variety of underlying events such as down, + * move, and up — and they should all be treated the same when it comes to tracking the entering & leaving of pointers + * to views. This method accomplishes that by recieving the pointer event, the target view (can be null in cases when + * the event indicates that the pointer has left the screen entirely), and a block/callback where the underlying event + * should be fired. + */ +#if !TARGET_OS_OSX +- (NSOrderedSet *)handleIncomingPointerEvent:(PointerEvent)event + onView:(nullable RCTUIView *)targetView // [macOS] +{ + int pointerId = event.pointerId; + CGPoint clientLocation = CGPointMake(event.clientPoint.x, event.clientPoint.y); + + NSOrderedSet *currentlyHoveredViews = + [_currentlyHoveredViewsPerPointer objectForKey:@(pointerId)]; + if (currentlyHoveredViews == nil) { + currentlyHoveredViews = [NSOrderedSet orderedSet]; + } + + RCTReactTaggedView *targetTaggedView = [RCTReactTaggedView wrap:targetView]; + RCTReactTaggedView *prevTargetTaggedView = [currentlyHoveredViews firstObject]; + RCTUIView *prevTargetView = prevTargetTaggedView.view; // [macOS] + + NSOrderedSet *eventPathViews = GetTouchableViewsInPathToRoot(targetView); + + // Out + if (prevTargetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) { + BOOL shouldEmitOutEvent = IsAnyViewInPathListeningToEvent(currentlyHoveredViews, ViewEvents::Offset::PointerOut); + SharedTouchEventEmitter eventEmitter = + GetTouchEmitterFromView(prevTargetView, [_rootComponentView convertPoint:clientLocation toView:prevTargetView]); + if (shouldEmitOutEvent && eventEmitter != nil) { + eventEmitter->onPointerOut(event); + } + } + + // Leaving + + // pointerleave events need to be emited from the deepest target to the root but + // we also need to efficiently keep track of if a view has a parent which is listening to the leave events, + // so we first iterate from the root to the target, collecting the views which need events fired for, of which + // we reverse iterate (now from target to root), actually emitting the events. + NSMutableOrderedSet *viewsToEmitLeaveEventsTo = [NSMutableOrderedSet orderedSet]; // [macOS] + + BOOL hasParentLeaveListener = NO; + for (RCTReactTaggedView *taggedView in [currentlyHoveredViews reverseObjectEnumerator]) { + RCTUIView *componentView = taggedView.view; // [macOS] + + BOOL shouldEmitEvent = componentView != nil && + (hasParentLeaveListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerLeave)); + + if (shouldEmitEvent && ![eventPathViews containsObject:taggedView]) { + [viewsToEmitLeaveEventsTo addObject:componentView]; + } + + if (shouldEmitEvent && !hasParentLeaveListener) { + hasParentLeaveListener = YES; + } + } + + for (RCTUIView *componentView in [viewsToEmitLeaveEventsTo reverseObjectEnumerator]) { // [macOS] + SharedTouchEventEmitter eventEmitter = + GetTouchEmitterFromView(componentView, [_rootComponentView convertPoint:clientLocation toView:componentView]); + if (eventEmitter != nil) { + eventEmitter->onPointerLeave(event); + } + } + + // Over + if (targetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) { + BOOL shouldEmitOverEvent = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerOver); + SharedTouchEventEmitter eventEmitter = + GetTouchEmitterFromView(targetView, [_rootComponentView convertPoint:clientLocation toView:targetView]); + if (shouldEmitOverEvent && eventEmitter != nil) { + eventEmitter->onPointerOver(event); + } + } + + // Entering + + // We only want to emit events to JS if there is a view that is currently listening to said event + // so we only send those event to the JS side if the element which has been entered is itself listening, + // or if one of its parents is listening in case those listeners care about the capturing phase. Adding the ability + // for native to distingusih between capturing listeners and not could be an optimization to futher reduce the number + // of events we send to JS + BOOL hasParentEnterListener = NO; + for (RCTReactTaggedView *taggedView in [eventPathViews reverseObjectEnumerator]) { + RCTUIView *componentView = taggedView.view; // [macOS] + + BOOL shouldEmitEvent = componentView != nil && + (hasParentEnterListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerEnter)); + + if (shouldEmitEvent && ![currentlyHoveredViews containsObject:taggedView]) { + SharedTouchEventEmitter eventEmitter = + GetTouchEmitterFromView(componentView, [_rootComponentView convertPoint:clientLocation toView:componentView]); + if (eventEmitter != nil) { + eventEmitter->onPointerEnter(event); + } + } + + if (shouldEmitEvent && !hasParentEnterListener) { + hasParentEnterListener = YES; + } + } + + [_currentlyHoveredViewsPerPointer setObject:eventPathViews forKey:@(pointerId)]; + + return eventPathViews; +} +#endif + + @end