Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ @implementation RCTViewComponentView {
BOOL _needsInvalidateLayer;
BOOL _isJSResponder;
BOOL _removeClippedSubviews;
#if TARGET_OS_OSX // [macOS
BOOL _hasMouseOver;
BOOL _hasClipViewBoundsObserver;
NSTrackingArea *_trackingArea;
#endif // macOS]
NSMutableArray<RCTUIView *> *_reactSubviews; // [macOS]
NSSet<NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
RCTPlatformView *_containerView; // [macOS]
Expand Down Expand Up @@ -645,6 +650,11 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask

_needsInvalidateLayer = NO;
[self invalidateLayer];

#if TARGET_OS_OSX // [macOS
[self updateTrackingAreas];
[self updateClipViewBoundsObserverIfNeeded];
#endif // macOS]
}

- (void)prepareForRecycle
Expand Down Expand Up @@ -1623,20 +1633,22 @@ - (BOOL)handleKeyboardEvent:(NSEvent *)event {
.functionKey = static_cast<bool>(modifierFlags & NSEventModifierFlagFunction),
};

// Emit the event to JS only once. By default, events, will bubble up the respnder chain
// when we call super, so let's emit the event only at the first responder. It would be
// simpler to check `if (self == self.window.firstResponder), however, that does not account
// for cases like TextInputComponentView, where the first responder may be a subview.
static const char kRCTViewKeyboardEventEmittedKey = 0;
NSNumber *emitted = objc_getAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey);
BOOL alreadyEmitted = [emitted boolValue];
if (!alreadyEmitted) {
if (event.type == NSEventTypeKeyDown) {
_eventEmitter->onKeyDown(keyEvent);
} else {
_eventEmitter->onKeyUp(keyEvent);
if (_eventEmitter) {
// Emit the event to JS only once. By default, events, will bubble up the respnder chain
// when we call super, so let's emit the event only at the first responder. It would be
// simpler to check `if (self == self.window.firstResponder), however, that does not account
// for cases like TextInputComponentView, where the first responder may be a subview.
static const char kRCTViewKeyboardEventEmittedKey = 0;
NSNumber *emitted = objc_getAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey);
BOOL alreadyEmitted = [emitted boolValue];
if (!alreadyEmitted) {
if (event.type == NSEventTypeKeyDown) {
_eventEmitter->onKeyDown(keyEvent);
} else {
_eventEmitter->onKeyUp(keyEvent);
}
objc_setAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
objc_setAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// If keyDownEvents or keyUpEvents specifies the event, block native handling of the event
Expand All @@ -1656,6 +1668,147 @@ - (void)keyUp:(NSEvent *)event {
}
}


#pragma mark - Mouse Events

- (void)emitMouseEvent {
if (!_eventEmitter) {
return;
}

NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream;
NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];

NSEventModifierFlags modifierFlags = self.window.currentEvent.modifierFlags;

MouseEvent mouseEvent = {
.clientX = locationInView.x,
.clientY = locationInView.y,
.screenX = locationInWindow.x,
.screenY = locationInWindow.y,
.altKey = static_cast<bool>(modifierFlags & NSEventModifierFlagOption),
.ctrlKey = static_cast<bool>(modifierFlags & NSEventModifierFlagControl),
.shiftKey = static_cast<bool>(modifierFlags & NSEventModifierFlagShift),
.metaKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCommand),
};

if (_hasMouseOver) {
_eventEmitter->onMouseEnter(mouseEvent);
} else {
_eventEmitter->onMouseLeave(mouseEvent);
}
}

- (void)updateMouseOverIfNeeded
{
// When an enclosing scrollview is scrolled using the scrollWheel or trackpad,
// the mouseExited: event does not get called on the view where mouseEntered: was previously called.
// This creates an unnatural pairing of mouse enter and exit events and can cause problems.
// We therefore explicitly check for this here and handle them by calling the appropriate callbacks.

BOOL hasMouseOver = _hasMouseOver;
NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream;
NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];
BOOL insideBounds = NSPointInRect(locationInView, self.visibleRect);

