Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docsite/api/view-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<View onAuxClick={(event) => {
console.log('Right clicked, button:', event.nativeEvent.button);
}}>
<Text>Right click me</Text>
</View>
```

> **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.
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const UIView = {
mouseDownCanMoveWindow: true,
enableFocusRing: true,
focusable: true,
onAuxClick: true,
onMouseEnter: true,
onMouseLeave: true,
onDoubleClick: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -394,6 +400,8 @@ const validAttributesForEventProps = ConditionallyIgnoredEventHandlers({
onTouchCancel: true,

// Pointer events
onAuxClick: true,
onAuxClickCapture: true,
onClick: true,
onClickCapture: true,
onPointerUp: true,
Expand Down
42 changes: 38 additions & 4 deletions packages/react-native/Libraries/Pressability/Pressability.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -554,6 +564,13 @@ export default class Pressability {
return;
}

// [macOS Only fire onPress for primary (left) mouse button clicks.
// Non-primary buttons (right, middle) should not trigger onPress.
if (!this._isDefaultPressButton(event?.nativeEvent?.button)) {
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) {
Expand Down Expand Up @@ -791,28 +808,45 @@ 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);
}
}

const isPrevActive = isActiveSignal(prevState);
const isNextActive = isActiveSignal(nextState);

if (!isPrevActive && isNextActive) {
// [macOS Don't activate press visual feedback for non-primary mouse buttons
// (e.g. right-click, middle-click). They should fire onAuxClick, not onPress.
const isPrimaryButton = this._isDefaultPressButton(
getTouchFromPressEvent(event).button,
);
// macOS]

if (!isPrevActive && isNextActive && isPrimaryButton /* [macOS] */) {
this._activate(event);
} else if (isPrevActive && !isNextActive) {
this._deactivate(event);
}

if (isPressInSignal(prevState) && signal === 'RESPONDER_RELEASE') {
// If we never activated (due to delays), activate and deactivate now.
if (!isNextActive && !isPrevActive) {
if (!isNextActive && !isPrevActive && isPrimaryButton /* [macOS] */) {
this._activate(event);
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2031,10 +2031,12 @@ - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
enum MouseEventType {
MouseEnter,
MouseLeave,
Click,
DoubleClick,
AuxClick,
};

- (void)emitMouseEvent:(MouseEventType)eventType
- (void)emitMouseEvent:(MouseEventType)eventType button:(int)button
{
if (!_eventEmitter) {
return;
Expand All @@ -2054,6 +2056,7 @@ - (void)emitMouseEvent:(MouseEventType)eventType
.ctrlKey = static_cast<bool>(modifierFlags & NSEventModifierFlagControl),
.shiftKey = static_cast<bool>(modifierFlags & NSEventModifierFlagShift),
.metaKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCommand),
.button = button,
};

switch (eventType) {
Expand All @@ -2065,12 +2068,25 @@ - (void)emitMouseEvent:(MouseEventType)eventType
_eventEmitter->onMouseLeave(mouseEvent);
break;

case Click:
_eventEmitter->onClick(mouseEvent);
break;

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,
Expand Down Expand Up @@ -2185,12 +2201,55 @@ - (void)mouseExited:(NSEvent *)event
- (void)mouseUp:(NSEvent *)event
{
BOOL hasDoubleClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::DoubleClick];
BOOL hasClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::Click];
if (hasDoubleClickEventHandler && event.clickCount == 2) {
[self emitMouseEvent :DoubleClick];
[self emitMouseEvent:DoubleClick];
} else if (hasClickEventHandler && event.clickCount == 1) {
[self emitMouseEvent:Click];
} else {
[super mouseUp:event];
}
}

- (void)rightMouseDown:(NSEvent *)event
{
// Accept rightMouseDown to prevent the default NSView behavior of passing it
// up the responder chain (which can trigger a context menu modal loop that
// consumes rightMouseUp).
BOOL hasAuxClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::AuxClick];
if (!hasAuxClickEventHandler) {
[super rightMouseDown:event];
}
}

- (void)rightMouseUp:(NSEvent *)event
{
BOOL hasAuxClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::AuxClick];
if (hasAuxClickEventHandler) {
[self emitMouseEvent:AuxClick button:2];
} else {
[super rightMouseUp:event];
}
}

- (void)otherMouseDown:(NSEvent *)event
{
// Accept otherMouseDown so that otherMouseUp is delivered to this view.
BOOL hasAuxClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::AuxClick];
if (!hasAuxClickEventHandler) {
[super otherMouseDown:event];
}
}

- (void)otherMouseUp:(NSEvent *)event
{
BOOL hasAuxClickEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::AuxClick];
if (hasAuxClickEventHandler) {
[self emitMouseEvent:AuxClick button:1];
} else {
[super otherMouseUp:event];
}
}
#endif // macOS]

- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -703,8 +703,12 @@ - (void)_dispatchActivePointers:(std::vector<ActivePointer>)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;
}
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait;
/**
* (Experimental and unused for Paper) Pointer event handlers.
*/
@property (nonatomic, assign) RCTBubblingEventBlock onAuxClick;
@property (nonatomic, assign) RCTBubblingEventBlock onClick;
@property (nonatomic, assign) RCTBubblingEventBlock onPointerCancel;
@property (nonatomic, assign) RCTBubblingEventBlock onPointerDown;
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,7 @@ - (void) updateAccessibilityRole:(RCTView *)view withDefaultView:(RCTView *)defa
RCT_CUSTOM_VIEW_PROPERTY(onTouchCancel, BOOL, RCTView) {}

// Experimental/WIP Pointer Events (not yet ready for use)
RCT_EXPORT_VIEW_PROPERTY(onAuxClick, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onClick, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPointerCancel, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPointerDown, RCTBubblingEventBlock)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ 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);
// pointerType lets Pressability's onClick guard distinguish native click
// events from accessibility/responder-based clicks, avoiding double onPress.
payload.setProperty(runtime, "pointerType", "mouse");
return payload;
};

Expand All @@ -78,12 +82,24 @@ void HostPlatformViewEventEmitter::onMouseLeave(const MouseEvent& mouseEvent) co
});
}

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

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

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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ class HostPlatformViewEventEmitter : public BaseViewEventEmitter {

void onMouseEnter(MouseEvent const& mouseEvent) const;
void onMouseLeave(MouseEvent const& mouseEvent) const;
void onClick(MouseEvent const& mouseEvent) const;
void onDoubleClick(MouseEvent const& mouseEvent) const;
void onAuxClick(MouseEvent const& mouseEvent) const;

#pragma mark - Drag and Drop Events

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ struct HostPlatformViewEvents {
MouseEnter = 4,
MouseLeave = 5,
DoubleClick = 6,
AuxClick = 7,
Click = 8,
};

constexpr bool operator[](const Offset offset) const {
Expand Down Expand Up @@ -74,6 +76,10 @@ 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]);
result[Offset::Click] =
convertRawProp(context, rawProps, "onClick", sourceValue[Offset::Click], defaultValue[Offset::Click]);

return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ void HostPlatformViewProps::setProp(
VIEW_EVENT_CASE_MACOS(MouseEnter);
VIEW_EVENT_CASE_MACOS(MouseLeave);
VIEW_EVENT_CASE_MACOS(DoubleClick);
VIEW_EVENT_CASE_MACOS(AuxClick);
VIEW_EVENT_CASE_MACOS(Click);
RAW_SET_PROP_SWITCH_CASE_BASIC(focusable);
RAW_SET_PROP_SWITCH_CASE_BASIC(enableFocusRing);
RAW_SET_PROP_SWITCH_CASE_BASIC(keyDownEvents);
Expand Down
Loading
Loading