@@ -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
@@ -1623,20 +1633,22 @@ - (BOOL)handleKeyboardEvent:(NSEvent *)event {
1623
1633
.functionKey = static_cast <bool >(modifierFlags & NSEventModifierFlagFunction),
1624
1634
};
1625
1635
1626
- // Emit the event to JS only once. By default, events, will bubble up the respnder chain
1627
- // when we call super, so let's emit the event only at the first responder. It would be
1628
- // simpler to check `if (self == self.window.firstResponder), however, that does not account
1629
- // for cases like TextInputComponentView, where the first responder may be a subview.
1630
- static const char kRCTViewKeyboardEventEmittedKey = 0 ;
1631
- NSNumber *emitted = objc_getAssociatedObject (event, &kRCTViewKeyboardEventEmittedKey );
1632
- BOOL alreadyEmitted = [emitted boolValue ];
1633
- if (!alreadyEmitted) {
1634
- if (event.type == NSEventTypeKeyDown) {
1635
- _eventEmitter->onKeyDown (keyEvent);
1636
- } else {
1637
- _eventEmitter->onKeyUp (keyEvent);
1636
+ if (_eventEmitter) {
1637
+ // Emit the event to JS only once. By default, events, will bubble up the respnder chain
1638
+ // when we call super, so let's emit the event only at the first responder. It would be
1639
+ // simpler to check `if (self == self.window.firstResponder), however, that does not account
1640
+ // for cases like TextInputComponentView, where the first responder may be a subview.
1641
+ static const char kRCTViewKeyboardEventEmittedKey = 0 ;
1642
+ NSNumber *emitted = objc_getAssociatedObject (event, &kRCTViewKeyboardEventEmittedKey );
1643
+ BOOL alreadyEmitted = [emitted boolValue ];
1644
+ if (!alreadyEmitted) {
1645
+ if (event.type == NSEventTypeKeyDown) {
1646
+ _eventEmitter->onKeyDown (keyEvent);
1647
+ } else {
1648
+ _eventEmitter->onKeyUp (keyEvent);
1649
+ }
1650
+ objc_setAssociatedObject (event, &kRCTViewKeyboardEventEmittedKey , @(YES ), OBJC_ASSOCIATION_RETAIN_NONATOMIC );
1638
1651
}
1639
- objc_setAssociatedObject (event, &kRCTViewKeyboardEventEmittedKey , @(YES ), OBJC_ASSOCIATION_RETAIN_NONATOMIC );
1640
1652
}
1641
1653
1642
1654
// If keyDownEvents or keyUpEvents specifies the event, block native handling of the event
@@ -1656,6 +1668,147 @@ - (void)keyUp:(NSEvent *)event {
1656
1668
}
1657
1669
}
1658
1670
1671
+
1672
+ #pragma mark - Mouse Events
1673
+
1674
+ - (void )emitMouseEvent {
1675
+ if (!_eventEmitter) {
1676
+ return ;
1677
+ }
1678
+
1679
+ NSPoint locationInWindow = self.window .mouseLocationOutsideOfEventStream ;
1680
+ NSPoint locationInView = [self convertPoint: locationInWindow fromView: nil ];
1681
+
1682
+ NSEventModifierFlags modifierFlags = self.window .currentEvent .modifierFlags ;
1683
+
1684
+ MouseEvent mouseEvent = {
1685
+ .clientX = locationInView.x ,
1686
+ .clientY = locationInView.y ,
1687
+ .screenX = locationInWindow.x ,
1688
+ .screenY = locationInWindow.y ,
1689
+ .altKey = static_cast <bool >(modifierFlags & NSEventModifierFlagOption),
1690
+ .ctrlKey = static_cast <bool >(modifierFlags & NSEventModifierFlagControl),
1691
+ .shiftKey = static_cast <bool >(modifierFlags & NSEventModifierFlagShift),
1692
+ .metaKey = static_cast <bool >(modifierFlags & NSEventModifierFlagCommand),
1693
+ };
1694
+
1695
+ if (_hasMouseOver) {
1696
+ _eventEmitter->onMouseEnter (mouseEvent);
1697
+ } else {
1698
+ _eventEmitter->onMouseLeave (mouseEvent);
1699
+ }
1700
+ }
1701
+
1702
+ - (void )updateMouseOverIfNeeded
1703
+ {
1704
+ // When an enclosing scrollview is scrolled using the scrollWheel or trackpad,
1705
+ // the mouseExited: event does not get called on the view where mouseEntered: was previously called.
1706
+ // This creates an unnatural pairing of mouse enter and exit events and can cause problems.
1707
+ // We therefore explicitly check for this here and handle them by calling the appropriate callbacks.
1708
+
1709
+ BOOL hasMouseOver = _hasMouseOver;
1710
+ NSPoint locationInWindow = self.window .mouseLocationOutsideOfEventStream ;
1711
+ NSPoint locationInView = [self convertPoint: locationInWindow fromView: nil ];
1712
+ BOOL insideBounds = NSPointInRect (locationInView, self.visibleRect );
1713
+
1714
+ // On macOS 14+ visibleRect can be larger than the view bounds
1715
+ insideBounds &= NSPointInRect (locationInView, self.bounds );
1716
+
1717
+ if (hasMouseOver && !insideBounds) {
1718
+ hasMouseOver = NO ;
1719
+ } else if (!hasMouseOver && insideBounds) {
1720
+ // The window's frame view must be used for hit testing against `locationInWindow`
1721
+ NSView *hitView = [self .window.contentView.superview hitTest: locationInWindow];
1722
+ hasMouseOver = [hitView isDescendantOf: self ];
1723
+ }
1724
+
1725
+ if (hasMouseOver != _hasMouseOver) {
1726
+ _hasMouseOver = hasMouseOver;
1727
+ [self emitMouseEvent ];
1728
+ }
1729
+ }
1730
+
1731
+ - (void )updateClipViewBoundsObserverIfNeeded
1732
+ {
1733
+ // Subscribe to view bounds changed notification so that the view can be notified when a
1734
+ // scroll event occurs either due to trackpad/gesture based scrolling or a scrollwheel event
1735
+ // both of which would not cause the mouseExited to be invoked.
1736
+
1737
+ NSClipView *clipView = self.window ? self.enclosingScrollView .contentView : nil ;
1738
+
1739
+ BOOL hasMouseEventHandler =
1740
+ _props->hostPlatformEvents [HostPlatformViewEvents::Offset::MouseEnter] ||
1741
+ _props->hostPlatformEvents [HostPlatformViewEvents::Offset::MouseLeave];
1742
+
1743
+ if (_hasClipViewBoundsObserver && (!clipView || !hasMouseEventHandler)) {
1744
+ _hasClipViewBoundsObserver = NO ;
1745
+ [[NSNotificationCenter defaultCenter ] removeObserver: self
1746
+ name: NSViewBoundsDidChangeNotification
1747
+ object: nil ];
1748
+ } else if (!_hasClipViewBoundsObserver && clipView && hasMouseEventHandler) {
1749
+ _hasClipViewBoundsObserver = YES ;
1750
+ [[NSNotificationCenter defaultCenter ] addObserver: self
1751
+ selector: @selector (updateMouseOverIfNeeded )
1752
+ name: NSViewBoundsDidChangeNotification
1753
+ object: clipView];
1754
+ [self updateMouseOverIfNeeded ];
1755
+ }
1756
+ }
1757
+
1758
+ - (void )viewDidMoveToWindow
1759
+ {
1760
+ [self updateClipViewBoundsObserverIfNeeded ];
1761
+ [super viewDidMoveToWindow ];
1762
+ }
1763
+
1764
+ - (void )updateTrackingAreas
1765
+ {
1766
+ BOOL hasMouseEventHandler =
1767
+ _props->hostPlatformEvents [HostPlatformViewEvents::Offset::MouseEnter] ||
1768
+ _props->hostPlatformEvents [HostPlatformViewEvents::Offset::MouseLeave];
1769
+ BOOL wouldRecreateIdenticalTrackingArea =
1770
+ hasMouseEventHandler && _trackingArea && NSEqualRects (self.bounds , [_trackingArea rect ]);
1771
+
1772
+ if (!wouldRecreateIdenticalTrackingArea) {
1773
+ [self removeTrackingArea: _trackingArea];
1774
+ if (hasMouseEventHandler) {
1775
+ _trackingArea = [[NSTrackingArea alloc ] initWithRect: self .bounds
1776
+ options: NSTrackingActiveAlways|NSTrackingMouseEnteredAndExited
1777
+ owner: self
1778
+ userInfo: nil ];
1779
+ [self addTrackingArea: _trackingArea];
1780
+ [self updateMouseOverIfNeeded ];
1781
+ }
1782
+ }
1783
+
1784
+ [super updateTrackingAreas ];
1785
+ }
1786
+
1787
+ - (void )mouseEntered:(NSEvent *)event
1788
+ {
1789
+ if (_hasMouseOver) {
1790
+ return ;
1791
+ }
1792
+
1793
+ // The window's frame view must be used for hit testing against `locationInWindow`
1794
+ NSView *hitView = [self .window.contentView.superview hitTest: event.locationInWindow];
1795
+ if (![hitView isDescendantOf: self ]) {
1796
+ return ;
1797
+ }
1798
+
1799
+ _hasMouseOver = YES ;
1800
+ [self emitMouseEvent ];
1801
+ }
1802
+
1803
+ - (void )mouseExited:(NSEvent *)event
1804
+ {
1805
+ if (!_hasMouseOver) {
1806
+ return ;
1807
+ }
1808
+
1809
+ _hasMouseOver = NO ;
1810
+ [self emitMouseEvent ];
1811
+ }
1659
1812
#endif // macOS]
1660
1813
1661
1814
- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
0 commit comments