@@ -49,6 +49,11 @@ @implementation RCTViewComponentView {
49
49
BOOL _needsInvalidateLayer;
50
50
BOOL _isJSResponder;
51
51
BOOL _removeClippedSubviews;
52
+ #if TARGET_OS_OSX // [macOS
53
+ BOOL _hasMouseOver;
54
+ BOOL _hasClipViewBoundsObserver;
55
+ NSTrackingArea *_trackingArea;
56
+ #endif // macOS]
52
57
NSMutableArray <RCTUIView *> *_reactSubviews; // [macOS]
53
58
NSSet <NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
54
59
RCTPlatformView *_containerView; // [macOS]
@@ -645,6 +650,11 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
645
650
646
651
_needsInvalidateLayer = NO ;
647
652
[self invalidateLayer ];
653
+
654
+ #if TARGET_OS_OSX // [macOS
655
+ [self updateTrackingAreas ];
656
+ [self updateClipViewBoundsObserverIfNeeded ];
657
+ #endif // macOS]
648
658
}
649
659
650
660
- (void )prepareForRecycle
@@ -1630,7 +1640,7 @@ - (BOOL)handleKeyboardEvent:(NSEvent *)event {
1630
1640
static const char kRCTViewKeyboardEventEmittedKey = 0 ;
1631
1641
NSNumber *emitted = objc_getAssociatedObject (event, &kRCTViewKeyboardEventEmittedKey );
1632
1642
BOOL alreadyEmitted = [emitted boolValue ];
1633
- if (!alreadyEmitted) {
1643
+ if (!alreadyEmitted && _eventEmitter ) {
1634
1644
if (event.type == NSEventTypeKeyDown) {
1635
1645
_eventEmitter->onKeyDown (keyEvent);
1636
1646
} else {
@@ -1656,6 +1666,147 @@ - (void)keyUp:(NSEvent *)event {
1656
1666
}
1657
1667
}
1658
1668
1669
+
1670
+ #pragma mark - Mouse Events
1671
+
1672
+ - (void )emitMouseEvent {
1673
+ if (!_eventEmitter) {
1674
+ return ;
1675
+ }
1676
+
1677
+ NSPoint locationInWindow = self.window .mouseLocationOutsideOfEventStream ;
1678
+ NSPoint locationInView = [self convertPoint: locationInWindow fromView: nil ];
1679
+
1680
+ NSEventModifierFlags modifierFlags = self.window .currentEvent .modifierFlags ;
1681
+
1682
+ MouseEvent mouseEvent = {
1683
+ .clientX = locationInView.x ,
1684
+ .clientY = locationInView.y ,
1685
+ .screenX = locationInWindow.x ,
1686
+ .screenY = locationInWindow.y ,
1687
+ .altKey = static_cast <bool >(modifierFlags & NSEventModifierFlagOption),
1688
+ .ctrlKey = static_cast <bool >(modifierFlags & NSEventModifierFlagControl),
1689
+ .shiftKey = static_cast <bool >(modifierFlags & NSEventModifierFlagShift),
1690
+ .metaKey = static_cast <bool >(modifierFlags & NSEventModifierFlagCommand),
1691
+ };
1692
+
1693
+ if (_hasMouseOver) {
1694
+ _eventEmitter->onMouseEnter (mouseEvent);
1695
+ } else {
1696
+ _eventEmitter->onMouseLeave (mouseEvent);
1697
+ }
1698
+ }
1699
+
1700
+ - (void )updateMouseOverIfNeeded
1701
+ {
1702
+ // When an enclosing scrollview is scrolled using the scrollWheel or trackpad,
1703
+ // the mouseExited: event does not get called on the view where mouseEntered: was previously called.
1704
+ // This creates an unnatural pairing of mouse enter and exit events and can cause problems.
1705
+ // We therefore explicitly check for this here and handle them by calling the appropriate callbacks.
1706
+
1707
+ BOOL hasMouseOver = _hasMouseOver;
1708
+ NSPoint locationInWindow = self.window .mouseLocationOutsideOfEventStream ;
1709
+ NSPoint locationInView = [self convertPoint: locationInWindow fromView: nil ];
1710
+ BOOL insideBounds = NSPointInRect (locationInView, self.visibleRect );
1711
+
1712
+ // On macOS 14+ visibleRect can be larger than the view bounds
1713
+ insideBounds &= NSPointInRect (locationInView, self.bounds );
1714
+
1715
+ if (hasMouseOver && !insideBounds) {
1716
+ hasMouseOver = NO ;
1717
+ } else if (!hasMouseOver && insideBounds) {
1718
+ // The window's frame view must be used for hit testing against `locationInWindow`
1719
+ NSView *hitView = [self .window.contentView.superview hitTest: locationInWindow];
1720
+ hasMouseOver = [hitView isDescendantOf: self ];
1721
+ }
1722
+
1723
+ if (hasMouseOver != _hasMouseOver) {
1724
+ _hasMouseOver = hasMouseOver;
1725
+ [self emitMouseEvent ];
1726
+ }
1727
+ }
1728
+
1729
+ - (void )updateClipViewBoundsObserverIfNeeded
1730
+ {
1731
+ // Subscribe to view bounds changed notification so that the view can be notified when a
1732
+ // scroll event occurs either due to trackpad/gesture based scrolling or a scrollwheel event
1733
+ // both of which would not cause the mouseExited to be invoked.
1734
+
1735
+ NSClipView *clipView = self.window ? self.enclosingScrollView .contentView : nil ;
1736
+
1737
+ BOOL hasMouseEventHandler =
1738
+ _props->hostPlatformEvents [HostPlatformViewEvents::Offset::MouseEnter] ||
1739
+ _props->hostPlatformEvents [HostPlatformViewEvents::Offset::MouseLeave];
1740
+
1741
+ if (_hasClipViewBoundsObserver && (!clipView || !hasMouseEventHandler)) {
1742
+ _hasClipViewBoundsObserver = NO ;
1743
+ [[NSNotificationCenter defaultCenter ] removeObserver: self
1744
+ name: NSViewBoundsDidChangeNotification
1745
+ object: nil ];
1746
+ } else if (!_hasClipViewBoundsObserver && clipView && hasMouseEventHandler) {
1747
+ _hasClipViewBoundsObserver = YES ;
1748
+ [[NSNotificationCenter defaultCenter ] addObserver: self
1749
+ selector: @selector (updateMouseOverIfNeeded )
1750
+ name: NSViewBoundsDidChangeNotification
1751
+ object: clipView];
1752
+ [self updateMouseOverIfNeeded ];
1753
+ }
1754
+ }
1755
+
1756
+ - (void )viewDidMoveToWindow
1757
+ {
1758
+ [self updateClipViewBoundsObserverIfNeeded ];
1759
+ [super viewDidMoveToWindow ];
1760
+ }
1761
+
1762
+ - (void )updateTrackingAreas
1763
+ {
1764
+ BOOL hasMouseEventHandler =
1765
+ _props->hostPlatformEvents [HostPlatformViewEvents::Offset::MouseEnter] ||
1766
+ _props->hostPlatformEvents [HostPlatformViewEvents::Offset::MouseLeave];
1767
+ BOOL wouldRecreateIdenticalTrackingArea =
1768
+ hasMouseEventHandler && _trackingArea && NSEqualRects (self.bounds , [_trackingArea rect ]);
1769
+
1770
+ if (!wouldRecreateIdenticalTrackingArea) {
1771
+ [self removeTrackingArea: _trackingArea];
1772
+ if (hasMouseEventHandler) {
1773
+ _trackingArea = [[NSTrackingArea alloc ] initWithRect: self .bounds
1774
+ options: NSTrackingActiveAlways|NSTrackingMouseEnteredAndExited
1775
+ owner: self
1776
+ userInfo: nil ];
1777
+ [self addTrackingArea: _trackingArea];
1778
+ [self updateMouseOverIfNeeded ];
1779
+ }
1780
+ }
1781
+
1782
+ [super updateTrackingAreas ];
1783
+ }
1784
+
1785
+ - (void )mouseEntered:(NSEvent *)event
1786
+ {
1787
+ if (_hasMouseOver) {
1788
+ return ;
1789
+ }
1790
+
1791
+ // The window's frame view must be used for hit testing against `locationInWindow`
1792
+ NSView *hitView = [self .window.contentView.superview hitTest: event.locationInWindow];
1793
+ if (![hitView isDescendantOf: self ]) {
1794
+ return ;
1795
+ }
1796
+
1797
+ _hasMouseOver = YES ;
1798
+ [self emitMouseEvent ];
1799
+ }
1800
+
1801
+ - (void )mouseExited:(NSEvent *)event
1802
+ {
1803
+ if (!_hasMouseOver) {
1804
+ return ;
1805
+ }
1806
+
1807
+ _hasMouseOver = NO ;
1808
+ [self emitMouseEvent ];
1809
+ }
1659
1810
#endif // macOS]
1660
1811
1661
1812
- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
0 commit comments