Skip to content

Commit 2dddc18

Browse files
SaadnajmiNick Lefevertido64
authored
feat(fabric): Implement mouse events (#2708)
## Summary: Cherry pick and rebase a bunch of changes to implement onMouseEnter and onMouseLeave for View in Fabric ## Test Plan: The existing test in "Pressable Feedback Events" works in Fabric. --------- Co-authored-by: Nick Lefever <[email protected]> Co-authored-by: Tommy Nguyen <[email protected]>
1 parent c49d7b2 commit 2dddc18

File tree

7 files changed

+275
-16
lines changed

7 files changed

+275
-16
lines changed

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 166 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,31 @@ void HostPlatformViewEventEmitter::onKeyUp(const KeyEvent& keyEvent) const {
5050
});
5151
}
5252

53+
#pragma mark - Mouse Events
54+
55+
static jsi::Value mouseEventPayload(jsi::Runtime& runtime, const MouseEvent& event) {
56+
auto payload = jsi::Object(runtime);
57+
payload.setProperty(runtime, "clientX", event.clientX);
58+
payload.setProperty(runtime, "clientY", event.clientY);
59+
payload.setProperty(runtime, "screenX", event.screenX);
60+
payload.setProperty(runtime, "screenY", event.screenY);
61+
payload.setProperty(runtime, "altKey", event.altKey);
62+
payload.setProperty(runtime, "ctrlKey", event.ctrlKey);
63+
payload.setProperty(runtime, "shiftKey", event.shiftKey);
64+
payload.setProperty(runtime, "metaKey", event.metaKey);
65+
return payload;
66+
};
67+
68+
void HostPlatformViewEventEmitter::onMouseEnter(const MouseEvent& mouseEvent) const {
69+
dispatchEvent("mouseEnter", [mouseEvent](jsi::Runtime &runtime) {
70+
return mouseEventPayload(runtime, mouseEvent);
71+
});
72+
}
73+
74+
void HostPlatformViewEventEmitter::onMouseLeave(const MouseEvent& mouseEvent) const {
75+
dispatchEvent("mouseLeave", [mouseEvent](jsi::Runtime &runtime) {
76+
return mouseEventPayload(runtime, mouseEvent);
77+
});
78+
}
79+
5380
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#include <react/renderer/components/view/BaseViewEventEmitter.h>
1313
#include <react/renderer/components/view/KeyEvent.h>
14+
#include <react/renderer/components/view/MouseEvent.h>
1415

1516
namespace facebook::react {
1617

@@ -25,8 +26,13 @@ class HostPlatformViewEventEmitter : public BaseViewEventEmitter {
2526

2627
#pragma mark - Keyboard Events
2728

28-
void onKeyDown(KeyEvent const &keyEvent) const;
29-
void onKeyUp(KeyEvent const &keyEvent) const;
29+
void onKeyDown(KeyEvent const& keyEvent) const;
30+
void onKeyUp(KeyEvent const& keyEvent) const;
31+
32+
#pragma mark - Mouse Events
33+
34+
void onMouseEnter(MouseEvent const& mouseEvent) const;
35+
void onMouseLeave(MouseEvent const& mouseEvent) const;
3036
};
3137

3238
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ struct HostPlatformViewEvents {
2525
// Keyboard Events
2626
KeyDown = 2,
2727
KeyUp = 3,
28+
29+
// Mouse Events
30+
MouseEnter = 4,
31+
MouseLeave = 5,
2832
};
2933

3034
constexpr bool operator[](const Offset offset) const {
@@ -57,10 +61,16 @@ static inline HostPlatformViewEvents convertRawProp(
5761
convertRawProp(context, rawProps, "onFocus", sourceValue[Offset::Focus], defaultValue[Offset::Focus]);
5862
result[Offset::Blur] =
5963
convertRawProp(context, rawProps, "onBlur", sourceValue[Offset::Blur], defaultValue[Offset::Blur]);
64+
// Keyboard Events
6065
result[Offset::KeyDown] =
6166
convertRawProp(context, rawProps, "onKeyDown", sourceValue[Offset::KeyDown], defaultValue[Offset::KeyDown]);
6267
result[Offset::KeyUp] =
6368
convertRawProp(context, rawProps, "onKeyUp", sourceValue[Offset::KeyUp], defaultValue[Offset::KeyUp]);
69+
// Mouse Events
70+
result[Offset::MouseEnter] =
71+
convertRawProp(context, rawProps, "onMouseEnter", sourceValue[Offset::MouseEnter], defaultValue[Offset::MouseEnter]);
72+
result[Offset::MouseLeave] =
73+
convertRawProp(context, rawProps, "onMouseLeave", sourceValue[Offset::MouseLeave], defaultValue[Offset::MouseLeave]);
6474

6575
return result;
6676
}

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ void HostPlatformViewProps::setProp(
9696
VIEW_EVENT_CASE_MACOS(Blur);
9797
VIEW_EVENT_CASE_MACOS(KeyDown);
9898
VIEW_EVENT_CASE_MACOS(KeyUp);
99+
VIEW_EVENT_CASE_MACOS(MouseEnter);
100+
VIEW_EVENT_CASE_MACOS(MouseLeave);
99101
RAW_SET_PROP_SWITCH_CASE_BASIC(focusable);
100102
RAW_SET_PROP_SWITCH_CASE_BASIC(enableFocusRing);
101103
RAW_SET_PROP_SWITCH_CASE_BASIC(keyDownEvents);

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewTraitsInitializer.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ inline bool formsStackingContext(const ViewProps& props) {
1717
}
1818

1919
inline bool formsView(const ViewProps& props) {
20-
return props.focusable;
20+
return props.focusable ||
21+
props.hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] ||
22+
props.hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave];
2123
}
2224

2325
} // namespace facebook::react::HostPlatformViewTraitsInitializer
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <react/renderer/graphics/Geometry.h>
11+
12+
namespace facebook::react {
13+
14+
/*
15+
* Describes a mouse enter/leave event.
16+
*/
17+
struct MouseEvent {
18+
/**
19+
* Pointer horizontal location in target view.
20+
*/
21+
Float clientX{0};
22+
23+
/**
24+
* Pointer vertical location in target view.
25+
*/
26+
Float clientY{0};
27+
28+
/**
29+
* Pointer horizontal location in window.
30+
*/
31+
Float screenX{0};
32+
33+
/**
34+
* Pointer vertical location in window.
35+
*/
36+
Float screenY{0};
37+
38+
/*
39+
* A flag indicating if the alt key is pressed.
40+
*/
41+
bool altKey{false};
42+
43+
/*
44+
* A flag indicating if the control key is pressed.
45+
*/
46+
bool ctrlKey{false};
47+
48+
/*
49+
* A flag indicating if the shift key is pressed.
50+
*/
51+
bool shiftKey{false};
52+
53+
/*
54+
* A flag indicating if the meta key is pressed.
55+
*/
56+
bool metaKey{false};
57+
};
58+
59+
} // namespace facebook::react

0 commit comments

Comments
 (0)