Skip to content

Commit

Permalink
[0.73-stable] New events for RCTUIView (#2137)
Browse files Browse the repository at this point in the history
* Move mouse events from RCTView to superclass RCTUIView

* Add focus and responder events

* Move mouse event implementations to RCTUIView class

---------

Co-authored-by: Adam Gleitman <[email protected]>
  • Loading branch information
amgleitman and Adam Gleitman authored Jun 18, 2024
1 parent 3dab1a2 commit 9b9d06b
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 143 deletions.
29 changes: 29 additions & 0 deletions packages/react-native/React/Base/RCTUIKit.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ NS_ASSUME_NONNULL_END

#import <AppKit/AppKit.h>

#import <React/RCTComponent.h>

NS_ASSUME_NONNULL_BEGIN

//
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
135 changes: 135 additions & 0 deletions packages/react-native/React/Base/macOS/RCTUIKit.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#import <React/RCTUIKit.h>

#import <React/RCTAssert.h>
#import <UIView+React.h>

#import <objc/runtime.h>

Expand Down Expand Up @@ -214,6 +215,7 @@ @implementation RCTUIView
@private
NSColor *_backgroundColor;
BOOL _clipsToBounds;
BOOL _hasMouseOver;
BOOL _userInteractionEnabled;
BOOL _mouseDownCanMoveWindow;
}
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 0 additions & 6 deletions packages/react-native/React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
137 changes: 0 additions & 137 deletions packages/react-native/React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ @implementation RCTView {
id<RCTEventDispatcherProtocol> _eventDispatcher; // [macOS]
#if TARGET_OS_OSX // [macOS
NSTrackingArea *_trackingArea;
BOOL _hasMouseOver;
BOOL _mouseDownCanMoveWindow;
#endif // macOS]
NSMutableDictionary<NSString *, NSDictionary *> *accessibilityActionsNameMap;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -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] ?: @[];
Expand Down Expand Up @@ -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 <NSDraggingInfo>)sender
{
NSPasteboard *pboard = sender.draggingPasteboard;
Expand Down

0 comments on commit 9b9d06b

Please sign in to comment.