From 0c56c095543ab254baebf7e21527625ee248edf8 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Thu, 2 Apr 2026 13:42:10 -0700 Subject: [PATCH 1/5] 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/5] 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 From 4218d5183be6442817c7a3ddba9d0fb64f84e9b3 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 8 Apr 2026 11:58:36 -0700 Subject: [PATCH 3/5] Filter non-primary button from onPress/onLongPress in responder path Match react-native-windows approach: add _isDefaultPressButton() helper and use it in all three press paths (onClick, LONG_PRESS_DETECTED, and RESPONDER_RELEASE). The button property is already forwarded from native macOS touch events via BaseTouch. Co-Authored-By: Claude Opus 4.6 --- .../Libraries/Pressability/Pressability.js | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/react-native/Libraries/Pressability/Pressability.js b/packages/react-native/Libraries/Pressability/Pressability.js index b5d6fd9e4103..9a8c5e98d514 100644 --- a/packages/react-native/Libraries/Pressability/Pressability.js +++ b/packages/react-native/Libraries/Pressability/Pressability.js @@ -410,6 +410,16 @@ export default class Pressability { _touchActivateTime: ?number; _touchState: TouchState = 'NOT_RESPONDER'; + // [macOS + /** + * Returns true if the button from a press event is the default (primary/left) + * button. A button value of 0 or undefined is considered the default button. + */ + _isDefaultPressButton(button: ?number): boolean { + return !button; + } + // macOS] + constructor(config: PressabilityConfig) { this.configure(config); } @@ -556,8 +566,7 @@ export default class Pressability { // [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) { + if (!this._isDefaultPressButton(event?.nativeEvent?.button)) { return; } // macOS] @@ -799,7 +808,12 @@ export default class Pressability { if (isPressInSignal(prevState) && signal === 'LONG_PRESS_DETECTED') { const {onLongPress} = this._config; - if (onLongPress != null) { + if ( + onLongPress != null && + this._isDefaultPressButton( + getTouchFromPressEvent(event).button, + ) /* [macOS] */ + ) { onLongPress(event); } } @@ -820,7 +834,12 @@ export default class Pressability { this._deactivate(event); } const {onLongPress, onPress, android_disableSound} = this._config; - if (onPress != null) { + if ( + onPress != null && + this._isDefaultPressButton( + getTouchFromPressEvent(event).button, + ) /* [macOS] */ + ) { const isPressCanceledByLongPress = onLongPress != null && prevState === 'RESPONDER_ACTIVE_LONG_PRESS_IN'; if (!isPressCanceledByLongPress) { From d741a6ac3af54cba9e3a32e6efb7c8107ff80890 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 8 Apr 2026 12:06:07 -0700 Subject: [PATCH 4/5] Add onAuxClick test page in View example and docsite entry - Add MouseClickEventsExample to ViewExample.js with both a plain View and a Pressable target to verify onAuxClick fires and onPress does not on right-click - Document onAuxClick in docsite view-events.md with event data spec and usage example - Add button property to onDoubleClick event data docs Co-Authored-By: Claude Opus 4.6 --- docsite/api/view-events.md | 29 +++++ .../rn-tester/js/examples/View/ViewExample.js | 106 ++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/docsite/api/view-events.md b/docsite/api/view-events.md index d4c1bf4a7fbe..0b5ef15925c8 100644 --- a/docsite/api/view-events.md +++ b/docsite/api/view-events.md @@ -126,6 +126,34 @@ Example: --- +### `onAuxClick` + +Fired when the user clicks on the view with a non-primary button (e.g., right-click or middle-click). This follows the [W3C `auxclick` event](https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event) specification. + +**Event Data:** Mouse event with the following properties: +- `clientX`: Horizontal position in the target view +- `clientY`: Vertical position in the target view +- `screenX`: Horizontal position in the window +- `screenY`: Vertical position in the window +- `altKey`: Whether Alt/Option key is pressed +- `ctrlKey`: Whether Control key is pressed +- `shiftKey`: Whether Shift key is pressed +- `metaKey`: Whether Command key is pressed +- `button`: The button number that was pressed (2 for right-click) + +Example: +```javascript + { + console.log('Right clicked, button:', event.nativeEvent.button); +}}> + Right click me + +``` + +> **Note:** Right-clicking a `Pressable` will fire `onAuxClick` but will **not** trigger `onPress`. Only primary (left) button clicks trigger `onPress`. + +--- + ### `onDoubleClick` Fired when the user double-clicks on the view. @@ -139,6 +167,7 @@ Fired when the user double-clicks on the view. - `ctrlKey`: Whether Control key is pressed - `shiftKey`: Whether Shift key is pressed - `metaKey`: Whether Command key is pressed +- `button`: The button number that was pressed (0 for left-click) Example: ```javascript diff --git a/packages/rn-tester/js/examples/View/ViewExample.js b/packages/rn-tester/js/examples/View/ViewExample.js index cbddb935dc3a..1dcc24fa66c3 100644 --- a/packages/rn-tester/js/examples/View/ViewExample.js +++ b/packages/rn-tester/js/examples/View/ViewExample.js @@ -768,6 +768,106 @@ function FocusBlurExample(): React.Node { ); } +// [macOS +function MouseClickEventsExample(): React.Node { + const [eventLog, setEventLog] = useState>([]); + + const appendEvent = (eventName: string) => { + setEventLog(prev => [eventName, ...prev.slice(0, 19)]); + }; + + return ( + + appendEvent(`auxClick (button=${e.nativeEvent.button})`)} + onDoubleClick={() => appendEvent('doubleClick')} + onMouseEnter={() => appendEvent('mouseEnter')} + onMouseLeave={() => appendEvent('mouseLeave')}> + + Click Target + + + Left-click, right-click, or double-click this box + + + + appendEvent(`pressable auxClick (button=${e.nativeEvent.button})`) + } + onPress={() => appendEvent('pressable onPress')} + onDoubleClick={() => appendEvent('pressable doubleClick')}> + + Pressable Target + + + Right-click should fire auxClick but NOT onPress + + + + + Event Log: + + {eventLog.length === 0 ? ( + + No events yet + + ) : ( + eventLog.map((event, i) => ( + + {event} + + )) + )} + + + ); +} + +const mouseClickStyles = StyleSheet.create({ + targetBox: { + backgroundColor: '#527FE4', + borderRadius: 5, + padding: 20, + marginBottom: 10, + alignItems: 'center', + }, + pressableBox: { + backgroundColor: '#7B61FF', + borderRadius: 5, + padding: 20, + marginBottom: 10, + alignItems: 'center', + }, + targetText: { + color: 'white', + fontWeight: 'bold', + fontSize: 16, + }, + hintText: { + color: 'rgba(255, 255, 255, 0.7)', + fontSize: 12, + marginTop: 4, + }, + logBox: { + backgroundColor: '#f0f0f0', + borderRadius: 5, + padding: 10, + maxHeight: 200, + }, + logHeader: { + fontWeight: 'bold', + marginBottom: 5, + }, + logEntry: { + fontSize: 12, + fontFamily: Platform.OS === 'macos' ? 'Menlo' : 'monospace', + color: '#333', + }, +}); +// macOS] + export default ({ title: 'View', documentationURL: 'https://reactnative.dev/docs/view', @@ -1437,5 +1537,11 @@ export default ({ name: 'focus-blur', render: FocusBlurExample, }, + { + title: 'Mouse Click Events', + name: 'mouse-click-events', + platform: 'macos', + render: MouseClickEventsExample, + }, ], }: RNTesterModule); From 8e1eeb8a063af634790debefd985c6d27ada6a05 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 8 Apr 2026 13:28:52 -0700 Subject: [PATCH 5/5] fix(ci): prevent backport update job from failing on every PR Move the GitHub App token generation step after the backport PR lookup in the update-backport job. The lookup now uses the default github.token (which always works) and all subsequent steps are skipped when no backport PRs exist. This prevents the job from failing on every PR push due to token generation errors when there's nothing to update. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/microsoft-backport.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/microsoft-backport.yml b/.github/workflows/microsoft-backport.yml index 9d19efb82643..fb33d0fb6266 100644 --- a/.github/workflows/microsoft-backport.yml +++ b/.github/workflows/microsoft-backport.yml @@ -200,17 +200,10 @@ jobs: contents: write pull-requests: write steps: - - name: Generate GitHub App token - uses: actions/create-github-app-token@v2 - id: app-token - with: - app-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - - name: Find linked backport PRs id: find-backports env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + GH_TOKEN: ${{ github.token }} HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} PR_NUMBER: ${{ github.event.pull_request.number }} REPO: ${{ github.repository }} @@ -234,6 +227,14 @@ jobs: echo "$BACKPORT_PRS" > /tmp/backport-prs.txt fi + - name: Generate GitHub App token + if: steps.find-backports.outputs.found == 'true' + uses: actions/create-github-app-token@v2 + id: app-token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + - name: Checkout if: steps.find-backports.outputs.found == 'true' uses: actions/checkout@v4