// On macOS 14+ visibleRect can be larger than the view bounds
insideBounds &= NSPointInRect(locationInView, self.bounds);

if (hasMouseOver && !insideBounds) {
hasMouseOver = NO;
} else if (!hasMouseOver && insideBounds) {
// The window's frame view must be used for hit testing against `locationInWindow`
NSView *hitView = [self.window.contentView.superview hitTest:locationInWindow];
hasMouseOver = [hitView isDescendantOf:self];
}

if (hasMouseOver != _hasMouseOver) {
_hasMouseOver = hasMouseOver;
[self emitMouseEvent];
}
}

- (void)updateClipViewBoundsObserverIfNeeded
{
// Subscribe to view bounds changed notification so that the view can be notified when a
// scroll event occurs either due to trackpad/gesture based scrolling or a scrollwheel event
// both of which would not cause the mouseExited to be invoked.

NSClipView *clipView = self.window ? self.enclosingScrollView.contentView : nil;

BOOL hasMouseEventHandler =
_props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] ||
_props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave];

if (_hasClipViewBoundsObserver && (!clipView || !hasMouseEventHandler)) {
_hasClipViewBoundsObserver = NO;
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSViewBoundsDidChangeNotification
object:nil];
} else if (!_hasClipViewBoundsObserver && clipView && hasMouseEventHandler) {
_hasClipViewBoundsObserver = YES;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateMouseOverIfNeeded)
name:NSViewBoundsDidChangeNotification
object:clipView];
[self updateMouseOverIfNeeded];
}
}

- (void)viewDidMoveToWindow
{
[self updateClipViewBoundsObserverIfNeeded];
[super viewDidMoveToWindow];
}

- (void)updateTrackingAreas
{
BOOL hasMouseEventHandler =
_props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] ||
_props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave];
BOOL wouldRecreateIdenticalTrackingArea =
hasMouseEventHandler && _trackingArea && NSEqualRects(self.bounds, [_trackingArea rect]);

if (!wouldRecreateIdenticalTrackingArea) {
[self removeTrackingArea:_trackingArea];
if (hasMouseEventHandler) {
_trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds
options:NSTrackingActiveAlways|NSTrackingMouseEnteredAndExited
owner:self
userInfo:nil];
[self addTrackingArea:_trackingArea];
[self updateMouseOverIfNeeded];
}
}

[super updateTrackingAreas];
}

- (void)mouseEntered:(NSEvent *)event
{
if (_hasMouseOver) {
return;
}

// The window's frame view must be used for hit testing against `locationInWindow`
NSView *hitView = [self.window.contentView.superview hitTest:event.locationInWindow];
if (![hitView isDescendantOf:self]) {
return;
}

_hasMouseOver = YES;
[self emitMouseEvent];
}

- (void)mouseExited:(NSEvent *)event
{
if (!_hasMouseOver) {
return;
}

_hasMouseOver = NO;
[self emitMouseEvent];
}
#endif // macOS]

- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,31 @@ void HostPlatformViewEventEmitter::onKeyUp(const KeyEvent& keyEvent) const {
});
}

#pragma mark - Mouse Events

static jsi::Value mouseEventPayload(jsi::Runtime& runtime, const MouseEvent& event) {
auto payload = jsi::Object(runtime);
payload.setProperty(runtime, "clientX", event.clientX);
payload.setProperty(runtime, "clientY", event.clientY);
payload.setProperty(runtime, "screenX", event.screenX);
payload.setProperty(runtime, "screenY", event.screenY);
payload.setProperty(runtime, "altKey", event.altKey);
payload.setProperty(runtime, "ctrlKey", event.ctrlKey);
payload.setProperty(runtime, "shiftKey", event.shiftKey);
payload.setProperty(runtime, "metaKey", event.metaKey);
return payload;
};

void HostPlatformViewEventEmitter::onMouseEnter(const MouseEvent& mouseEvent) const {
dispatchEvent("mouseEnter", [mouseEvent](jsi::Runtime &runtime) {
return mouseEventPayload(runtime, mouseEvent);
});
}

