From 0c56c095543ab254baebf7e21527625ee248edf8 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Thu, 2 Apr 2026 13:42:10 -0700 Subject: [PATCH 1/2] Add onAuxClick event for handling right-click on macOS Implements onAuxClick following the same pattern as onDoubleClick, wired end-to-end from native rightMouseUp: through C++ event emitters to JS. Also adds a `button` field to MouseEvent and filters non-primary button clicks from triggering onPress in Pressability. Inspired by microsoft/react-native-windows#15920. Co-Authored-By: Claude Opus 4.6 --- .../View/ReactNativeViewAttributes.js | 1 + .../Components/View/ViewPropTypes.d.ts | 1 + .../Components/View/ViewPropTypes.js | 1 + .../NativeComponent/BaseViewConfig.macos.js | 4 ++++ .../Libraries/Pressability/Pressability.js | 8 +++++++ .../View/RCTViewComponentView.mm | 23 ++++++++++++++++++- .../view/HostPlatformViewEventEmitter.cpp | 7 ++++++ .../view/HostPlatformViewEventEmitter.h | 1 + .../components/view/HostPlatformViewEvents.h | 3 +++ .../renderer/components/view/MouseEvent.h | 8 +++++++ .../js/examples/Pressable/PressableExample.js | 1 + 11 files changed, 57 insertions(+), 1 deletion(-) 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.d.ts b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts index 2433689fa8ac..61ce9194bd5e 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts @@ -139,6 +139,7 @@ export interface ViewPropsMacOS { enableFocusRing?: boolean | undefined; onMouseEnter?: ((event: MouseEvent) => void) | undefined; onMouseLeave?: ((event: MouseEvent) => void) | undefined; + onAuxClick?: ((event: MouseEvent) => void) | undefined; onDoubleClick?: ((event: MouseEvent) => void) | undefined; onDragEnter?: ((event: DragEvent) => void) | undefined; onDragLeave?: ((event: DragEvent) => void) | undefined; diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js index d8281aaf7fb6..ae8abec85856 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -130,6 +130,7 @@ export type KeyboardEventProps = $ReadOnly<{| type MouseEventProps = $ReadOnly<{ onMouseEnter?: ?(event: MouseEvent) => void, onMouseLeave?: ?(event: MouseEvent) => void, + onAuxClick?: ?(event: MouseEvent) => void, // [macOS] onDoubleClick?: ?(event: MouseEvent) => void, // [macOS] }>; diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js index bb766656a917..75ef6b960d5e 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js @@ -34,6 +34,9 @@ const bubblingEventTypes = { const directEventTypes = { ...PlatformBaseViewConfigIos.directEventTypes, + topAuxClick: { + registrationName: 'onAuxClick', + }, topDoubleClick: { registrationName: 'onDoubleClick', }, @@ -70,6 +73,7 @@ const validAttributesForNonEventProps = { // Props for bubbling and direct events const validAttributesForEventProps = ConditionallyIgnoredEventHandlers({ + onAuxClick: true, onBlur: true, onDoubleClick: true, onDragEnter: 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/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/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')} From a750192be77a4982aee5a8b821490ba2be3df218 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Thu, 2 Apr 2026 14:20:23 -0700 Subject: [PATCH 2/2] Align onAuxClick with upstream pointer event system Port the shared changes from facebook/react-native#56298: - Add onAuxClick/onAuxClickCapture to TouchEventEmitter (shared C++) - Add AuxClick/AuxClickCapture to ViewEvents in primitives.h - Add prop conversions in propsConversions.h - Register topAuxClick as a bubbling event in BaseViewConfig.ios.js - Add dispatch logic in RCTSurfacePointerHandler.mm (iOS path) - Type onAuxClick as PointerEvent in PointerEventProps (Flow + TS) Remove redundant macOS-specific JS registrations since macOS now inherits the bubbling event from the iOS base config. Co-Authored-By: Claude Opus 4.6 --- .../Libraries/Components/View/ViewPropTypes.d.ts | 1 - .../Libraries/Components/View/ViewPropTypes.js | 3 ++- .../Libraries/NativeComponent/BaseViewConfig.ios.js | 8 ++++++++ .../Libraries/NativeComponent/BaseViewConfig.macos.js | 4 ---- .../react-native/React/Fabric/RCTSurfacePointerHandler.mm | 8 ++++++-- .../react/renderer/components/view/TouchEventEmitter.cpp | 4 ++++ .../react/renderer/components/view/TouchEventEmitter.h | 1 + .../react/renderer/components/view/primitives.h | 2 ++ .../react/renderer/components/view/propsConversions.h | 4 ++++ packages/react-native/ReactNativeApi.d.ts | 2 ++ 10 files changed, 29 insertions(+), 8 deletions(-) diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts index 61ce9194bd5e..2433689fa8ac 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts @@ -139,7 +139,6 @@ export interface ViewPropsMacOS { enableFocusRing?: boolean | undefined; onMouseEnter?: ((event: MouseEvent) => void) | undefined; onMouseLeave?: ((event: MouseEvent) => void) | undefined; - onAuxClick?: ((event: MouseEvent) => void) | undefined; onDoubleClick?: ((event: MouseEvent) => void) | undefined; onDragEnter?: ((event: DragEvent) => void) | undefined; onDragLeave?: ((event: DragEvent) => void) | undefined; diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js index ae8abec85856..b495fe687720 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -130,12 +130,13 @@ export type KeyboardEventProps = $ReadOnly<{| type MouseEventProps = $ReadOnly<{ onMouseEnter?: ?(event: MouseEvent) => void, onMouseLeave?: ?(event: MouseEvent) => void, - onAuxClick?: ?(event: MouseEvent) => void, // [macOS] onDoubleClick?: ?(event: MouseEvent) => void, // [macOS] }>; // 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/NativeComponent/BaseViewConfig.macos.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js index 75ef6b960d5e..bb766656a917 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js @@ -34,9 +34,6 @@ const bubblingEventTypes = { const directEventTypes = { ...PlatformBaseViewConfigIos.directEventTypes, - topAuxClick: { - registrationName: 'onAuxClick', - }, topDoubleClick: { registrationName: 'onDoubleClick', }, @@ -73,7 +70,6 @@ const validAttributesForNonEventProps = { // Props for bubbling and direct events const validAttributesForEventProps = ConditionallyIgnoredEventHandlers({ - onAuxClick: true, onBlur: true, onDoubleClick: true, onDragEnter: true, 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/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