diff --git a/packages/react-native/Libraries/Text/Text.d.ts b/packages/react-native/Libraries/Text/Text.d.ts index b90bf240ba72d9..db7aea477d51e1 100644 --- a/packages/react-native/Libraries/Text/Text.d.ts +++ b/packages/react-native/Libraries/Text/Text.d.ts @@ -101,9 +101,20 @@ export interface TextPropsAndroid { android_hyphenationFrequency?: 'normal' | 'none' | 'full' | undefined; } +// [macOS +export interface TextPropsMacOS { + enableFocusRing?: boolean | undefined; + focusable?: boolean | undefined; + onMouseEnter?: ((event: MouseEvent) => void) | undefined; + onMouseLeave?: ((event: MouseEvent) => void) | undefined; + tooltip?: string | undefined; +} +// macOS] + // https://reactnative.dev/docs/text#props export interface TextProps extends TextPropsIOS, + TextPropsMacOS, // [macOS] TextPropsAndroid, AccessibilityProps { /** diff --git a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm index d9b1e2ffd5aa73..09eda7f6e3e25d 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.mm @@ -85,16 +85,24 @@ - (void)uiManagerWillPerformMounting NSNumber *tag = self.reactTag; NSMutableArray *descendantViewTags = [NSMutableArray new]; - [textStorage enumerateAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName - inRange:NSMakeRange(0, textStorage.length) - options:0 - usingBlock:^(RCTShadowView *shadowView, NSRange range, __unused BOOL *stop) { - if (!shadowView) { - return; - } + NSMutableArray *virtualSubviewTags = [NSMutableArray new]; // [macOS] + +// [macOS - Enumerate embedded shadow views and virtual subviews in one loop + [textStorage enumerateAttributesInRange:NSMakeRange(0, textStorage.length) + options:0 + usingBlock:^(NSDictionary *_Nonnull attrs, NSRange range, __unused BOOL * _Nonnull stop) { + id embeddedViewAttribute = attrs[RCTBaseTextShadowViewEmbeddedShadowViewAttributeName]; + if ([embeddedViewAttribute isKindOfClass:[RCTShadowView class]]) { + RCTShadowView *embeddedShadowView = (RCTShadowView *)embeddedViewAttribute; + [descendantViewTags addObject:embeddedShadowView.reactTag]; + } - [descendantViewTags addObject:shadowView.reactTag]; - }]; + id tagAttribute = attrs[RCTTextAttributesTagAttributeName]; + if ([tagAttribute isKindOfClass:[NSNumber class]] && ![tagAttribute isEqualToNumber:tag]) { + [virtualSubviewTags addObject:tagAttribute]; + } + }]; +// macOS] [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { // [macOS] RCTTextView *textView = (RCTTextView *)viewRegistry[tag]; @@ -113,11 +121,25 @@ - (void)uiManagerWillPerformMounting [descendantViews addObject:descendantView]; }]; +// [macOS + NSMutableArray *virtualSubviews = [NSMutableArray arrayWithCapacity:virtualSubviewTags.count]; + [virtualSubviewTags + enumerateObjectsUsingBlock:^(NSNumber *_Nonnull virtualSubviewTag, NSUInteger index, BOOL *_Nonnull stop) { + RCTPlatformView *virtualSubview = viewRegistry[virtualSubviewTag]; + if ([virtualSubview isKindOfClass:[RCTVirtualTextView class]]) { + [virtualSubviews addObject:(RCTVirtualTextView *)virtualSubview]; + } + }]; +// macOS] + // Removing all references to Shadow Views to avoid unnecessary retaining. [textStorage removeAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName range:NSMakeRange(0, textStorage.length)]; - [textView setTextStorage:textStorage contentFrame:contentFrame descendantViews:descendantViews]; + [textView setTextStorage:textStorage + contentFrame:contentFrame + descendantViews:descendantViews + virtualSubviews:virtualSubviews]; // [macOS] }]; } diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.h b/packages/react-native/Libraries/Text/Text/RCTTextView.h index 3de368b73b6bba..b66c68399e9390 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.h +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.h @@ -7,6 +7,7 @@ #import #import // [macOS] +#import // [macOS] #import // [macOS] @@ -22,6 +23,13 @@ NS_ASSUME_NONNULL_BEGIN contentFrame:(CGRect)contentFrame descendantViews:(NSArray *)descendantViews; // [macOS] +// [macOS +- (void)setTextStorage:(NSTextStorage *)textStorage + contentFrame:(CGRect)contentFrame + descendantViews:(NSArray *)descendantViews + virtualSubviews:(NSArray *_Nullable)virtualSubviews; +// macOS] + /** * (Experimental and unused for Paper) Pointer event handlers. */ diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.mm b/packages/react-native/Libraries/Text/Text/RCTTextView.mm index 2557e712c1f737..96cc8e639c52c1 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.mm @@ -12,6 +12,7 @@ #endif // [macOS] #import // [macOS] +#import // [macOS] #import #import #import // [macOS] @@ -62,6 +63,8 @@ @implementation RCTTextView { id _eventDispatcher; // [macOS] NSArray *_Nullable _descendantViews; // [macOS] + NSArray *_Nullable _virtualSubviews; // [macOS] + RCTUIView *_Nullable _currentHoveredSubview; // [macOS] NSTextStorage *_Nullable _textStorage; CGRect _contentFrame; } @@ -99,6 +102,7 @@ - (instancetype)initWithFrame:(CGRect)frame _textView.layoutManager.usesFontLeading = NO; _textStorage = _textView.textStorage; [self addSubview:_textView]; + _currentHoveredSubview = nil; #endif // macOS] RCTUIViewSetContentModeRedraw(self); // [macOS] } @@ -162,6 +166,20 @@ - (void)setTextStorage:(NSTextStorage *)textStorage contentFrame:(CGRect)contentFrame descendantViews:(NSArray *)descendantViews // [macOS] { + // [macOS - to keep track of virtualSubviews as well + [self setTextStorage:textStorage + contentFrame:contentFrame + descendantViews:descendantViews + virtualSubviews:nil]; +} + +- (void)setTextStorage:(NSTextStorage *)textStorage + contentFrame:(CGRect)contentFrame + descendantViews:(NSArray *)descendantViews + virtualSubviews:(NSArray *)virtualSubviews +{ + // macOS] + // This lets the textView own its text storage on macOS // We update and replace the text container `_textView.textStorage.attributedString` when text/layout changes #if !TARGET_OS_OSX // [macOS] @@ -204,6 +222,8 @@ - (void)setTextStorage:(NSTextStorage *)textStorage [self addSubview:view]; } + _virtualSubviews = virtualSubviews; // [macOS] + [self setNeedsDisplay]; } @@ -398,6 +418,21 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture } #else // [macOS +- (BOOL)hasMouseHoverEvent +{ + if ([super hasMouseHoverEvent]) { + return YES; + } + + // We only care about virtual subviews here. + // Embedded views (e.g., ) handle mouse hover events themselves. + NSUInteger indexOfChildWithMouseHoverEvent = [_virtualSubviews indexOfObjectPassingTest:^BOOL(RCTVirtualTextView *_Nonnull childView, NSUInteger idx, BOOL *_Nonnull stop) { + *stop = [childView hasMouseHoverEvent]; + return *stop; + }]; + return indexOfChildWithMouseHoverEvent != NSNotFound; +} + - (NSView *)hitTest:(NSPoint)point { // We will forward mouse click events to the NSTextView ourselves to prevent NSTextView from swallowing events that may be handled in JS (e.g. long press). @@ -412,6 +447,110 @@ - (NSView *)hitTest:(NSPoint)point return isTextViewClick ? self : hitView; } +- (NSNumber *)reactTagAtMouseLocationFromEvent:(NSEvent *)event +{ + NSPoint locationInSelf = [self convertPoint:event.locationInWindow fromView:nil]; + NSPoint locationInInnerTextView = [self convertPoint:locationInSelf toView:_textView]; // This is needed if the parent view has padding + return [self reactTagAtPoint:locationInInnerTextView]; +} + +- (void)mouseEntered:(NSEvent *)event +{ + // superclass invokes self.onMouseEnter, so do this first + [super mouseEntered:event]; + + [self updateHoveredSubviewWithEvent:event]; +} + +- (void)mouseExited:(NSEvent *)event +{ + [self updateHoveredSubviewWithEvent:event]; + + // superclass invokes self.onMouseLeave, so do this last + [super mouseExited:event]; +} + +- (void)mouseMoved:(NSEvent *)event +{ + [super mouseMoved:event]; + [self updateHoveredSubviewWithEvent:event]; +} + +- (void)updateHoveredSubviewWithEvent:(NSEvent *)event +{ + RCTUIView *hoveredView = nil; + + if ([event type] != NSEventTypeMouseExited && _virtualSubviews != nil) { + NSNumber *reactTagOfHoveredView = [self reactTagAtMouseLocationFromEvent:event]; + + if (reactTagOfHoveredView == nil) { + // This happens if we hover over an embedded view, which will handle its own mouse events + return; + } + + if ([reactTagOfHoveredView isEqualToNumber:self.reactTag]) { + // We're hovering over the root Text element + hoveredView = self; + } else { + // Maybe we're hovering over a child Text element? + NSUInteger index = [_virtualSubviews indexOfObjectPassingTest:^BOOL(RCTVirtualTextView *_Nonnull view, NSUInteger idx, BOOL *_Nonnull stop) { + *stop = [[view reactTag] isEqualToNumber:reactTagOfHoveredView]; + return *stop; + }]; + if (index != NSNotFound) { + hoveredView = _virtualSubviews[index]; + } + } + } + + if (_currentHoveredSubview == hoveredView) { + return; + } + + // self will always be an ancestor of any views we pass in here, so it serves as a good default option. + // Also, if we do set from/to nil, we have to call the relevant events on the entire subtree. + RCTUIManager *uiManager = [[_eventDispatcher bridge] uiManager]; + RCTShadowView *oldShadowView = [uiManager shadowViewForReactTag:[(_currentHoveredSubview ?: self) reactTag]]; + RCTShadowView *newShadowView = [uiManager shadowViewForReactTag:[(hoveredView ?: self) reactTag]]; + + // Find the common ancestor between the two shadow views + RCTShadowView *commonAncestor = [oldShadowView ancestorSharedWithShadowView:newShadowView]; + + for (RCTShadowView *exitedShadowView = oldShadowView; exitedShadowView != commonAncestor && exitedShadowView != nil; exitedShadowView = [exitedShadowView reactSuperview]) { + RCTPlatformView *exitedView = [uiManager viewForReactTag:[exitedShadowView reactTag]]; + if (![exitedView isKindOfClass:[RCTUIView class]]) { + RCTLogError(@"Unexpected view of type %@ found in hierarchy, must be RCTUIView or subclass", [exitedView class]); + continue; + } + + RCTUIView *exitedReactView = (RCTUIView *)exitedView; + [self sendMouseEventWithBlock:[exitedReactView onMouseLeave] + locationInfo:[self locationInfoFromEvent:event] + modifierFlags:event.modifierFlags + additionalData:nil]; + } + + // We cache these so we can call them from outermost to innermost + NSMutableArray *enteredViewHierarchy = [NSMutableArray new]; + for (RCTShadowView *enteredShadowView = newShadowView; enteredShadowView != commonAncestor && enteredShadowView != nil; enteredShadowView = [enteredShadowView reactSuperview]) { + RCTPlatformView *enteredView = [uiManager viewForReactTag:[enteredShadowView reactTag]]; + if (![enteredView isKindOfClass:[RCTUIView class]]) { + RCTLogError(@"Unexpected view of type %@ found in hierarchy, must be RCTUIView or subclass", [enteredView class]); + continue; + } + + [enteredViewHierarchy addObject:(RCTUIView *)enteredView]; + } + for (NSInteger i = [enteredViewHierarchy count] - 1; i >= 0; i--) { + [self sendMouseEventWithBlock:[[enteredViewHierarchy objectAtIndex:i] onMouseEnter] + locationInfo:[self locationInfoFromEvent:event] + modifierFlags:event.modifierFlags + additionalData:nil]; + } + + _currentHoveredSubview = hoveredView; +} + - (void)rightMouseDown:(NSEvent *)event { diff --git a/packages/react-native/Libraries/Text/TextProps.js b/packages/react-native/Libraries/Text/TextProps.js index 51d8d84bc1b709..25356d0ec3e062 100644 --- a/packages/react-native/Libraries/Text/TextProps.js +++ b/packages/react-native/Libraries/Text/TextProps.js @@ -288,5 +288,19 @@ export type TextProps = $ReadOnly<{| * @platform macos */ enableFocusRing?: ?boolean, + + /** + * This event is called when the mouse hovers over this component. + * + * @platform macos + */ + onMouseEnter?: ?(event: MouseEvent) => void, + + /** + * This event is called when the mouse moves off of this component. + * + * @platform macos + */ + onMouseLeave?: ?(event: MouseEvent) => void, // macOS] |}>; diff --git a/packages/react-native/React/Base/RCTUIKit.h b/packages/react-native/React/Base/RCTUIKit.h index f58f24d1d2daec..aad3257664bd5d 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,16 @@ CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *path); - (void)setNeedsDisplay; +// Methods related to mouse events +- (BOOL)hasMouseHoverEvent; +- (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 +438,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..75f5c83eec3c6c 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,7 +215,9 @@ @implementation RCTUIView @private NSColor *_backgroundColor; BOOL _clipsToBounds; + BOOL _hasMouseOver; BOOL _userInteractionEnabled; + NSTrackingArea *_trackingArea; BOOL _mouseDownCanMoveWindow; } @@ -287,9 +290,171 @@ - (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]; + } + } +} + +- (BOOL)hasMouseHoverEvent +{ + // This can be overridden by subclasses as needed. + // e.g., RCTTextView, which consolidates its JS children into a single gigantic NSAttributedString + return self.onMouseEnter || self.onMouseLeave; +} + +- (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); +} + +- (void)updateTrackingAreas +{ + BOOL hasMouseHoverEvent = [self hasMouseHoverEvent]; + BOOL wouldRecreateIdenticalTrackingArea = hasMouseHoverEvent && _trackingArea && NSEqualRects(self.bounds, [_trackingArea rect]); + + if (!wouldRecreateIdenticalTrackingArea) { + if (_trackingArea) { + [self removeTrackingArea:_trackingArea]; + } + + if (hasMouseHoverEvent) { + _trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds + options:NSTrackingActiveAlways|NSTrackingMouseEnteredAndExited + owner:self + userInfo:nil]; + [self addTrackingArea:_trackingArea]; + } + } + + [super updateTrackingAreas]; +} + - (BOOL)mouseDownCanMoveWindow{ return _mouseDownCanMoveWindow; } diff --git a/packages/react-native/React/Views/RCTShadowView.h b/packages/react-native/React/Views/RCTShadowView.h index 8c7567076bb309..e02c71bef76c0c 100644 --- a/packages/react-native/React/Views/RCTShadowView.h +++ b/packages/react-native/React/Views/RCTShadowView.h @@ -241,6 +241,13 @@ typedef void (^RCTApplierBlock)(NSDictionary *vie */ - (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor; +// [macOS +/** + * Returns the closest ancestor shared by this shadow view and another specified shadow view. + */ +- (RCTShadowView *)ancestorSharedWithShadowView:(RCTShadowView *)shadowView; +// macOS] + /** * Checks if the current shadow view is a descendant of the provided `ancestor` */ diff --git a/packages/react-native/React/Views/RCTShadowView.m b/packages/react-native/React/Views/RCTShadowView.m index 802d6b6da8d149..8effd4a3c688af 100644 --- a/packages/react-native/React/Views/RCTShadowView.m +++ b/packages/react-native/React/Views/RCTShadowView.m @@ -182,6 +182,25 @@ - (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor return (CGRect){offset, self.layoutMetrics.frame.size}; } +// [macOS +- (RCTShadowView *)ancestorSharedWithShadowView:(RCTShadowView *)shadowView +{ + // TODO: Can this be optimized by climbing up both hierarchies at the same time? + NSMutableSet *selfSuperviews = [NSMutableSet set]; + for (RCTShadowView *view = self; view != nil; view = [view reactSuperview]) { + [selfSuperviews addObject:view]; + } + + for (RCTShadowView *candidateView = shadowView; candidateView != nil; candidateView = [candidateView reactSuperview]) { + if ([selfSuperviews containsObject:candidateView]) { + return candidateView; + } + } + + return nil; +} +// macOS] + - (BOOL)viewIsDescendantOf:(RCTShadowView *)ancestor { RCTShadowView *shadowView = self; 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..ac9859818d7042 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -136,8 +136,6 @@ @implementation RCTView { RCTUIColor *_backgroundColor; // [macOS] id _eventDispatcher; // [macOS] #if TARGET_OS_OSX // [macOS - NSTrackingArea *_trackingArea; - BOOL _hasMouseOver; BOOL _mouseDownCanMoveWindow; #endif // macOS] NSMutableDictionary *accessibilityActionsNameMap; @@ -778,67 +776,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 @@ -1527,46 +1465,6 @@ - (BOOL)acceptsFirstResponder return [self focusable] || [super acceptsFirstResponder]; } -- (void)updateTrackingAreas -{ - BOOL hasMouseHoverEvent = self.onMouseEnter || self.onMouseLeave; - BOOL wouldRecreateIdenticalTrackingArea = hasMouseHoverEvent && _trackingArea && NSEqualRects(self.bounds, [_trackingArea rect]); - - if (!wouldRecreateIdenticalTrackingArea) { - if (_trackingArea) { - [self removeTrackingArea:_trackingArea]; - } - - if (hasMouseHoverEvent) { - _trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds - options:NSTrackingActiveAlways|NSTrackingMouseEnteredAndExited - owner:self - userInfo:nil]; - [self addTrackingArea:_trackingArea]; - } - } - - [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 +1477,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 +1555,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; diff --git a/packages/rn-tester/js/examples/Text/TextExample.ios.js b/packages/rn-tester/js/examples/Text/TextExample.ios.js index 220d09cc424649..6ae2de8bd5a635 100644 --- a/packages/rn-tester/js/examples/Text/TextExample.ios.js +++ b/packages/rn-tester/js/examples/Text/TextExample.ios.js @@ -1343,6 +1343,27 @@ const examples = [ }, }, // [macOS + { + title: 'Mouse hover events on nested Text elements', + render: function (): React.Node { + function mouseProps(name: string) { + return { + onMouseEnter: () => console.log(`Enter ${name}`), + onMouseLeave: () => console.log(`Leave ${name}`), + }; + } + + return ( + + This is some text with{' '} + + a nested element + {' '} + that tracks mouse hover events + + ); + }, + }, { title: 'Text components inheriting color from parent', render: function (): React.Node {