Skip to content

Commit

Permalink
[paper][touchhandler] Catch and broadcast outside window mouse up eve…
Browse files Browse the repository at this point in the history
…nts as opt-in

Summary:

Test Plan:

Reviewers:

Subscribers:

Tasks:

Tags:

Differential Revision: https://phabricator.intern.facebook.com/D43558607

# Conflicts:
#	React/Base/RCTTouchHandler.h
#	React/Base/RCTTouchHandler.m
  • Loading branch information
Nick authored and shwanton committed May 2, 2024
1 parent d6081c4 commit 9d08c3e
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 2 deletions.
6 changes: 6 additions & 0 deletions packages/react-native/React/Base/RCTTouchHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@

#import <React/RCTFrameUpdate.h>

#if TARGET_OS_OSX // [macOS
static NSString *const RCTTouchHandlerOutsideViewMouseUpNotification = @"RCTTouchHandlerOutsideViewMouseUpNotification";
#endif // macOS]

@class RCTBridge;

@interface RCTTouchHandler : UIGestureRecognizer
@property (class, nonatomic, assign) BOOL notifyOutsideViewEvents; // [macOS]

- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;

- (void)attachToView:(RCTUIView *)view; // [macOS]
- (void)detachFromView:(RCTUIView *)view; // [macOS]
+ (void)notifyOutsideViewMouseUp:(NSEvent *) event; // [macOS]

- (void)cancel;

Expand Down
93 changes: 91 additions & 2 deletions packages/react-native/React/Base/RCTTouchHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
#if !TARGET_OS_OSX // [macOS]
#import <UIKit/UIGestureRecognizerSubclass.h>
#endif // [macOS]
#import <React/RCTUITextField.h> // [macOS]

#import <React/RCTUtils.h>
#import <React/RCTUITextField.h>

#import "RCTAssert.h"
#import "RCTBridge.h"
Expand All @@ -22,6 +24,43 @@
#import "RCTUtils.h"
#import "UIView+React.h"

#if TARGET_OS_OSX // [macOS
@interface NSApplication (RCTTouchHandlerOverride)
- (NSEvent*)override_nextEventMatchingMask:(NSEventMask)mask
untilDate:(NSDate*)expiration
inMode:(NSRunLoopMode)mode
dequeue:(BOOL)dequeue;
@end

@implementation NSApplication (RCTTouchHandlerOverride)

+ (void)load
{
RCTSwapInstanceMethods(self, @selector(nextEventMatchingMask:untilDate:inMode:dequeue:), @selector(override_nextEventMatchingMask:untilDate:inMode:dequeue:));
}

- (NSEvent*)override_nextEventMatchingMask:(NSEventMask)mask
untilDate:(NSDate*)expiration
inMode:(NSRunLoopMode)mode
dequeue:(BOOL)dequeue
{
NSEvent* event = [self override_nextEventMatchingMask:mask
untilDate:expiration
inMode:mode
dequeue:dequeue];
if (dequeue && (event.type == NSEventTypeLeftMouseUp || event.type == NSEventTypeRightMouseUp || event.type == NSEventTypeOtherMouseUp)) {
RCTTouchHandler *targetTouchHandler = [RCTTouchHandler touchHandlerForEvent:event];
if (!targetTouchHandler) {
[RCTTouchHandler notifyOutsideViewMouseUp:event];
}
}

return event;
}

@end
#endif // macOS]

@interface RCTTouchHandler () <UIGestureRecognizerDelegate>
@end

Expand Down Expand Up @@ -50,6 +89,16 @@ @implementation RCTTouchHandler {
uint16_t _coalescingKey;
}

static BOOL _notifyOutsideViewEvents = NO;

+ (BOOL)notifyOutsideViewEvents {
return _notifyOutsideViewEvents;
}

+ (void)setNotifyOutsideViewEvents:(BOOL)newNotifyOutsideViewEvents {
_notifyOutsideViewEvents = newNotifyOutsideViewEvents;
}

- (instancetype)initWithBridge:(RCTBridge *)bridge
{
RCTAssertParam(bridge);
Expand All @@ -72,6 +121,10 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
self.delaysPrimaryMouseButtonEvents = NO; // default is NO.
self.delaysSecondaryMouseButtonEvents = NO; // default is NO.
self.delaysOtherMouseButtonEvents = NO; // default is NO.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(endOutsideViewMouseUp:)
name:RCTTouchHandlerOutsideViewMouseUpNotification
object:[RCTTouchHandler class]];
#endif // macOS]

self.delegate = self;
Expand All @@ -85,6 +138,10 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)coder)
#endif // macOS]

- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)attachToView:(RCTUIView *)view // [macOS]
{
RCTAssert(self.view == nil, @"RCTTouchHandler already has attached view.");
Expand Down Expand Up @@ -120,7 +177,9 @@ - (void)_recordNewTouches:(NSSet *)touches
// This means the state machine in Pressability.js on JS side is in a stuck state. Best we can do
// to get it unstuck is to send touch cancellation.
if (_lastRightMouseDown != NULL && [_nativeTouches containsObject:_lastRightMouseDown]) {
[self cancelTouchWithEvent:_lastRightMouseDown];
if (![RCTTouchHandler notifyOutsideViewEvents]) {
[self cancelTouchWithEvent:_lastRightMouseDown];
}
_lastRightMouseDown = NULL;
}
// Keep track of any active RightMouseDown touches. We reset it to NULL if interaction ends correctly
Expand Down Expand Up @@ -634,6 +693,32 @@ + (instancetype)touchHandlerForView:(NSView *)view {
return nil;
}

+ (void)notifyOutsideViewMouseUp:(NSEvent *) event {
if (![RCTTouchHandler notifyOutsideViewEvents]) {
return;
}
[[NSNotificationCenter defaultCenter] postNotificationName:RCTTouchHandlerOutsideViewMouseUpNotification
object:self
userInfo:@{@"event": event}];
}

- (void)endOutsideViewMouseUp:(NSNotification *)notification {
NSEvent *event = notification.userInfo[@"event"];

NSInteger index = [_nativeTouches indexOfObjectPassingTest:^BOOL(NSEvent *touch, __unused NSUInteger idx, __unused BOOL *stop) {
return touch.eventNumber == event.eventNumber;
}];
if (index == NSNotFound) {
return;
}

if ([self isDuplicateEvent:event]) {
return;
}

[self interactionsEnded:[NSSet setWithObject:event] withEvent:event];
}

// Showing a context menu via RightMouseDown prevents receiving RightMouseUp event
// and propagating touchEnd event to JS side, leaving the Responder state machine
// on JS side (in Pressabity.js) in an intermediate state, that will not be able to
Expand All @@ -646,6 +731,10 @@ + (instancetype)touchHandlerForView:(NSView *)view {
// on LeftMouseUp)
- (void)willShowMenuWithEvent:(NSEvent *)event
{
if ([RCTTouchHandler notifyOutsideViewEvents]) {
return;
}

if (event.type == NSEventTypeRightMouseDown) {
[self interactionsEnded:[NSSet setWithObject:event] withEvent:event];
}
Expand Down

0 comments on commit 9d08c3e

Please sign in to comment.