void HostPlatformViewEventEmitter::onMouseLeave(const MouseEvent& mouseEvent) const {
dispatchEvent("mouseLeave", [mouseEvent](jsi::Runtime &runtime) {
return mouseEventPayload(runtime, mouseEvent);
});
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#include <react/renderer/components/view/BaseViewEventEmitter.h>
#include <react/renderer/components/view/KeyEvent.h>
#include <react/renderer/components/view/MouseEvent.h>

namespace facebook::react {

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

#pragma mark - Keyboard Events

void onKeyDown(KeyEvent const &keyEvent) const;
void onKeyUp(KeyEvent const &keyEvent) const;
void onKeyDown(KeyEvent const& keyEvent) const;
void onKeyUp(KeyEvent const& keyEvent) const;

#pragma mark - Mouse Events

void onMouseEnter(MouseEvent const& mouseEvent) const;
void onMouseLeave(MouseEvent const& mouseEvent) const;
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ struct HostPlatformViewEvents {
// Keyboard Events
KeyDown = 2,
KeyUp = 3,

// Mouse Events
MouseEnter = 4,
MouseLeave = 5,
};

constexpr bool operator[](const Offset offset) const {
Expand Down Expand Up @@ -57,10 +61,16 @@ static inline HostPlatformViewEvents convertRawProp(
convertRawProp(context, rawProps, "onFocus", sourceValue[Offset::Focus], defaultValue[Offset::Focus]);
result[Offset::Blur] =
convertRawProp(context, rawProps, "onBlur", sourceValue[Offset::Blur], defaultValue[Offset::Blur]);
// Keyboard Events
result[Offset::KeyDown] =
convertRawProp(context, rawProps, "onKeyDown", sourceValue[Offset::KeyDown], defaultValue[Offset::KeyDown]);
result[Offset::KeyUp] =
convertRawProp(context, rawProps, "onKeyUp", sourceValue[Offset::KeyUp], defaultValue[Offset::KeyUp]);
// Mouse Events
result[Offset::MouseEnter] =
convertRawProp(context, rawProps, "onMouseEnter", sourceValue[Offset::MouseEnter], defaultValue[Offset::MouseEnter]);
result[Offset::MouseLeave] =
convertRawProp(context, rawProps, "onMouseLeave", sourceValue[Offset::MouseLeave], defaultValue[Offset::MouseLeave]);

return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ void HostPlatformViewProps::setProp(
VIEW_EVENT_CASE_MACOS(Blur);
VIEW_EVENT_CASE_MACOS(KeyDown);
VIEW_EVENT_CASE_MACOS(KeyUp);
VIEW_EVENT_CASE_MACOS(MouseEnter);
VIEW_EVENT_CASE_MACOS(MouseLeave);
RAW_SET_PROP_SWITCH_CASE_BASIC(focusable);
RAW_SET_PROP_SWITCH_CASE_BASIC(enableFocusRing);
RAW_SET_PROP_SWITCH_CASE_BASIC(keyDownEvents);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ inline bool formsStackingContext(const ViewProps& props) {
}

inline bool formsView(const ViewProps& props) {
return props.focusable;
return props.focusable ||
props.hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] ||
props.hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave];
}

} // namespace facebook::react::HostPlatformViewTraitsInitializer
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#include <react/renderer/graphics/Geometry.h>

namespace facebook::react {

/*
* Describes a mouse enter/leave event.
*/
struct MouseEvent {
/**
* Pointer horizontal location in target view.
*/
Float clientX{0};

/**
* Pointer vertical location in target view.
*/
Float clientY{0};

/**
* Pointer horizontal location in window.
*/
Float screenX{0};

/**
* Pointer vertical location in window.
*/
Float screenY{0};

/*
* A flag indicating if the alt key is pressed.
*/
bool altKey{false};

/*
* A flag indicating if the control key is pressed.
*/
bool ctrlKey{false};

/*
* A flag indicating if the shift key is pressed.
*/
bool shiftKey{false};

/*
* A flag indicating if the meta key is pressed.
*/
bool metaKey{false};
};

} // namespace facebook::react
Loading