@@ -49,6 +49,11 @@ @implementation RCTViewComponentView {
4949 BOOL _needsInvalidateLayer;
5050 BOOL _isJSResponder;
5151 BOOL _removeClippedSubviews;
52+ #if TARGET_OS_OSX // [macOS
53+ BOOL _hasMouseOver;
54+ BOOL _hasClipViewBoundsObserver;
55+ NSTrackingArea *_trackingArea;
56+ #endif // macOS]
5257 NSMutableArray <RCTUIView *> *_reactSubviews; // [macOS]
5358 NSSet <NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
5459 RCTPlatformView *_containerView; // [macOS]
@@ -645,6 +650,11 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
645650
646651 _needsInvalidateLayer = NO ;
647652 [self invalidateLayer ];
653+
654+ #if TARGET_OS_OSX // [macOS
655+ [self updateTrackingAreas ];
656+ [self updateClipViewBoundsObserverIfNeeded ];
657+ #endif // macOS]
648658}
649659
650660- (void )prepareForRecycle
@@ -1623,20 +1633,22 @@ - (BOOL)handleKeyboardEvent:(NSEvent *)event {
16231633 .functionKey = static_cast <bool >(modifierFlags & NSEventModifierFlagFunction),
16241634 };
16251635
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 );
16381651 }
1639- objc_setAssociatedObject (event, &kRCTViewKeyboardEventEmittedKey , @(YES ), OBJC_ASSOCIATION_RETAIN_NONATOMIC );
16401652 }
16411653
16421654 // If keyDownEvents or keyUpEvents specifies the event, block native handling of the event
@@ -1656,6 +1668,147 @@ - (void)keyUp:(NSEvent *)event {
16561668 }
16571669}
16581670
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+ }
16591812#endif // macOS]
16601813
16611814- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
0 commit comments