diff --git a/ReactCommon/react/renderer/components/view/macOS/KeyEvent.h b/ReactCommon/react/renderer/components/view/macOS/KeyEvent.h new file mode 100644 index 00000000000000..b0eeebed85d4d3 --- /dev/null +++ b/ReactCommon/react/renderer/components/view/macOS/KeyEvent.h @@ -0,0 +1,107 @@ +/* + * 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 + +namespace facebook::react { + +/* + * Describes a request to handle a key input. + */ +struct HandledKey { + /** + * The key for the event aligned to https://www.w3.org/TR/uievents-key/. + */ + std::string key{}; + + /* + * A flag indicating if the alt key is pressed. + */ + std::optional altKey{}; + + /* + * A flag indicating if the control key is pressed. + */ + std::optional ctrlKey{}; + + /* + * A flag indicating if the shift key is pressed. + */ + std::optional shiftKey{}; + + /* + * A flag indicating if the meta key is pressed. + */ + std::optional metaKey{}; +}; + +inline static bool operator==(const HandledKey &lhs, const HandledKey &rhs) { + return lhs.key == rhs.key && lhs.altKey == rhs.altKey && lhs.ctrlKey == rhs.ctrlKey && + lhs.shiftKey == rhs.shiftKey && lhs.metaKey == rhs.metaKey; +} + +/** + * Key event emitted by handled key events. + */ +struct KeyEvent { + /** + * The key for the event aligned to https://www.w3.org/TR/uievents-key/. + */ + std::string key{}; + + /* + * 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}; + + /* + * A flag indicating if the caps lock key is pressed. + */ + bool capsLockKey{false}; + + /* + * A flag indicating if the key on the numeric pad is pressed. + */ + bool numericPadKey{false}; + + /* + * A flag indicating if the help key is pressed. + */ + bool helpKey{false}; + + /* + * A flag indicating if a function key is pressed. + */ + bool functionKey{false}; +}; + +inline static bool operator==(const KeyEvent &lhs, const HandledKey &rhs) { + return lhs.key == rhs.key && + (!rhs.altKey.has_value() || lhs.altKey == *rhs.altKey) && + (!rhs.ctrlKey.has_value() || lhs.ctrlKey == *rhs.ctrlKey) && + (!rhs.shiftKey.has_value() || lhs.shiftKey == *rhs.shiftKey) && + (!rhs.metaKey.has_value() || lhs.metaKey == *rhs.metaKey); +} + +} // namespace facebook::react diff --git a/ReactCommon/react/renderer/components/view/macOS/MacOSViewEventEmitter.cpp b/ReactCommon/react/renderer/components/view/macOS/MacOSViewEventEmitter.cpp new file mode 100644 index 00000000000000..274cf0d8e7941d --- /dev/null +++ b/ReactCommon/react/renderer/components/view/macOS/MacOSViewEventEmitter.cpp @@ -0,0 +1,69 @@ +/* + * 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. + */ + +#include "MacOSViewEventEmitter.h" + +namespace facebook::react { + +#pragma mark - Keyboard Events + +static jsi::Value keyEventPayload(jsi::Runtime &runtime, KeyEvent const &event) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "key", jsi::String::createFromUtf8(runtime, event.key)); + payload.setProperty(runtime, "ctrlKey", event.ctrlKey); + payload.setProperty(runtime, "shiftKey", event.shiftKey); + payload.setProperty(runtime, "altKey", event.altKey); + payload.setProperty(runtime, "metaKey", event.metaKey); + payload.setProperty(runtime, "capsLockKey", event.capsLockKey); + payload.setProperty(runtime, "numericPadKey", event.numericPadKey); + payload.setProperty(runtime, "helpKey", event.helpKey); + payload.setProperty(runtime, "functionKey", event.functionKey); + return payload; +}; + +void MacOSViewEventEmitter::onKeyDown(KeyEvent const &keyEvent) const { + dispatchEvent( + "keyDown", + [keyEvent](jsi::Runtime &runtime) { return keyEventPayload(runtime, keyEvent); }, + EventPriority::AsynchronousBatched); +} + +void MacOSViewEventEmitter::onKeyUp(KeyEvent const &keyEvent) const { + dispatchEvent( + "keyUp", + [keyEvent](jsi::Runtime &runtime) { return keyEventPayload(runtime, keyEvent); }, + EventPriority::AsynchronousBatched); +} + +static jsi::Value mouseEventPayload(jsi::Runtime &runtime, MouseEvent const &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 MacOSViewEventEmitter::onMouseEnter(MouseEvent const &mouseEvent) const { + dispatchEvent( + "mouseEnter", + [mouseEvent](jsi::Runtime &runtime) { return mouseEventPayload(runtime, mouseEvent); }, + EventPriority::AsynchronousBatched); +} + +void MacOSViewEventEmitter::onMouseLeave(MouseEvent const &mouseEvent) const { + dispatchEvent( + "mouseLeave", + [mouseEvent](jsi::Runtime &runtime) { return mouseEventPayload(runtime, mouseEvent); }, + EventPriority::AsynchronousBatched); +} + +} // namespace facebook::react diff --git a/ReactCommon/react/renderer/components/view/macOS/MacOSViewEventEmitter.h b/ReactCommon/react/renderer/components/view/macOS/MacOSViewEventEmitter.h new file mode 100644 index 00000000000000..58b71428e44dfc --- /dev/null +++ b/ReactCommon/react/renderer/components/view/macOS/MacOSViewEventEmitter.h @@ -0,0 +1,31 @@ +/* + * 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 +#include +#include + +namespace facebook::react { + +class MacOSViewEventEmitter : public TouchEventEmitter { + public: + using TouchEventEmitter::TouchEventEmitter; + +#pragma mark - Keyboard Events + + 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 diff --git a/ReactCommon/react/renderer/components/view/macOS/MacOSViewProps.cpp b/ReactCommon/react/renderer/components/view/macOS/MacOSViewProps.cpp new file mode 100644 index 00000000000000..288a1315ccb19b --- /dev/null +++ b/ReactCommon/react/renderer/components/view/macOS/MacOSViewProps.cpp @@ -0,0 +1,69 @@ +/* + * 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. + */ + +#include "MacOSViewProps.h" + +#include +#include +#include + +namespace facebook::react { + +MacOSViewProps::MacOSViewProps( + const PropsParserContext &context, + const MacOSViewProps &sourceProps, + const RawProps &rawProps, + bool shouldSetRawProps) + : macOSViewEvents( + CoreFeatures::enablePropIteratorSetter ? sourceProps.macOSViewEvents + : convertRawProp(context, rawProps, sourceProps.macOSViewEvents, {})), + focusable( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.focusable + : convertRawProp(context, rawProps, "focusable", sourceProps.focusable, {})), + enableFocusRing( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.enableFocusRing + : convertRawProp(context, rawProps, "enableFocusRing", sourceProps.enableFocusRing, true)), + validKeysDown( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.validKeysDown + : convertRawProp(context, rawProps, "validKeysDown", sourceProps.validKeysDown, {})), + validKeysUp( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.validKeysUp + : convertRawProp(context, rawProps, "validKeysUp", sourceProps.validKeysUp, {})){}; + +#define VIEW_EVENT_CASE_MACOS(eventType, eventString) \ + case CONSTEXPR_RAW_PROPS_KEY_HASH(eventString): { \ + MacOSViewEvents defaultViewEvents{}; \ + bool res = defaultViewEvents[eventType]; \ + if (value.hasValue()) { \ + fromRawValue(context, value, res); \ + } \ + macOSViewEvents[eventType] = res; \ + return; \ + } + +void MacOSViewProps::setProp( + const PropsParserContext &context, + RawPropsPropNameHash hash, + const char *propName, + RawValue const &value) { + switch (hash) { + VIEW_EVENT_CASE_MACOS(MacOSViewEvents::Offset::KeyDown, "onKeyDown"); + VIEW_EVENT_CASE_MACOS(MacOSViewEvents::Offset::KeyUp, "onKeyUp"); + VIEW_EVENT_CASE_MACOS(MacOSViewEvents::Offset::MouseEnter, "onMouseEnter"); + VIEW_EVENT_CASE_MACOS(MacOSViewEvents::Offset::MouseLeave, "onMouseLeave"); + RAW_SET_PROP_SWITCH_CASE_BASIC(focusable, false); + RAW_SET_PROP_SWITCH_CASE_BASIC(enableFocusRing, true); + RAW_SET_PROP_SWITCH_CASE_BASIC(validKeysDown, {}); + RAW_SET_PROP_SWITCH_CASE_BASIC(validKeysUp, {}); + } +} + +} // namespace facebook::react diff --git a/ReactCommon/react/renderer/components/view/macOS/MacOSViewProps.h b/ReactCommon/react/renderer/components/view/macOS/MacOSViewProps.h new file mode 100644 index 00000000000000..cff1a6a706fe2d --- /dev/null +++ b/ReactCommon/react/renderer/components/view/macOS/MacOSViewProps.h @@ -0,0 +1,42 @@ +/* + * 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 +#include +#include +#include + +namespace facebook::react { + +class MacOSViewProps { + public: + MacOSViewProps() = default; + MacOSViewProps( + const PropsParserContext &context, + const MacOSViewProps &sourceProps, + const RawProps &rawProps, + bool shouldSetRawProps = true); + + void + setProp( + const PropsParserContext &context, + RawPropsPropNameHash hash, + const char *propName, + RawValue const &value); + + MacOSViewEvents macOSViewEvents{}; + + bool focusable{false}; + bool enableFocusRing{true}; + + std::optional> validKeysDown{}; + std::optional> validKeysUp{}; +}; + +} // namespace facebook::react diff --git a/ReactCommon/react/renderer/components/view/macOS/MouseEvent.h b/ReactCommon/react/renderer/components/view/macOS/MouseEvent.h new file mode 100644 index 00000000000000..457083eec2fc4e --- /dev/null +++ b/ReactCommon/react/renderer/components/view/macOS/MouseEvent.h @@ -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 + +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 diff --git a/ReactCommon/react/renderer/components/view/macOS/conversions.h b/ReactCommon/react/renderer/components/view/macOS/conversions.h new file mode 100644 index 00000000000000..79f9e108b39f20 --- /dev/null +++ b/ReactCommon/react/renderer/components/view/macOS/conversions.h @@ -0,0 +1,63 @@ +/* + * 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 +#include +#include +#include +#include + +#include +#include + +namespace facebook::react { + +static inline MacOSViewEvents convertRawProp( + const PropsParserContext &context, + const RawProps &rawProps, + const MacOSViewEvents &sourceValue, + const MacOSViewEvents &defaultValue) { + MacOSViewEvents result{}; + using Offset = MacOSViewEvents::Offset; + + 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]); + + 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; +} + +inline void fromRawValue(const PropsParserContext &context, const RawValue &value, HandledKey &result) { + if (value.hasType>()) { + auto map = static_cast>(value); + for (const auto &pair : map) { + if (pair.first == "key") { + result.key = static_cast(pair.second); + } else if (pair.first == "altKey") { + result.altKey = static_cast(pair.second); + } else if (pair.first == "ctrlKey") { + result.ctrlKey = static_cast(pair.second); + } else if (pair.first == "shiftKey") { + result.shiftKey = static_cast(pair.second); + } else if (pair.first == "metaKey") { + result.metaKey = static_cast(pair.second); + } + } + } else if (value.hasType()) { + result.key = (std::string)value; + } +} + +} // namespace facebook::react diff --git a/ReactCommon/react/renderer/components/view/macOS/primitives.h b/ReactCommon/react/renderer/components/view/macOS/primitives.h new file mode 100644 index 00000000000000..855ffd6abd5206 --- /dev/null +++ b/ReactCommon/react/renderer/components/view/macOS/primitives.h @@ -0,0 +1,42 @@ +/* + * 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 + +namespace facebook::react { + +struct MacOSViewEvents { + std::bitset<8> bits{}; + + enum class Offset : uint8_t { + KeyDown = 1, + KeyUp = 2, + + MouseEnter = 3, + MouseLeave = 4, + }; + + constexpr bool operator[](const Offset offset) const { + return bits[static_cast(offset)]; + } + + std::bitset<8>::reference operator[](const Offset offset) { + return bits[static_cast(offset)]; + } +}; + +inline static bool operator==(MacOSViewEvents const &lhs, MacOSViewEvents const &rhs) { + return lhs.bits == rhs.bits; +} + +inline static bool operator!=(MacOSViewEvents const &lhs, MacOSViewEvents const &rhs) { + return lhs.bits != rhs.bits; +} + +} // namespace facebook::react diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 78e80e67d7a1e6..756f5e8acc46f1 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -20,6 +20,10 @@ #import #import #import +#import // [macOS] +#if TARGET_OS_OSX // [macOS +#import // [macOS] +#endif // macOS] #import #import #import @@ -45,6 +49,9 @@ @implementation RCTViewComponentView { BOOL _needsInvalidateLayer; BOOL _isJSResponder; BOOL _removeClippedSubviews; + BOOL _hasMouseOver; // [macOS] + BOOL _hasClipViewBoundsObserver; // [macOS] + NSTrackingArea *_trackingArea; // [macOS] NSMutableArray *_reactSubviews; // [macOS] NSSet *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN; RCTPlatformView *_containerView; // [macOS] @@ -579,6 +586,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & needsInvalidateLayer = YES; } +#if TARGET_OS_OSX // [macOS + // `focusable` + self.focusable = (bool)newViewProps.focusable; + // `enableFocusRing` + self.enableFocusRing = (bool)newViewProps.enableFocusRing; +#endif // macOS] + _needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer; _props = std::static_pointer_cast(props); @@ -641,6 +655,9 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask _needsInvalidateLayer = NO; [self invalidateLayer]; + + [self updateTrackingAreas]; + [self updateClipViewBoundsObserverIfNeeded]; } - (void)prepareForRecycle @@ -1510,6 +1527,210 @@ - (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)acti } } +#if TARGET_OS_OSX // [macOS + +#pragma mark - Keyboard Events + +- (BOOL)handleKeyboardEvent:(NSEvent *)event { + BOOL keyDown = event.type == NSEventTypeKeyDown; + BOOL hasHandler = keyDown ? _props->macOSViewEvents[facebook::react::MacOSViewEvents::Offset::KeyDown] + : _props->macOSViewEvents[facebook::react::MacOSViewEvents::Offset::KeyUp]; + if (hasHandler) { + auto validKeys = keyDown ? _props->validKeysDown : _props->validKeysUp; + + // If the view is focusable and the component didn't explicity set the validKeysDown or validKeysUp, + // allow enter/return and spacebar key events to mimic the behavior of native controls. + if (self.focusable && !validKeys.has_value()) { + validKeys = { { .key = "Enter" }, { .key = " " } }; + } + + // Convert the event to a KeyEvent + NSEventModifierFlags modifierFlags = event.modifierFlags; + facebook::react::KeyEvent keyEvent = { + .key = [[RCTViewKeyboardEvent keyFromEvent:event] UTF8String], + .altKey = static_cast(modifierFlags & NSEventModifierFlagOption), + .ctrlKey = static_cast(modifierFlags & NSEventModifierFlagControl), + .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), + .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), + .capsLockKey = static_cast(modifierFlags & NSEventModifierFlagCapsLock), + .numericPadKey = static_cast(modifierFlags & NSEventModifierFlagNumericPad), + .helpKey = static_cast(modifierFlags & NSEventModifierFlagHelp), + .functionKey = static_cast(modifierFlags & NSEventModifierFlagFunction), + }; + + BOOL shouldBlock = NO; + for (auto const &validKey : *validKeys) { + if (keyEvent == validKey) { + shouldBlock = YES; + break; + } + } + + if (_eventEmitter && shouldBlock) { + if (keyDown) { + _eventEmitter->onKeyDown(keyEvent); + } else { + _eventEmitter->onKeyUp(keyEvent); + } + return YES; + } + } + + return NO; +} + +- (void)keyDown:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyDown:event]; + } +} + +- (void)keyUp:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyUp:event]; + } +} + + +#pragma mark - Mouse Events + +- (void)sendMouseEvent:(BOOL)isMouseOver { + 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(modifierFlags & NSEventModifierFlagOption), + .ctrlKey = static_cast(modifierFlags & NSEventModifierFlagControl), + .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), + .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), + }; + + if (isMouseOver) { + _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.0 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 sendMouseEvent:hasMouseOver]; + } +} + +- (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->macOSViewEvents[facebook::react::MacOSViewEvents::Offset::MouseEnter] || + _props->macOSViewEvents[facebook::react::MacOSViewEvents::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 +{ + if (_trackingArea) { + [self removeTrackingArea:_trackingArea]; + } + + if ( + _props->macOSViewEvents[facebook::react::MacOSViewEvents::Offset::MouseEnter] || + _props->macOSViewEvents[facebook::react::MacOSViewEvents::Offset::MouseLeave] + ) { + _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 sendMouseEvent:_hasMouseOver]; +} + +- (void)mouseExited:(NSEvent *)event +{ + if (!_hasMouseOver) { + return; + } + + _hasMouseOver = NO; + [self sendMouseEvent:_hasMouseOver]; +} +#endif // macOS] + - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point { return _eventEmitter; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.h index 584bc01239ce44..1bd62d95a09703 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.h @@ -13,13 +13,14 @@ #include #include -#include "TouchEventEmitter.h" +#include "HostPlatformViewEventEmitter.h" namespace facebook::react { -class BaseViewEventEmitter : public TouchEventEmitter { +class BaseViewEventEmitter : public HostPlatformViewEventEmitter { public: - using TouchEventEmitter::TouchEventEmitter; + using HostPlatformViewEventEmitter::HostPlatformViewEventEmitter; + #pragma mark - Accessibility diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp index 8214e2eb67d925..c03b49e0249592 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -59,6 +59,7 @@ BaseViewProps::BaseViewProps( const std::function& filterObjectKeys) : YogaStylableProps(context, sourceProps, rawProps, filterObjectKeys), AccessibilityProps(context, sourceProps, rawProps), + HostPlatformViewProps(context, sourceProps, rawProps, shouldSetRawProps), opacity( ReactNativeFeatureFlags::enableCppPropsIteratorSetter() ? sourceProps.opacity @@ -369,6 +370,7 @@ void BaseViewProps::setProp( // reuse the same values. YogaStylableProps::setProp(context, hash, propName, value); AccessibilityProps::setProp(context, hash, propName, value); + HostPlatformViewProps::setProp(context, hash, propName, value); static auto defaults = BaseViewProps{}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h index 2165952d3fd823..eec3fa6fa2b54f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -25,8 +26,9 @@ namespace facebook::react { -class BaseViewProps : public YogaStylableProps, public AccessibilityProps { +class BaseViewProps : public YogaStylableProps, public AccessibilityProps, public HostPlatformViewProps { public: + using SharedViewProps = std::shared_ptr; BaseViewProps() = default; BaseViewProps( const PropsParserContext& context, diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/HostPlatformViewEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/view/HostPlatformViewEventEmitter.h new file mode 100644 index 00000000000000..ba323a7928efdd --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/HostPlatformViewEventEmitter.h @@ -0,0 +1,26 @@ +/* + * 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 + +#if TARGET_OS_OSX + +#include + +namespace facebook::react { +using HostPlatformViewEventEmitter = MacOSViewEventEmitter; +} // namespace facebook::react + +#else + +#include + +namespace facebook::react { + using HostPlatformViewEventEmitter = TouchEventEmitter; +} // namespace facebook::react + +#endif diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/HostPlatformViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/HostPlatformViewProps.h new file mode 100644 index 00000000000000..36f00723ffcf30 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/HostPlatformViewProps.h @@ -0,0 +1,38 @@ +/* + * 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 + +#if !TARGET_OS_OSX // [macOS +#include +#include +#else // [macOS +#include +#endif // macOS] + +namespace facebook::react { +#if !TARGET_OS_OSX // [macOS] +class HostPlatformViewProps { + public: + HostPlatformViewProps() = default; + HostPlatformViewProps( + const PropsParserContext &context, + const HostPlatformViewProps &sourceProps, + const RawProps &rawProps, + bool shouldSetRawProps = true) {} + + void + setProp( + const PropsParserContext &context, + RawPropsPropNameHash hash, + const char *propName, + RawValue const &value) {} +}; +#else // [macOS + using HostPlatformViewProps = MacOSViewProps; +#endif // macOS] +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index bcc4ef045fcddf..1fedadcc37842a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -62,6 +62,12 @@ void ViewShadowNode::initialize() noexcept { !viewProps.filter.empty() || viewProps.mixBlendMode != BlendMode::Normal || viewProps.isolation == Isolation::Isolate || +#if TARGET_OS_OSX // [macOS + viewProps.focusable || + viewProps.enableFocusRing || + viewProps.macOSViewEvents[MacOSViewEvents::Offset::MouseEnter] || + viewProps.macOSViewEvents[MacOSViewEvents::Offset::MouseLeave] || +#endif // macOS] HostPlatformViewTraitsInitializer::formsStackingContext(viewProps); bool formsView = formsStackingContext ||