diff --git a/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js index f349a0a9b0a7..e494e151a019 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js @@ -44,6 +44,7 @@ const UIView = { mouseDownCanMoveWindow: true, enableFocusRing: true, focusable: true, + onAuxClick: true, onMouseEnter: true, onMouseLeave: true, onDoubleClick: true, diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js index d8281aaf7fb6..b495fe687720 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -135,6 +135,8 @@ type MouseEventProps = $ReadOnly<{ // Experimental/Work in Progress Pointer Event Callbacks (not yet ready for use) type PointerEventProps = $ReadOnly<{ + onAuxClick?: ?(event: PointerEvent) => void, + onAuxClickCapture?: ?(event: PointerEvent) => void, onClick?: ?(event: PointerEvent) => void, onClickCapture?: ?(event: PointerEvent) => void, onPointerEnter?: ?(event: PointerEvent) => void, diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js index 5c96ddeabeff..0cda0bf11cb1 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js @@ -89,6 +89,12 @@ const bubblingEventTypes = { }, // Experimental/Work in Progress Pointer Events (not yet ready for use) + topAuxClick: { + phasedRegistrationNames: { + captured: 'onAuxClickCapture', + bubbled: 'onAuxClick', + }, + }, topClick: { phasedRegistrationNames: { captured: 'onClickCapture', @@ -394,6 +400,8 @@ const validAttributesForEventProps = ConditionallyIgnoredEventHandlers({ onTouchCancel: true, // Pointer events + onAuxClick: true, + onAuxClickCapture: true, onClick: true, onClickCapture: true, onPointerUp: true, diff --git a/packages/react-native/Libraries/Pressability/Pressability.js b/packages/react-native/Libraries/Pressability/Pressability.js index 39609d8b9a24..b5d6fd9e4103 100644 --- a/packages/react-native/Libraries/Pressability/Pressability.js +++ b/packages/react-native/Libraries/Pressability/Pressability.js @@ -554,6 +554,14 @@ export default class Pressability { return; } + // [macOS Only fire onPress for primary (left) mouse button clicks. + // Non-primary buttons (right, middle) should not trigger onPress. + const button = event?.nativeEvent?.button; + if (button != null && button !== 0) { + return; + } + // macOS] + // for non-pointer click events (e.g. accessibility clicks), we should only dispatch when we're the "real" target // in particular, we shouldn't respond to clicks from nested pressables if (event?.currentTarget !== event?.target) { 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 1e31f460f8a9..032bb16ce505 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -2032,9 +2032,10 @@ - (BOOL)performDragOperation:(id )sender MouseEnter, MouseLeave, DoubleClick, + AuxClick, }; -- (void)emitMouseEvent:(MouseEventType)eventType +- (void)emitMouseEvent:(MouseEventType)eventType button:(int)button { if (!_eventEmitter) { return; @@ -2054,6 +2055,7 @@ - (void)emitMouseEvent:(MouseEventType)eventType .ctrlKey = static_cast(modifierFlags & NSEventModifierFlagControl), .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), + .button = button, }; switch (eventType) { @@ -2068,9 +2070,18 @@ - (void)emitMouseEvent:(MouseEventType)eventType case DoubleClick: _eventEmitter->onDoubleClick(mouseEvent); break; + + case AuxClick: + _eventEmitter->onAuxClick(mouseEvent); + break; } } +- (void)emitMouseEvent:(MouseEventType)eventType +{ + [self emitMouseEvent:eventType button:0]; +} + - (void)updateMouseOverIfNeeded { // When an enclosing scrollview is scrolled using the scrollWheel or trackpad, @@ -2191,6 +2202,16 @@ - (void)mouseUp:(NSEvent *)event [super mouseUp:event]; } } + +- (void)rightMouseUp:(NSEvent *)event +{ + BOOL hasAuxClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::AuxClick]; + if (hasAuxClickEventHandler) { + [self emitMouseEvent:AuxClick button:2]; + } else { + [super rightMouseUp:event]; + } +} #endif // macOS] - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point diff --git a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm index 8536d3284792..308c14de42d2 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm @@ -703,8 +703,12 @@ - (void)_dispatchActivePointers:(std::vector)activePointers event } case RCTPointerEventTypeEnd: { eventEmitter->onPointerUp(pointerEvent); - if (pointerEvent.isPrimary && pointerEvent.button == 0 && IsPointerWithinInitialTree(activePointer)) { - eventEmitter->onClick(std::move(pointerEvent)); + if (pointerEvent.isPrimary && pointerEvent.button == 0) { + if (IsPointerWithinInitialTree(activePointer)) { + eventEmitter->onClick(std::move(pointerEvent)); + } + } else if (IsPointerWithinInitialTree(activePointer)) { + eventEmitter->onAuxClick(std::move(pointerEvent)); } break; } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp index 6264d5fe50d6..3a390c6bd886 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp @@ -86,6 +86,10 @@ void TouchEventEmitter::onTouchCancel(TouchEvent event) const { "touchCancel", std::move(event), RawEvent::Category::ContinuousEnd); } +void TouchEventEmitter::onAuxClick(PointerEvent event) const { + dispatchPointerEvent("auxClick", std::move(event), RawEvent::Category::Discrete); +} + void TouchEventEmitter::onClick(PointerEvent event) const { dispatchPointerEvent("click", std::move(event), RawEvent::Category::Discrete); } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.h index e7dcfa7d84aa..60ce1dd8a424 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/TouchEventEmitter.h @@ -29,6 +29,7 @@ class TouchEventEmitter : public EventEmitter { void onTouchEnd(TouchEvent event) const; void onTouchCancel(TouchEvent event) const; + void onAuxClick(PointerEvent event) const; void onClick(PointerEvent event) const; void onPointerCancel(PointerEvent event) const; void onPointerDown(PointerEvent event) const; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp index 357ab6917ef5..6ed3d0d84274 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp @@ -63,6 +63,7 @@ static jsi::Object mouseEventPayload(jsi::Runtime& runtime, const MouseEvent& ev payload.setProperty(runtime, "ctrlKey", event.ctrlKey); payload.setProperty(runtime, "shiftKey", event.shiftKey); payload.setProperty(runtime, "metaKey", event.metaKey); + payload.setProperty(runtime, "button", event.button); return payload; }; @@ -84,6 +85,12 @@ void HostPlatformViewEventEmitter::onDoubleClick(const MouseEvent& mouseEvent) c }); } +void HostPlatformViewEventEmitter::onAuxClick(const MouseEvent& mouseEvent) const { + dispatchEvent("auxClick", [mouseEvent](jsi::Runtime& runtime) { + return mouseEventPayload(runtime, mouseEvent); + }); +} + #pragma mark - Drag and Drop Events jsi::Value HostPlatformViewEventEmitter::dataTransferPayload( diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h index c5587906a519..f0f4e912f66b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h @@ -34,6 +34,7 @@ class HostPlatformViewEventEmitter : public BaseViewEventEmitter { void onMouseEnter(MouseEvent const& mouseEvent) const; void onMouseLeave(MouseEvent const& mouseEvent) const; void onDoubleClick(MouseEvent const& mouseEvent) const; + void onAuxClick(MouseEvent const& mouseEvent) const; #pragma mark - Drag and Drop Events diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h index cefa27d007a3..a180e9c2ebd3 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h @@ -30,6 +30,7 @@ struct HostPlatformViewEvents { MouseEnter = 4, MouseLeave = 5, DoubleClick = 6, + AuxClick = 7, }; constexpr bool operator[](const Offset offset) const { @@ -74,6 +75,8 @@ static inline HostPlatformViewEvents convertRawProp( convertRawProp(context, rawProps, "onMouseLeave", sourceValue[Offset::MouseLeave], defaultValue[Offset::MouseLeave]); result[Offset::DoubleClick] = convertRawProp(context, rawProps, "onDoubleClick", sourceValue[Offset::DoubleClick], defaultValue[Offset::DoubleClick]); + result[Offset::AuxClick] = + convertRawProp(context, rawProps, "onAuxClick", sourceValue[Offset::AuxClick], defaultValue[Offset::AuxClick]); return result; } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h index 334506c93adc..a53210a409d4 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h @@ -54,6 +54,14 @@ struct MouseEvent { * A flag indicating if the meta key is pressed. */ bool metaKey{false}; + + /** + * The button number that was pressed when the mouse event was fired: + * 0 = primary button (usually the left button) + * 1 = auxiliary button (usually the middle/wheel button) + * 2 = secondary button (usually the right button) + */ + int button{0}; }; struct DataTransferFile { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h index 8ec8e1ab29be..290d2b4bc3ab 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h @@ -67,6 +67,8 @@ struct ViewEvents { PointerDownCapture = 35, PointerUp = 36, PointerUpCapture = 37, + AuxClick = 38, + AuxClickCapture = 39, }; constexpr bool operator[](const Offset offset) const { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h index 74dcc8f16420..e15c33572222 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/propsConversions.h @@ -830,6 +830,10 @@ static inline ViewEvents convertRawProp( "onPointerOutCapture", sourceValue[Offset::PointerOutCapture], defaultValue[Offset::PointerOutCapture]); + result[Offset::AuxClick] = + convertRawProp(context, rawProps, "onAuxClick", sourceValue[Offset::AuxClick], defaultValue[Offset::AuxClick]); + result[Offset::AuxClickCapture] = convertRawProp( + context, rawProps, "onAuxClickCapture", sourceValue[Offset::AuxClickCapture], defaultValue[Offset::AuxClickCapture]); result[Offset::Click] = convertRawProp( context, rawProps, diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index c02c6ddd74ff..999c3f8916a2 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -3676,6 +3676,8 @@ declare type PlatformType = | WindowsPlatform declare type PointerEvent = NativeSyntheticEvent declare type PointerEventProps = { + readonly onAuxClick?: (event: PointerEvent) => void + readonly onAuxClickCapture?: (event: PointerEvent) => void readonly onClick?: (event: PointerEvent) => void readonly onClickCapture?: (event: PointerEvent) => void readonly onGotPointerCapture?: (e: PointerEvent) => void diff --git a/packages/rn-tester/js/examples/Pressable/PressableExample.js b/packages/rn-tester/js/examples/Pressable/PressableExample.js index 47211b7c8283..a69ca61b4f32 100644 --- a/packages/rn-tester/js/examples/Pressable/PressableExample.js +++ b/packages/rn-tester/js/examples/Pressable/PressableExample.js @@ -131,6 +131,7 @@ function PressableFeedbackEvents() { onDragLeave={() => appendEvent('dragLeave')} onDrop={() => appendEvent('drop')} draggedTypes={'fileUrl'} + onAuxClick={() => appendEvent('auxClick')} onDoubleClick={() => appendEvent('doubleClick')} // macOS] onPress={() => appendEvent('press')}