diff --git a/packages/react-native/React/Base/RCTUIKit.h b/packages/react-native/React/Base/RCTUIKit.h index f58f24d1d2daec..005fb61dcda592 100644 --- a/packages/react-native/React/Base/RCTUIKit.h +++ b/packages/react-native/React/Base/RCTUIKit.h @@ -123,6 +123,8 @@ NS_ASSUME_NONNULL_END #import +#import + NS_ASSUME_NONNULL_BEGIN // @@ -403,6 +405,15 @@ CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *path); - (void)setNeedsDisplay; +// Methods related to mouse events +- (NSDictionary*)locationInfoFromDraggingLocation:(NSPoint)locationInWindow; +- (NSDictionary*)locationInfoFromEvent:(NSEvent*)event; + +- (void)sendMouseEventWithBlock:(RCTDirectEventBlock)block + locationInfo:(NSDictionary*)locationInfo + modifierFlags:(NSEventModifierFlags)modifierFlags + additionalData:(NSDictionary*)additionalData; + // FUTURE: When Xcode 14 is no longer supported (CI is building with Xcode 15), we can remove this override since it's now declared on NSView @property BOOL clipsToBounds; @property (nonatomic, copy) NSColor *backgroundColor; @@ -426,6 +437,24 @@ CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *path); */ @property (nonatomic, assign) BOOL enableFocusRing; +// Mouse events +@property (nonatomic, copy) RCTDirectEventBlock onMouseEnter; +@property (nonatomic, copy) RCTDirectEventBlock onMouseLeave; +@property (nonatomic, copy) RCTDirectEventBlock onDragEnter; +@property (nonatomic, copy) RCTDirectEventBlock onDragLeave; +@property (nonatomic, copy) RCTDirectEventBlock onDrop; + +// Focus events +@property (nonatomic, copy) RCTBubblingEventBlock onBlur; +@property (nonatomic, copy) RCTBubblingEventBlock onFocus; + +@property (nonatomic, copy) RCTBubblingEventBlock onResponderGrant; +@property (nonatomic, copy) RCTBubblingEventBlock onResponderMove; +@property (nonatomic, copy) RCTBubblingEventBlock onResponderRelease; +@property (nonatomic, copy) RCTBubblingEventBlock onResponderTerminate; +@property (nonatomic, copy) RCTBubblingEventBlock onResponderTerminationRequest; +@property (nonatomic, copy) RCTBubblingEventBlock onStartShouldSetResponder; + @end // UIScrollView diff --git a/packages/react-native/React/Base/macOS/RCTUIKit.m b/packages/react-native/React/Base/macOS/RCTUIKit.m index 01c96db75eeeca..cf1fe08e735fc0 100644 --- a/packages/react-native/React/Base/macOS/RCTUIKit.m +++ b/packages/react-native/React/Base/macOS/RCTUIKit.m @@ -12,6 +12,7 @@ #import #import +#import #import @@ -214,6 +215,7 @@ @implementation RCTUIView @private NSColor *_backgroundColor; BOOL _clipsToBounds; + BOOL _hasMouseOver; BOOL _userInteractionEnabled; BOOL _mouseDownCanMoveWindow; } @@ -287,9 +289,142 @@ - (BOOL)isFirstResponder { - (void)viewDidMoveToWindow { + // Subscribe to view bounds changed notification so that the view can be notified when a + // scroll event occurs either due to trackpad/gesture based scrolling or a scrollwheel event + // both of which would not cause the mouseExited to be invoked. + + if ([self window] == nil) { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSViewBoundsDidChangeNotification + object:nil]; + } + else if ([[self enclosingScrollView] contentView] != nil) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(viewBoundsChanged:) + name:NSViewBoundsDidChangeNotification + object:[[self enclosingScrollView] contentView]]; + } + + [self reactViewDidMoveToWindow]; // [macOS] Github#1412 + [self didMoveToWindow]; } +- (void)viewBoundsChanged:(NSNotification*)__unused inNotif +{ + // When an enclosing scrollview is scrolled using the scrollWheel or trackpad, + // the mouseExited: event does not get called on the view where mouseEntered: was previously called. + // This creates an unnatural pairing of mouse enter and exit events and can cause problems. + // We therefore explicitly check for this here and handle them by calling the appropriate callbacks. + + if (!_hasMouseOver && self.onMouseEnter) + { + NSPoint locationInWindow = [[self window] mouseLocationOutsideOfEventStream]; + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + + if (NSPointInRect(locationInView, [self bounds])) + { + _hasMouseOver = YES; + + [self sendMouseEventWithBlock:self.onMouseEnter + locationInfo:[self locationInfoFromDraggingLocation:locationInWindow] + modifierFlags:0 + additionalData:nil]; + } + } + else if (_hasMouseOver && self.onMouseLeave) + { + NSPoint locationInWindow = [[self window] mouseLocationOutsideOfEventStream]; + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + + if (!NSPointInRect(locationInView, [self bounds])) + { + _hasMouseOver = NO; + + [self sendMouseEventWithBlock:self.onMouseLeave + locationInfo:[self locationInfoFromDraggingLocation:locationInWindow] + modifierFlags:0 + additionalData:nil]; + } + } +} + +- (NSDictionary*)locationInfoFromDraggingLocation:(NSPoint)locationInWindow +{ + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + + return @{@"screenX": @(locationInWindow.x), + @"screenY": @(locationInWindow.y), + @"clientX": @(locationInView.x), + @"clientY": @(locationInView.y) + }; +} + +- (NSDictionary*)locationInfoFromEvent:(NSEvent*)event +{ + NSPoint locationInWindow = event.locationInWindow; + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + + return @{@"screenX": @(locationInWindow.x), + @"screenY": @(locationInWindow.y), + @"clientX": @(locationInView.x), + @"clientY": @(locationInView.y) + }; +} + +- (void)mouseEntered:(NSEvent *)event +{ + _hasMouseOver = YES; + [self sendMouseEventWithBlock:self.onMouseEnter + locationInfo:[self locationInfoFromEvent:event] + modifierFlags:event.modifierFlags + additionalData:nil]; +} + +- (void)mouseExited:(NSEvent *)event +{ + _hasMouseOver = NO; + [self sendMouseEventWithBlock:self.onMouseLeave + locationInfo:[self locationInfoFromEvent:event] + modifierFlags:event.modifierFlags + additionalData:nil]; +} + +- (void)sendMouseEventWithBlock:(RCTDirectEventBlock)block + locationInfo:(NSDictionary*)locationInfo + modifierFlags:(NSEventModifierFlags)modifierFlags + additionalData:(NSDictionary*)additionalData +{ + if (block == nil) { + return; + } + + NSMutableDictionary *body = [NSMutableDictionary new]; + + if (modifierFlags & NSEventModifierFlagShift) { + body[@"shiftKey"] = @YES; + } + if (modifierFlags & NSEventModifierFlagControl) { + body[@"ctrlKey"] = @YES; + } + if (modifierFlags & NSEventModifierFlagOption) { + body[@"altKey"] = @YES; + } + if (modifierFlags & NSEventModifierFlagCommand) { + body[@"metaKey"] = @YES; + } + + if (locationInfo) { + [body addEntriesFromDictionary:locationInfo]; + } + + if (additionalData) { + [body addEntriesFromDictionary:additionalData]; + } + + block(body); +} + - (BOOL)mouseDownCanMoveWindow{ return _mouseDownCanMoveWindow; } diff --git a/packages/react-native/React/Views/RCTView.h b/packages/react-native/React/Views/RCTView.h index da1704914906ae..d096c4c09b1fb3 100644 --- a/packages/react-native/React/Views/RCTView.h +++ b/packages/react-native/React/Views/RCTView.h @@ -167,12 +167,6 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; // that we can set through JS and the getter for `allowsVibrancy` can read in RCTView. @property (nonatomic, assign) BOOL allowsVibrancyInternal; -@property (nonatomic, copy) RCTDirectEventBlock onMouseEnter; -@property (nonatomic, copy) RCTDirectEventBlock onMouseLeave; -@property (nonatomic, copy) RCTDirectEventBlock onDragEnter; -@property (nonatomic, copy) RCTDirectEventBlock onDragLeave; -@property (nonatomic, copy) RCTDirectEventBlock onDrop; - // Keyboarding events // NOTE does not properly work with single line text inputs (most key downs). This is because those are // presumably handled by the window's field editor. To make it work, we'd need to look into providing diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index 709e92d20c7c40..46a76d7632f594 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -137,7 +137,6 @@ @implementation RCTView { id _eventDispatcher; // [macOS] #if TARGET_OS_OSX // [macOS NSTrackingArea *_trackingArea; - BOOL _hasMouseOver; BOOL _mouseDownCanMoveWindow; #endif // macOS] NSMutableDictionary *accessibilityActionsNameMap; @@ -778,67 +777,7 @@ -(void)didUpdateShadow [self setShadow:shadow]; } -- (void)viewDidMoveToWindow -{ - // Subscribe to view bounds changed notification so that the view can be notified when a - // scroll event occurs either due to trackpad/gesture based scrolling or a scrollwheel event - // both of which would not cause the mouseExited to be invoked. - - if ([self window] == nil) { - [[NSNotificationCenter defaultCenter] removeObserver:self - name:NSViewBoundsDidChangeNotification - object:nil]; - } - else if ([[self enclosingScrollView] contentView] != nil) { - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(viewBoundsChanged:) - name:NSViewBoundsDidChangeNotification - object:[[self enclosingScrollView] contentView]]; - } - - [self reactViewDidMoveToWindow]; // [macOS] Github#1412 - - [super viewDidMoveToWindow]; -} - -- (void)viewBoundsChanged:(NSNotification*)__unused inNotif -{ - // When an enclosing scrollview is scrolled using the scrollWheel or trackpad, - // the mouseExited: event does not get called on the view where mouseEntered: was previously called. - // This creates an unnatural pairing of mouse enter and exit events and can cause problems. - // We therefore explicitly check for this here and handle them by calling the appropriate callbacks. - - if (!_hasMouseOver && self.onMouseEnter) - { - NSPoint locationInWindow = [[self window] mouseLocationOutsideOfEventStream]; - NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; - - if (NSPointInRect(locationInView, [self bounds])) - { - _hasMouseOver = YES; - [self sendMouseEventWithBlock:self.onMouseEnter - locationInfo:[self locationInfoFromDraggingLocation:locationInWindow] - modifierFlags:0 - additionalData:nil]; - } - } - else if (_hasMouseOver && self.onMouseLeave) - { - NSPoint locationInWindow = [[self window] mouseLocationOutsideOfEventStream]; - NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; - - if (!NSPointInRect(locationInView, [self bounds])) - { - _hasMouseOver = NO; - - [self sendMouseEventWithBlock:self.onMouseLeave - locationInfo:[self locationInfoFromDraggingLocation:locationInWindow] - modifierFlags:0 - additionalData:nil]; - } - } -} #endif // macOS] #pragma mark - Statics for dealing with layoutGuides @@ -1549,24 +1488,6 @@ - (void)updateTrackingAreas [super updateTrackingAreas]; } -- (void)mouseEntered:(NSEvent *)event -{ - _hasMouseOver = YES; - [self sendMouseEventWithBlock:self.onMouseEnter - locationInfo:[self locationInfoFromEvent:event] - modifierFlags:event.modifierFlags - additionalData:nil]; -} - -- (void)mouseExited:(NSEvent *)event -{ - _hasMouseOver = NO; - [self sendMouseEventWithBlock:self.onMouseLeave - locationInfo:[self locationInfoFromEvent:event] - modifierFlags:event.modifierFlags - additionalData:nil]; -} - - (BOOL)mouseDownCanMoveWindow{ return _mouseDownCanMoveWindow; } @@ -1579,53 +1500,6 @@ - (BOOL)allowsVibrancy { return _allowsVibrancyInternal; } -- (NSDictionary*)locationInfoFromEvent:(NSEvent*)event -{ - NSPoint locationInWindow = event.locationInWindow; - NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; - - return @{@"screenX": @(locationInWindow.x), - @"screenY": @(locationInWindow.y), - @"clientX": @(locationInView.x), - @"clientY": @(locationInView.y) - }; -} - -- (void)sendMouseEventWithBlock:(RCTDirectEventBlock)block - locationInfo:(NSDictionary*)locationInfo - modifierFlags:(NSEventModifierFlags)modifierFlags - additionalData:(NSDictionary*)additionalData -{ - if (block == nil) { - return; - } - - NSMutableDictionary *body = [NSMutableDictionary new]; - - if (modifierFlags & NSEventModifierFlagShift) { - body[@"shiftKey"] = @YES; - } - if (modifierFlags & NSEventModifierFlagControl) { - body[@"ctrlKey"] = @YES; - } - if (modifierFlags & NSEventModifierFlagOption) { - body[@"altKey"] = @YES; - } - if (modifierFlags & NSEventModifierFlagCommand) { - body[@"metaKey"] = @YES; - } - - if (locationInfo) { - [body addEntriesFromDictionary:locationInfo]; - } - - if (additionalData) { - [body addEntriesFromDictionary:additionalData]; - } - - block(body); -} - - (NSDictionary*)dataTransferInfoFromPasteboard:(NSPasteboard*)pasteboard { NSArray *fileNames = [pasteboard propertyListForType:NSFilenamesPboardType] ?: @[]; @@ -1704,17 +1578,6 @@ - (NSDictionary*)dataTransferInfoFromPasteboard:(NSPasteboard*)pasteboard @"types": types}}; } -- (NSDictionary*)locationInfoFromDraggingLocation:(NSPoint)locationInWindow -{ - NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; - - return @{@"screenX": @(locationInWindow.x), - @"screenY": @(locationInWindow.y), - @"clientX": @(locationInView.x), - @"clientY": @(locationInView.y) - }; -} - - (NSDragOperation)draggingEntered:(id )sender { NSPasteboard *pboard = sender.draggingPasteboard;