diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.h b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.h new file mode 100644 index 00000000000000..c487c96372da17 --- /dev/null +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#if TARGET_OS_OSX // [macOS + +#import + +#import "RCTTextUIKit.h" + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTWrappedTextView : RCTPlatformView + +@property (nonatomic, weak) id textInputDelegate; +@property (assign) BOOL hideVerticalScrollIndicator; + +@end + +NS_ASSUME_NONNULL_END + +#endif // macOS] diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.m b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.m new file mode 100644 index 00000000000000..cef137859ab75c --- /dev/null +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.m @@ -0,0 +1,221 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#if TARGET_OS_OSX // [macOS + +#import + +#import +#import + +@implementation RCTWrappedTextView { + RCTUITextView *_forwardingTextView; + RCTUIScrollView *_scrollView; + RCTClipView *_clipView; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + self.hideVerticalScrollIndicator = NO; + + _scrollView = [[RCTUIScrollView alloc] initWithFrame:self.bounds]; + _scrollView.backgroundColor = [RCTUIColor clearColor]; + _scrollView.drawsBackground = NO; + _scrollView.borderType = NSNoBorder; + _scrollView.hasHorizontalRuler = NO; + _scrollView.hasVerticalRuler = NO; + _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [_scrollView setHasVerticalScroller:YES]; + [_scrollView setHasHorizontalScroller:NO]; + + _clipView = [[RCTClipView alloc] initWithFrame:_scrollView.bounds]; + [_scrollView setContentView:_clipView]; + + _forwardingTextView = [[RCTUITextView alloc] initWithFrame:_scrollView.bounds]; + _forwardingTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _forwardingTextView.delegate = self; + + _forwardingTextView.verticallyResizable = YES; + _forwardingTextView.horizontallyResizable = YES; + _forwardingTextView.textContainer.containerSize = NSMakeSize(FLT_MAX, FLT_MAX); + _forwardingTextView.textContainer.widthTracksTextView = YES; + _forwardingTextView.textInputDelegate = self; + + _scrollView.documentView = _forwardingTextView; + _scrollView.contentView.postsBoundsChangedNotifications = YES; + + // Enable the focus ring by default + _scrollView.enableFocusRing = YES; + [self addSubview:_scrollView]; + + // a register for those notifications on the content view. + #if !TARGET_OS_OSX // [macOS] + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(boundsDidChange:) + name:NSViewBoundsDidChangeNotification + object:_scrollView.contentView]; + #else // [macOS + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(scrollViewDidScroll:) + name:NSViewBoundsDidChangeNotification + object:_scrollView.contentView]; + #endif // macOS] + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (BOOL)isFlipped +{ + return YES; +} + +#pragma mark - +#pragma mark Method forwarding to text view + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + [invocation invokeWithTarget:_forwardingTextView]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector +{ + if ([_forwardingTextView respondsToSelector:selector]) { + return [_forwardingTextView methodSignatureForSelector:selector]; + } + + return [super methodSignatureForSelector:selector]; +} + +- (void)boundsDidChange:(NSNotification *)notification +{ +} + +#pragma mark - +#pragma mark First Responder forwarding + +- (NSResponder *)responder +{ + return _forwardingTextView; +} + +- (BOOL)acceptsFirstResponder +{ + return _forwardingTextView.acceptsFirstResponder; +} + +- (BOOL)becomeFirstResponder +{ + return [_forwardingTextView becomeFirstResponder]; +} + +- (BOOL)resignFirstResponder +{ + return [_forwardingTextView resignFirstResponder]; +} + +#pragma mark - +#pragma mark Text Input delegate forwarding + +- (id)textInputDelegate +{ + return _forwardingTextView.textInputDelegate; +} + +- (void)setTextInputDelegate:(id)textInputDelegate +{ + _forwardingTextView.textInputDelegate = textInputDelegate; +} + +#pragma mark - +#pragma mark Scrolling control + +#if TARGET_OS_OSX // [macOS +- (void)scrollViewDidScroll:(NSNotification *)notification +{ + [self.textInputDelegate scrollViewDidScroll:_scrollView]; +} +#endif // macOS] + +- (BOOL)scrollEnabled +{ + return _scrollView.isScrollEnabled; +} + +- (void)setScrollEnabled:(BOOL)scrollEnabled +{ + if (scrollEnabled) { + _scrollView.scrollEnabled = YES; + [_clipView setConstrainScrolling:NO]; + } else { + _scrollView.scrollEnabled = NO; + [_clipView setConstrainScrolling:YES]; + } +} + +- (BOOL)shouldShowVerticalScrollbar +{ + // Hide vertical scrollbar if explicity set to NO + if (self.hideVerticalScrollIndicator) { + return NO; + } + + // Hide vertical scrollbar if attributed text overflows view + CGSize textViewSize = [_forwardingTextView intrinsicContentSize]; + NSClipView *clipView = (NSClipView *)_scrollView.contentView; + if (textViewSize.height > clipView.bounds.size.height) { + return YES; + }; + + return NO; +} + +- (void)textInputDidChange +{ + [_scrollView setHasVerticalScroller:[self shouldShowVerticalScrollbar]]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + [_forwardingTextView setAttributedText:attributedText]; + [_scrollView setHasVerticalScroller:[self shouldShowVerticalScrollbar]]; +} + +#pragma mark - +#pragma mark Text Container Inset override for NSTextView + +// This method is there to match the textContainerInset property on RCTUITextField +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInsets +{ + // RCTUITextView has logic in setTextContainerInset[s] to convert th UIEdgeInsets to a valid NSSize struct + _forwardingTextView.textContainerInsets = textContainerInsets; +} + +#pragma mark - +#pragma mark Focus ring + +- (BOOL)enableFocusRing +{ + return _scrollView.enableFocusRing; +} + +- (void)setEnableFocusRing:(BOOL)enableFocusRing +{ + _scrollView.enableFocusRing = enableFocusRing; +} + +@end + +#endif // macOS] diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm index aab2d70e9e7650..5f50c67472dadb 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm @@ -391,17 +391,17 @@ - (void)textViewDidChangeSelection:(__unused UITextView *)textView [self textViewProbablyDidChangeSelection]; } +#endif // [macOS] + #pragma mark - UIScrollViewDelegate -- (void)scrollViewDidScroll:(UIScrollView *)scrollView +- (void)scrollViewDidScroll:(RCTUIScrollView *)scrollView // [macOS] { if ([_backedTextInputView.textInputDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) { [_backedTextInputView.textInputDelegate scrollViewDidScroll:scrollView]; } } -#endif // [macOS] - #if TARGET_OS_OSX // [macOS #pragma mark - NSTextViewDelegate diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h index 6807acc06235fc..97777009786f85 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h @@ -38,6 +38,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign, readonly) BOOL textWasPasted; #else // [macOS @property (nonatomic, assign) BOOL textWasPasted; +@property (nonatomic, readonly) NSResponder *responder; +@property (nonatomic, assign) BOOL enableFocusRing; #endif // macOS] @property (nonatomic, assign, readonly) BOOL dictationRecognizing; @property (nonatomic, assign) UIEdgeInsets textContainerInset; @@ -62,6 +64,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign, readonly) UIEdgeInsets contentInset; #if TARGET_OS_OSX // [macOS @property (nonatomic, assign) CGFloat pointScaleFactor; +@property (nonatomic, getter=isAutomaticSpellingCorrectionEnabled) BOOL automaticSpellingCorrectionEnabled; +@property (nonatomic, getter=isGrammarCheckingEnabled) BOOL grammarCheckingEnabled; +@property (nonatomic, getter=isContinuousSpellCheckingEnabled) BOOL continuousSpellCheckingEnabled; #endif // macOS] // This protocol disallows direct access to `selectedTextRange` property because @@ -81,6 +86,7 @@ NS_ASSUME_NONNULL_BEGIN #if TARGET_OS_OSX // [macOS // UITextInput method for OSX - (CGSize)sizeThatFits:(CGSize)size; +- (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes; #endif // macOS] // This protocol disallows direct access to `text` property because diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h index ae55ec1c8b0683..c085e78eca23a2 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h @@ -45,7 +45,7 @@ NS_ASSUME_NONNULL_BEGIN #if !TARGET_OS_OSX // [macOS] @property (nonatomic, assign, getter=isEditable) BOOL editable; #else // [macOS -@property (assign, getter=isEditable) BOOL editable; +@property (atomic, assign, getter=isEditable) BOOL editable; #endif // macOS] @property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled; @property (nonatomic, strong, nullable) NSString *inputAccessoryViewID; @@ -56,7 +56,7 @@ NS_ASSUME_NONNULL_BEGIN #if TARGET_OS_OSX // [macOS @property (nonatomic, copy, nullable) NSString *text; @property (nonatomic, copy, nullable) NSAttributedString *attributedText; -@property (nonatomic, copy) NSDictionary *defaultTextAttributes; +@property (nonatomic, strong, nullable) NSDictionary *defaultTextAttributes; @property (nonatomic, assign) NSTextAlignment textAlignment; @property (nonatomic, getter=isAutomaticTextReplacementEnabled) BOOL automaticTextReplacementEnabled; @property (nonatomic, getter=isAutomaticSpellingCorrectionEnabled) BOOL automaticSpellingCorrectionEnabled; @@ -66,6 +66,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong, nullable) RCTUIColor *selectionColor; @property (weak, nullable) id delegate; @property (nonatomic, assign) CGFloat pointScaleFactor; + +- (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes; #endif // macOS] @property (nonatomic, getter=isGhostTextChanging) BOOL ghostTextChanging; // [macOS] diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm index b85551c58b7ab0..112436a7a85ad0 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm @@ -93,6 +93,9 @@ @implementation RCTUITextField { #endif RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; NSDictionary *_defaultTextAttributes; +#if TARGET_OS_OSX // [macOS + NSArray *_readablePasteboardTypes; +#endif // macOS] } #if TARGET_OS_OSX // [macOS @@ -196,6 +199,11 @@ - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset #if TARGET_OS_OSX // [macOS +- (NSResponder *)responder +{ + return self; +} + + (Class)cellClass { return RCTUITextFieldCell.class; @@ -660,5 +668,12 @@ - (void)keyUp:(NSEvent *)event { } } #endif // macOS] + +#if TARGET_OS_OSX // [macOS +- (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes +{ + _readablePasteboardTypes = readablePasteboardTypes; +} +#endif // macOS] @end diff --git a/packages/react-native/React/Base/macOS/RCTUIKit.m b/packages/react-native/React/Base/macOS/RCTUIKit.m index 01c96db75eeeca..ae08d1d9e29172 100644 --- a/packages/react-native/React/Base/macOS/RCTUIKit.m +++ b/packages/react-native/React/Base/macOS/RCTUIKit.m @@ -216,6 +216,7 @@ @implementation RCTUIView BOOL _clipsToBounds; BOOL _userInteractionEnabled; BOOL _mouseDownCanMoveWindow; + BOOL _respondsToDisplayLayer; } + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key @@ -245,6 +246,7 @@ @implementation RCTUIView self->_userInteractionEnabled = YES; self->_enableFocusRing = YES; self->_mouseDownCanMoveWindow = YES; + self->_respondsToDisplayLayer = [self respondsToSelector:@selector(displayLayer:)]; } return self; } @@ -343,7 +345,12 @@ - (void)updateLayer // so it has to be reset from the view's NSColor ivar. [layer setBackgroundColor:[_backgroundColor CGColor]]; } - [(id)self displayLayer:layer]; + + // In Fabric, wantsUpdateLayer is always enabled and doesn't guarantee that + // the instance has a displayLayer method. + if (_respondsToDisplayLayer) { + [(id)self displayLayer:layer]; + } } - (void)drawRect:(CGRect)rect diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm index 8377b7b3728716..3a25c02b2e7fb5 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm @@ -181,8 +181,14 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask [super finalizeUpdates:updateMask]; if (!_adapter) { +#if !TARGET_OS_OSX // [macOS] _adapter = [[RCTLegacyViewManagerInteropCoordinatorAdapter alloc] initWithCoordinator:[self _coordinator] reactTag:self.tag]; +#else // [macOS + _adapter = [[RCTLegacyViewManagerInteropCoordinatorAdapter alloc] initWithCoordinator:[self _coordinator] + reactTag:self.reactTag.integerValue]; +#endif // macOS] + __weak __typeof(self) weakSelf = self; _adapter.eventInterceptor = ^(std::string eventName, folly::dynamic event) { if (weakSelf) { diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index 74026ca2292976..f30dd1c7f5f002 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -10,7 +10,9 @@ #if !TARGET_OS_OSX // [macOS] #import -#endif // [macOS] +#else // [macOS +#import +#endif // macOS] #import #import @@ -27,22 +29,54 @@ using namespace facebook::react; +#if TARGET_OS_OSX // [macOS +@interface RCTParagraphComponentUnfocusableTextView : NSTextView +@end + +@implementation RCTParagraphComponentUnfocusableTextView + +- (BOOL)canBecomeKeyView +{ + return NO; +} + +- (BOOL)resignFirstResponder +{ + // Don't relinquish first responder while selecting text. + if (self.selectable && NSRunLoop.currentRunLoop.currentMode == NSEventTrackingRunLoopMode) { + return NO; + } + + return [super resignFirstResponder]; +} + +@end +#endif // macOS] + #if !TARGET_OS_OSX // [macOS] @interface RCTParagraphComponentView () @property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0)); @end -#endif // [macOS] @implementation RCTParagraphComponentView { ParagraphShadowNode::ConcreteState::Shared _state; ParagraphAttributes _paragraphAttributes; RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; -#if !TARGET_OS_OSX // [macOS] UILongPressGestureRecognizer *_longPressGestureRecognizer; -#endif // [macOS] } +#else // [macOS +@interface RCTParagraphComponentView () +@end + +@implementation RCTParagraphComponentView { + ParagraphShadowNode::ConcreteState::Shared _state; + ParagraphAttributes _paragraphAttributes; + RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; + RCTParagraphComponentUnfocusableTextView *_textView; +} +#endif // macOS] - (instancetype)initWithFrame:(CGRect)frame { @@ -53,7 +87,27 @@ - (instancetype)initWithFrame:(CGRect)frame #if !TARGET_OS_OSX // [macOS] self.contentMode = UIViewContentModeRedraw; self.opaque = NO; -#endif // [macOS] +#else // [macOS + // Make the RCTParagraphComponentView accessible and available in the a11y hierarchy. + self.accessibilityElement = YES; + self.accessibilityRole = NSAccessibilityStaticTextRole; + // Fix blurry text on non-retina displays. + self.canDrawSubviewsIntoLayer = YES; + // The NSTextView is responsible for drawing text and managing selection. + _textView = [[RCTParagraphComponentUnfocusableTextView alloc] initWithFrame:self.bounds]; + _textView.delegate = self; + // The RCTParagraphComponentUnfocusableTextView is only used for rendering and should not appear in the a11y hierarchy. + _textView.accessibilityElement = NO; + _textView.usesFontPanel = NO; + _textView.drawsBackground = NO; + _textView.linkTextAttributes = @{}; + _textView.editable = NO; + _textView.selectable = NO; + _textView.verticallyResizable = NO; + _textView.layoutManager.usesFontLeading = NO; + self.contentView = _textView; + self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; +#endif // macOS] } return self; @@ -108,7 +162,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & } else { [self disableContextMenu]; } -#endif // [macOS] +#else // [macOS + _textView.selectable = newParagraphProps.isSelectable; +#endif // macOS] } [super updateProps:props oldProps:oldProps]; @@ -117,18 +173,77 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState { _state = std::static_pointer_cast(state); +#if !TARGET_OS_OSX // [macOS] [self setNeedsDisplay]; +#else // [macOS + [self _updateTextView]; +#endif // macOS] } +#if TARGET_OS_OSX // [macOS +- (void)updateLayoutMetrics:(LayoutMetrics const &)layoutMetrics + oldLayoutMetrics:(LayoutMetrics const &)oldLayoutMetrics +{ + [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; + [self _updateTextView]; +} + +- (void)_updateTextView +{ + if (!_state) { + return; + } + + auto textLayoutManager = _state->getData().paragraphLayoutManager.getTextLayoutManager(); + + if (!textLayoutManager) { + return; + } + + RCTTextLayoutManager *nativeTextLayoutManager = + (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); + + CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); + + NSTextStorage *textStorage = [nativeTextLayoutManager getTextStorageForAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes frame:frame]; + + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; + + [_textView replaceTextContainer:textContainer]; + + NSArray *managers = [[textStorage layoutManagers] copy]; + for (NSLayoutManager *manager in managers) { + [textStorage removeLayoutManager:manager]; + } + + _textView.minSize = frame.size; + _textView.maxSize = frame.size; + _textView.frame = frame; + _textView.textStorage.attributedString = textStorage; + + [self setNeedsDisplay]; +} +#endif // macOS] + - (void)prepareForRecycle { [super prepareForRecycle]; _state.reset(); _accessibilityProvider = nil; + +#if TARGET_OS_OSX // [macOS + // Clear the text view to avoid displaying the previous text on recycle with undefined text content. + _textView.string = @""; +#endif // macOS] } - (void)drawRect:(CGRect)rect { +#if TARGET_OS_OSX // [macOS + return; +#endif // macOS] + if (!_state) { return; } @@ -154,6 +269,7 @@ - (NSString *)accessibilityLabel return self.attributedText.string; } +#if !TARGET_OS_OSX // [macOS] - (BOOL)isAccessibilityElement { // All accessibility functionality of the component is implemented in `accessibilityElements` method below. @@ -162,7 +278,6 @@ - (BOOL)isAccessibilityElement return NO; } -#if !TARGET_OS_OSX // [macOS] - (NSArray *)accessibilityElements { const auto ¶graphProps = static_cast(*_props); @@ -267,14 +382,93 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture [menuController showMenuFromView:self rect:self.bounds]; } } -#endif // [macOS] +#else // [macOS +- (NSView *)hitTest:(CGPoint)point withEvent:(NSEvent *)event +{ + // 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). + NSView *hitView = [super hitTest:point withEvent:event]; + + NSEventType eventType = NSApp.currentEvent.type; + BOOL isMouseClickEvent = NSEvent.pressedMouseButtons > 0; + BOOL isMouseMoveEventType = eventType == NSEventTypeMouseMoved || eventType == NSEventTypeMouseEntered || eventType == NSEventTypeMouseExited || eventType == NSEventTypeCursorUpdate; + BOOL isMouseMoveEvent = !isMouseClickEvent && isMouseMoveEventType; + BOOL isTextViewClick = (hitView && hitView == _textView) && !isMouseMoveEvent; + + return isTextViewClick ? self : hitView; +} + +- (NSView *)hitTest:(NSPoint)point +{ + return [self hitTest:point withEvent:NSApp.currentEvent]; +} + +- (void)mouseDown:(NSEvent *)event +{ + if (!_textView.selectable) { + [super mouseDown:event]; + return; + } + // Double/triple-clicks should be forwarded to the NSTextView. + BOOL shouldForward = event.clickCount > 1; + + if (!shouldForward) { + // Peek at next event to know if a selection should begin. + NSEvent *nextEvent = [self.window nextEventMatchingMask:NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged + untilDate:[NSDate distantFuture] + inMode:NSEventTrackingRunLoopMode + dequeue:NO]; + shouldForward = nextEvent.type == NSEventTypeLeftMouseDragged; + } + + if (shouldForward) { + NSView *contentView = self.window.contentView; + // -[NSView hitTest:] takes coordinates in a view's superview coordinate system. + NSPoint point = [contentView.superview convertPoint:event.locationInWindow fromView:nil]; + + // Start selection if we're still selectable and hit-testable. + if (_textView.selectable && [contentView hitTest:point] == self) { + [self.window makeFirstResponder:_textView]; + [_textView mouseDown:event]; + } + } else { + // Clear selection for single clicks. + _textView.selectedRange = NSMakeRange(NSNotFound, 0); + } +} + +#pragma mark - Selection + +- (void)textDidEndEditing:(NSNotification *)notification +{ + _textView.selectedRange = NSMakeRange(NSNotFound, 0); +} + +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] - (BOOL)canBecomeFirstResponder { const auto ¶graphProps = static_cast(*_props); return paragraphProps.isSelectable; } +#else +- (BOOL)becomeFirstResponder +{ + if (![super becomeFirstResponder]) { + return NO; + } + return YES; +} + +- (BOOL)canBecomeFirstResponder +{ + return self.focusable; +} +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { const auto ¶graphProps = static_cast(*_props); @@ -289,6 +483,7 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return NO; #endif // macOS] } +#endif // [macOS] - (void)copy:(id)sender { diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index 2c12cfa47909ab..c88fcbf299b33b 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -12,9 +12,20 @@ #import #import + +#if !TARGET_OS_OSX // [macOS] #import +#else // [macOS +#include +#include +#endif // macOS] + #import #import +#if TARGET_OS_OSX // [macOS +#import +#import +#endif // macOS] #import "RCTConversions.h" #import "RCTTextInputNativeCommands.h" @@ -70,7 +81,11 @@ - (instancetype)initWithFrame:(CGRect)frame _props = defaultProps; auto &props = *defaultProps; +#if !TARGET_OS_OSX // [macOS] _backedTextInputView = props.traits.multiline ? [RCTUITextView new] : [RCTUITextField new]; +#else // [macOS + _backedTextInputView = props.traits.multiline ? [[RCTWrappedTextView alloc] initWithFrame:self.bounds] : [RCTUITextField new]; +#endif // macOS] _backedTextInputView.textInputDelegate = self; _ignoreNextTextInputCall = NO; _comingFromJS = NO; @@ -90,7 +105,12 @@ - (void)didMoveToWindow if (props.autoFocus) { #if !TARGET_OS_OSX // [macOS] [_backedTextInputView becomeFirstResponder]; -#endif // [macOS] +#else // [macOS + NSWindow *window = _backedTextInputView.window; + if (window) { + [window makeFirstResponder:_backedTextInputView.responder]; + } +#endif // macOS] } _didMoveToWindow = YES; } @@ -127,13 +147,20 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _backedTextInputView.autocapitalizationType = RCTUITextAutocapitalizationTypeFromAutocapitalizationType(newTextInputProps.traits.autocapitalizationType); } +#endif +#if !TARGET_OS_OSX // [macOS] if (newTextInputProps.traits.autoCorrect != oldTextInputProps.traits.autoCorrect) { _backedTextInputView.autocorrectionType = RCTUITextAutocorrectionTypeFromOptionalBool(newTextInputProps.traits.autoCorrect); } -#endif // [macOS] - +#else // [macOS + if (newTextInputProps.traits.autoCorrect != oldTextInputProps.traits.autoCorrect && newTextInputProps.traits.autoCorrect.has_value()) { + _backedTextInputView.automaticSpellingCorrectionEnabled = + newTextInputProps.traits.autoCorrect.value(); + } +#endif // macOS] + if (newTextInputProps.traits.contextMenuHidden != oldTextInputProps.traits.contextMenuHidden) { _backedTextInputView.contextMenuHidden = newTextInputProps.traits.contextMenuHidden; } @@ -152,12 +179,26 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _backedTextInputView.keyboardAppearance = RCTUIKeyboardAppearanceFromKeyboardAppearance(newTextInputProps.traits.keyboardAppearance); } - +#endif + +#if !TARGET_OS_OSX // [macOS] if (newTextInputProps.traits.spellCheck != oldTextInputProps.traits.spellCheck) { _backedTextInputView.spellCheckingType = RCTUITextSpellCheckingTypeFromOptionalBool(newTextInputProps.traits.spellCheck); } -#endif // [macOS] +#else // [macOS + if (newTextInputProps.traits.spellCheck != oldTextInputProps.traits.spellCheck && newTextInputProps.traits.spellCheck.has_value()) { + _backedTextInputView.continuousSpellCheckingEnabled = + newTextInputProps.traits.spellCheck.value(); + } +#endif // macOS] + +#if TARGET_OS_OSX // [macOS + if (newTextInputProps.traits.grammarCheck != oldTextInputProps.traits.grammarCheck && newTextInputProps.traits.grammarCheck.has_value()) { + _backedTextInputView.grammarCheckingEnabled = + newTextInputProps.traits.grammarCheck.value(); + } +#endif // macOS] if (newTextInputProps.traits.caretHidden != oldTextInputProps.traits.caretHidden) { _backedTextInputView.caretHidden = newTextInputProps.traits.caretHidden; @@ -174,11 +215,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _backedTextInputView.scrollEnabled = newTextInputProps.traits.scrollEnabled; } -#if !TARGET_OS_OSX // [macOS] if (newTextInputProps.traits.secureTextEntry != oldTextInputProps.traits.secureTextEntry) { +#if !TARGET_OS_OSX // [macOS] _backedTextInputView.secureTextEntry = newTextInputProps.traits.secureTextEntry; +#else // [macOS + [self _setSecureTextEntry:newTextInputProps.traits.secureTextEntry]; +#endif // macOS] } +#if !TARGET_OS_OSX // [macOS] if (newTextInputProps.traits.keyboardType != oldTextInputProps.traits.keyboardType) { _backedTextInputView.keyboardType = RCTUIKeyboardTypeFromKeyboardType(newTextInputProps.traits.keyboardType); } @@ -227,6 +272,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) { _backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID); } + +#if TARGET_OS_OSX // [macOS + if (newTextInputProps.traits.pastedTypes!= oldTextInputProps.traits.pastedTypes) { + NSArray *types = RCTPasteboardTypeArrayFromProps(newTextInputProps.traits.pastedTypes); + [_backedTextInputView setReadablePasteBoardTypes:types]; + } +#endif // macOS] + [super updateProps:props oldProps:oldProps]; [self setDefaultInputAccessoryView]; @@ -438,49 +491,171 @@ - (void)textInputDidChangeSelection } #if TARGET_OS_OSX // [macOS -- (void)automaticSpellingCorrectionDidChange:(BOOL)enabled {} +- (void)setEnableFocusRing:(BOOL)enableFocusRing { + [super setEnableFocusRing:enableFocusRing]; + if ([_backedTextInputView respondsToSelector:@selector(setEnableFocusRing:)]) { + [_backedTextInputView setEnableFocusRing:enableFocusRing]; + } +} +- (void)automaticSpellingCorrectionDidChange:(BOOL)enabled { + if (_eventEmitter) { + std::static_pointer_cast(_eventEmitter)->onAutoCorrectChange({.enabled = static_cast(enabled)}); + } +} -- (void)continuousSpellCheckingDidChange:(BOOL)enabled {} +- (void)continuousSpellCheckingDidChange:(BOOL)enabled +{ + if (_eventEmitter) { + std::static_pointer_cast(_eventEmitter)->onSpellCheckChange({.enabled = static_cast(enabled)}); + } +} +- (void)grammarCheckingDidChange:(BOOL)enabled +{ + if (_eventEmitter) { + std::static_pointer_cast(_eventEmitter)->onGrammarCheckChange({.enabled = static_cast(enabled)}); + } +} -- (void)grammarCheckingDidChange:(BOOL)enabled {} +- (BOOL)hasValidKeyDownOrValidKeyUp:(nonnull NSString *)key { + std::string keyString = key.UTF8String; + if (_props->validKeysDown.has_value()) { + for (auto const &validKey : *_props->validKeysDown) { + if (validKey.key == keyString) { + return YES; + } + } + } -- (BOOL)hasValidKeyDownOrValidKeyUp:(nonnull NSString *)key { - return YES; + if (_props->validKeysUp.has_value()) { + for (auto const &validKey : *_props->validKeysUp) { + if (validKey.key == keyString) { + return YES; + } + } + } + + return NO; } -- (void)submitOnKeyDownIfNeeded:(nonnull NSEvent *)event {} +- (void)submitOnKeyDownIfNeeded:(nonnull NSEvent *)event +{ + BOOL shouldSubmit = NO; + NSDictionary *keyEvent = [RCTViewKeyboardEvent bodyFromEvent:event]; + auto const &props = *std::static_pointer_cast(_props); + if (props.traits.submitKeyEvents.empty()) { + shouldSubmit = [keyEvent[@"key"] isEqualToString:@"Enter"] + && ![keyEvent[@"altKey"] boolValue] + && ![keyEvent[@"shiftKey"] boolValue] + && ![keyEvent[@"ctrlKey"] boolValue] + && ![keyEvent[@"metaKey"] boolValue] + && ![keyEvent[@"functionKey"] boolValue]; // Default clearTextOnSubmit key + } else { + NSString *keyValue = keyEvent[@"key"]; + NSUInteger keyValueLength = [keyValue lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + std::string key = std::string([keyValue UTF8String], keyValueLength); + for (auto const &submitKeyEvent : props.traits.submitKeyEvents) { + if ( + submitKeyEvent.key == key && + submitKeyEvent.altKey == [keyEvent[@"altKey"] boolValue] && + submitKeyEvent.shiftKey == [keyEvent[@"shiftKey"] boolValue] && + submitKeyEvent.ctrlKey == [keyEvent[@"ctrlKey"] boolValue] && + submitKeyEvent.metaKey == [keyEvent[@"metaKey"] boolValue] && + submitKeyEvent.functionKey == [keyEvent[@"functionKey"] boolValue] + ) { + shouldSubmit = YES; + break; + } + } + } + + if (shouldSubmit) { + if (_eventEmitter) { + auto const &textInputEventEmitter = *std::static_pointer_cast(_eventEmitter); + textInputEventEmitter.onSubmitEditing([self _textInputMetrics]); + } + + if (props.traits.clearTextOnSubmit) { + _backedTextInputView.attributedText = nil; + [self textInputDidChange]; + } + } +} -- (void)textInputDidCancel {} +- (void)textInputDidCancel +{ + if (_eventEmitter) { + KeyPressMetrics keyPressMetrics; + keyPressMetrics.text = RCTStringFromNSString(@"\x1B"); // Escape key + keyPressMetrics.eventCount = _mostRecentEventCount; + + auto const &textInputEventEmitter = *std::static_pointer_cast(_eventEmitter); + auto const &props = *std::static_pointer_cast(_props); + if (props.onKeyPressSync) { + textInputEventEmitter.onKeyPressSync(keyPressMetrics); + } else { + textInputEventEmitter.onKeyPress(keyPressMetrics); + } + } + + [self textInputDidEndEditing]; +} - (NSDragOperation)textInputDraggingEntered:(nonnull id)draggingInfo { + if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) { + return [self draggingEntered:draggingInfo]; + } return NSDragOperationNone; } - (void)textInputDraggingExited:(nonnull id)draggingInfo { - return; + if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) { + [self draggingExited:draggingInfo]; + } } -- (BOOL)textInputShouldHandleDeleteBackward:(nonnull id)sender { +- (BOOL)textInputShouldHandleDragOperation:(nonnull id)draggingInfo { + if ([draggingInfo.draggingPasteboard availableTypeFromArray:self.registeredDraggedTypes]) { + [self performDragOperation:draggingInfo]; + return NO; + } + return YES; } -- (BOOL)textInputShouldHandleDeleteForward:(nonnull id)sender { +- (BOOL)textInputShouldHandleDeleteBackward:(nonnull id)sender { return YES; } -- (BOOL)textInputShouldHandleDragOperation:(nonnull id)draggingInfo { +- (BOOL)textInputShouldHandleDeleteForward:(nonnull id)sender { return YES; } - (BOOL)textInputShouldHandleKeyEvent:(nonnull NSEvent *)event { - return YES; + return ![self handleKeyboardEvent:event]; } - (BOOL)textInputShouldHandlePaste:(nonnull id)sender { - return YES; + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + NSPasteboardType fileType = [pasteboard availableTypeFromArray:@[NSFilenamesPboardType, NSPasteboardTypePNG, NSPasteboardTypeTIFF]]; + NSArray* pastedTypes = ((RCTUITextView*) _backedTextInputView).readablePasteboardTypes; + + // If there's a fileType that is of interest, notify JS. Also blocks notifying JS if it's a text paste + if (_eventEmitter && fileType != nil && [pastedTypes containsObject:fileType]) { + auto const &textInputEventEmitter = *std::static_pointer_cast(_eventEmitter); + std::vector dataTransferItems{}; + [self buildDataTransferItems:dataTransferItems forPasteboard:pasteboard]; + + TextInputEventEmitter::PasteEvent pasteEvent = { + .dataTransferItems = dataTransferItems, + }; + textInputEventEmitter.onPaste(pasteEvent); + } + + // Only allow pasting text. + return fileType == nil; } #endif // macOS] @@ -490,7 +665,28 @@ - (BOOL)textInputShouldHandlePaste:(nonnull id)s - (void)scrollViewDidScroll:(RCTUIScrollView *)scrollView // [macOS] { if (_eventEmitter) { +#if !TARGET_OS_OSX // [macOS] static_cast(*_eventEmitter).onScroll([self _textInputMetrics]); +#else // [macOS + TextInputMetrics metrics = [self _textInputMetrics]; // [macOS] + + CGPoint contentOffset = scrollView.contentOffset; + metrics.contentOffset = {contentOffset.x, contentOffset.y}; + + UIEdgeInsets contentInset = scrollView.contentInset; + metrics.contentInset = {contentInset.left, contentInset.top, contentInset.right, contentInset.bottom}; + + CGSize contentSize = scrollView.contentSize; + metrics.contentSize = {contentSize.width, contentSize.height}; + + CGSize layoutMeasurement = scrollView.bounds.size; + metrics.layoutMeasurement = {layoutMeasurement.width, layoutMeasurement.height}; + + CGFloat zoomScale = scrollView.zoomScale ?: 1; + metrics.zoomScale = zoomScale; + + static_cast(*_eventEmitter).onScroll(metrics); +#endif // macOS] } } @@ -503,12 +699,27 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args - (void)focus { +#if !TARGET_OS_OSX // [macOS] [_backedTextInputView becomeFirstResponder]; +#else // [macOS + NSWindow *window = _backedTextInputView.window; + if (window) { + [window makeFirstResponder:_backedTextInputView.responder]; + } +#endif // macOS] } - (void)blur { +#if !TARGET_OS_OSX // [macOS] [_backedTextInputView resignFirstResponder]; +#else + NSWindow *window = _backedTextInputView.window; + if (window && window.firstResponder == _backedTextInputView.responder) { + // Calling makeFirstResponder with nil will call resignFirstResponder and make the window the first responder + [window makeFirstResponder:nil]; + } +#endif // macOS]; } - (void)setTextAndSelection:(NSInteger)eventCount @@ -537,7 +748,11 @@ - (void)setTextAndSelection:(NSInteger)eventCount UITextRange *range = [_backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition]; [_backedTextInputView setSelectedTextRange:range notifyDelegate:NO]; } -#endif // [macOS] +#else // [macOS + NSInteger startPosition = MIN(start, end); + NSInteger endPosition = MAX(start, end); + [_backedTextInputView setSelectedTextRange:NSMakeRange(startPosition, endPosition - startPosition) notifyDelegate:YES]; +#endif // macOS] _comingFromJS = NO; } @@ -654,8 +869,8 @@ - (void)_updateState toPosition:selectedTextRange.end]; return AttributedString::Range{(int)start, (int)(end - start)}; #else // [macOS - // [Fabric] Placeholder till we implement selection in Fabric - return AttributedString::Range({0, 1}); + NSRange selectedTextRange = [_backedTextInputView selectedTextRange]; + return AttributedString::Range{(int)selectedTextRange.location, (int)selectedTextRange.length}; #endif // macOS] } @@ -676,13 +891,27 @@ - (void)_restoreTextSelection - (void)_setAttributedString:(NSAttributedString *)attributedString { +#if TARGET_OS_OSX // [macOS + // When the text view displays temporary content (e.g. completions, accents), do not update the attributed string. + if (_backedTextInputView.hasMarkedText) { + return; + } +#endif // macOS] + if ([self _textOf:attributedString equals:_backedTextInputView.attributedText]) { return; } #if !TARGET_OS_OSX // [macOS] UITextRange *selectedRange = _backedTextInputView.selectedTextRange; - NSInteger oldTextLength = _backedTextInputView.attributedText.string.length; +#else + NSRange selection = [_backedTextInputView selectedTextRange]; +#endif // macOS] + NSAttributedString *oldAttributedText = [_backedTextInputView.attributedText copy]; + NSInteger oldTextLength = oldAttributedText.string.length; + _backedTextInputView.attributedText = attributedString; + +#if !TARGET_OS_OSX // [macOS] if (selectedRange.empty) { // Maintaining a cursor position relative to the end of the old text. NSInteger offsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument @@ -696,7 +925,16 @@ - (void)_setAttributedString:(NSAttributedString *)attributedString } [self _restoreTextSelection]; _lastStringStateWasUpdatedWith = attributedString; -#endif // [macOS] +#else // [macOS + if (selection.length == 0) { + // Maintaining a cursor position relative to the end of the old text. + NSInteger start = selection.location; + NSInteger offsetFromEnd = oldTextLength - start; + NSInteger newOffset = _backedTextInputView.attributedText.length - offsetFromEnd; + [_backedTextInputView setSelectedTextRange:NSMakeRange(newOffset, 0) + notifyDelegate:YES]; + } +#endif // macOS] } - (void)_setMultiline:(BOOL)multiline @@ -705,7 +943,7 @@ - (void)_setMultiline:(BOOL)multiline #if !TARGET_OS_OSX // [macOS] RCTUIView *backedTextInputView = multiline ? [RCTUITextView new] : [RCTUITextField new]; #else // [macOS - RCTUITextView *backedTextInputView = [RCTUITextView new]; + RCTPlatformView *backedTextInputView = multiline ? [RCTWrappedTextView new] : [RCTUITextField new]; #endif // macOS] backedTextInputView.frame = _backedTextInputView.frame; RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView); @@ -713,6 +951,27 @@ - (void)_setMultiline:(BOOL)multiline [self addSubview:_backedTextInputView]; } +#if TARGET_OS_OSX // [macOS +- (void)_setSecureTextEntry:(BOOL)secureTextEntry +{ + [_backedTextInputView removeFromSuperview]; + RCTPlatformView *backedTextInputView = secureTextEntry ? [RCTUISecureTextField new] : [RCTUITextField new]; + backedTextInputView.frame = _backedTextInputView.frame; + RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView); + + // Copy the text field specific properties if we came from a single line input before the switch + if ([_backedTextInputView isKindOfClass:[RCTUITextField class]]) { + RCTUITextField *previousTextField = (RCTUITextField *)_backedTextInputView; + RCTUITextField *newTextField = (RCTUITextField *)backedTextInputView; + newTextField.textAlignment = previousTextField.textAlignment; + newTextField.text = previousTextField.text; + } + + _backedTextInputView = backedTextInputView; + [self addSubview:_backedTextInputView]; +} +#endif // macOS] + - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText { // When the dictation is running we can't update the attributed text on the backed up text view diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h index fd4bb5fd03f2c3..906e1b47b0faa9 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h @@ -22,8 +22,8 @@ void RCTCopyBackedTextInput( RCTUIView *fromTextInput, RCTUIView *toTextInput #else // [macOS - RCTUITextView *fromTextInput, - RCTUITextView *toTextInput + RCTPlatformView *fromTextInput, + RCTPlatformView *toTextInput #endif // macOS] ); diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm index 319ec09fedcfa5..c890d602822bed 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm @@ -23,8 +23,8 @@ void RCTCopyBackedTextInput( RCTUIView *fromTextInput, RCTUIView *toTextInput #else // [macOS - RCTUITextView *fromTextInput, - RCTUITextView *toTextInput + RCTPlatformView *fromTextInput, + RCTPlatformView *toTextInput #endif // macOS] ) { @@ -32,6 +32,15 @@ void RCTCopyBackedTextInput( toTextInput.placeholder = fromTextInput.placeholder; toTextInput.placeholderColor = fromTextInput.placeholderColor; toTextInput.textContainerInset = fromTextInput.textContainerInset; + +#if TARGET_OS_OSX // [macOS + toTextInput.accessibilityElement = fromTextInput.accessibilityElement; + toTextInput.accessibilityHelp = fromTextInput.accessibilityHelp; + toTextInput.accessibilityIdentifier = fromTextInput.accessibilityIdentifier; + toTextInput.accessibilityLabel = fromTextInput.accessibilityLabel; + toTextInput.accessibilityRole = fromTextInput.accessibilityRole; + toTextInput.autoresizingMask = fromTextInput.autoresizingMask; +#endif // macOS] #if TARGET_OS_IOS // [macOS] [visionOS] toTextInput.inputAccessoryView = fromTextInput.inputAccessoryView; #endif // [macOS] [visionOS] diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h index 7a903401112fa8..6479fccd17f476 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -56,7 +56,7 @@ NS_ASSUME_NONNULL_BEGIN * transparent in favour of some subview. * Defaults to `self`. */ -@property (nonatomic, strong, nullable, readonly) NSObject *accessibilityElement; +@property (nonatomic, strong, nullable, readonly) RCTPlatformView *accessibilityElement; // [macOS] /** * Insets used when hit testing inside this view. @@ -75,6 +75,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask NS_REQUIRES_SUPER; - (void)prepareForRecycle NS_REQUIRES_SUPER; +#if TARGET_OS_OSX // [macOS +- (BOOL)handleKeyboardEvent:(NSEvent *)event; +- (void)buildDataTransferItems:(std::vector &)dataTransferItems forPasteboard:(NSPasteboard *)pasteboard; +#endif // macOS] + /* * This is a fragment of temporary workaround that we need only temporary and will get rid of soon. */ diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index afd8d23a5d989a..8d18b7ee2822e8 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -16,6 +16,11 @@ #import #import // [macOS] #import +#import // [macOS] +#import // [macOS] +#if TARGET_OS_OSX // [macOS +#import // [macOS] +#endif // macOS] #import #import #import @@ -33,6 +38,10 @@ @implementation RCTViewComponentView { BOOL _needsInvalidateLayer; BOOL _isJSResponder; BOOL _removeClippedSubviews; + BOOL _hasMouseOver; // [macOS] + BOOL _hasClipViewBoundsObserver; // [macOS] + NSTrackingArea *_trackingArea; // [macOS] + NSCursor *_cursor; // [macOS] NSMutableArray *_reactSubviews; // [macOS] NSSet *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN; } @@ -273,7 +282,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & // `cursor` if (oldViewProps.cursor != newViewProps.cursor) { - needsInvalidateLayer = YES; +#if !TARGET_OS_OSX // [macOS] + needsInvalidateLayer = YES; // `cursor` +#else // [macOS + _cursor = NSCursorFromCursor(newViewProps.cursor); +#endif // macOS] } // `shouldRasterize` @@ -296,7 +309,17 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & oldViewProps.transformOrigin != newViewProps.transformOrigin) && ![_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"transform"]) { auto newTransform = newViewProps.resolveTransform(_layoutMetrics); - self.layer.transform = RCTCATransform3DFromTransformMatrix(newTransform); + CATransform3D transform = RCTCATransform3DFromTransformMatrix(newTransform); +#if TARGET_OS_OSX // [macOS + CGPoint anchorPoint = self.layer.anchorPoint; + if (CGPointEqualToPoint(anchorPoint, CGPointZero) && !CATransform3DEqualToTransform(transform, CATransform3DIdentity)) { + // https://developer.apple.com/documentation/quartzcore/calayer/1410817-anchorpoint + // This compensates for the fact that layer.anchorPoint is {0, 0} instead of {0.5, 0.5} on macOS for some reason. + CATransform3D originAdjust = CATransform3DTranslate(CATransform3DIdentity, self.frame.size.width / 2, self.frame.size.height / 2, 0); + transform = CATransform3DConcat(CATransform3DConcat(CATransform3DInvert(originAdjust), transform), originAdjust); + } +#endif // macOS] + self.layer.transform = transform; self.layer.allowsEdgeAntialiasing = newViewProps.transform != Transform::Identity(); } @@ -326,10 +349,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & self.nativeId = RCTNSStringFromStringNilIfEmpty(newViewProps.nativeId); } -#if !TARGET_OS_OSX // [macOS] // `accessible` if (oldViewProps.accessible != newViewProps.accessible) { +#if !TARGET_OS_OSX // [macOS] self.accessibilityElement.isAccessibilityElement = newViewProps.accessible; +#else // [macOS + self.accessibilityElement.accessibilityElement = newViewProps.accessible; +#endif // macOS] } // `accessibilityLabel` @@ -345,9 +371,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & // `accessibilityHint` if (oldViewProps.accessibilityHint != newViewProps.accessibilityHint) { +#if !TARGET_OS_OSX // [macOS] self.accessibilityElement.accessibilityHint = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint); +#else // [macOS + self.accessibilityElement.accessibilityHelp = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint); +#endif // macOS] } +#if !TARGET_OS_OSX // [macOS] // `accessibilityViewIsModal` if (oldViewProps.accessibilityViewIsModal != newViewProps.accessibilityViewIsModal) { self.accessibilityElement.accessibilityViewIsModal = newViewProps.accessibilityViewIsModal; @@ -357,13 +388,19 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if (oldViewProps.accessibilityElementsHidden != newViewProps.accessibilityElementsHidden) { self.accessibilityElement.accessibilityElementsHidden = newViewProps.accessibilityElementsHidden; } +#endif // [macOS] // `accessibilityTraits` if (oldViewProps.accessibilityTraits != newViewProps.accessibilityTraits) { +#if !TARGET_OS_OSX // [macOS] self.accessibilityElement.accessibilityTraits = RCTUIAccessibilityTraitsFromAccessibilityTraits(newViewProps.accessibilityTraits); +#else // [macOS + self.accessibilityElement.accessibilityRole = RCTUIAccessibilityRoleFromAccessibilityTraits(newViewProps.accessibilityTraits); +#endif // macOS] } +#if !TARGET_OS_OSX // [macOS] // `accessibilityState` if (oldViewProps.accessibilityState != newViewProps.accessibilityState) { self.accessibilityTraits &= ~(UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected); @@ -379,6 +416,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if (oldViewProps.accessibilityIgnoresInvertColors != newViewProps.accessibilityIgnoresInvertColors) { self.accessibilityIgnoresInvertColors = newViewProps.accessibilityIgnoresInvertColors; } +#endif // [macOS] // `accessibilityValue` if (oldViewProps.accessibilityValue != newViewProps.accessibilityValue) { @@ -397,8 +435,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & self.accessibilityElement.accessibilityValue = nil; } } -#endif // [macOS] - + // `testId` if (oldViewProps.testId != newViewProps.testId) { self.accessibilityIdentifier = RCTNSStringFromString(newViewProps.testId); @@ -409,6 +446,45 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & self.layer.zPosition = newViewProps.zIndex.value_or(0); } +#if TARGET_OS_OSX // [macOS + // `focusable` + self.focusable = (bool)newViewProps.focusable; + // `enableFocusRing` + self.enableFocusRing = (bool)newViewProps.enableFocusRing; + + // `draggedTypes` + if (oldViewProps.draggedTypes != newViewProps.draggedTypes) { + if (oldViewProps.draggedTypes.has_value()) { + [self unregisterDraggedTypes]; + } + + if (newViewProps.draggedTypes.has_value()) { + NSMutableArray *pasteboardTypes = [NSMutableArray new]; + for (const auto &draggedType : *newViewProps.draggedTypes) { + if (draggedType == DraggedType::FileUrl) { + [pasteboardTypes addObject:NSFilenamesPboardType]; + } else if (draggedType == DraggedType::Image) { + [pasteboardTypes addObject:NSPasteboardTypePNG]; + [pasteboardTypes addObject:NSPasteboardTypeTIFF]; + } else if (draggedType == DraggedType::String) { + [pasteboardTypes addObject:NSPasteboardTypeString]; + } + } + [self registerForDraggedTypes:pasteboardTypes]; + } + } + + // `tooltip` + if (oldViewProps.tooltip != newViewProps.tooltip) { + if (newViewProps.tooltip.has_value()) { + self.toolTip = RCTNSStringFromStringNilIfEmpty(newViewProps.tooltip.value()); + } else { + self.toolTip = nil; + } + } + +#endif // macOS] + _needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer; _props = std::static_pointer_cast(props); @@ -463,6 +539,9 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask _needsInvalidateLayer = NO; [self invalidateLayer]; + + [self updateTrackingAreas]; + [self updateClipViewBoundsObserverIfNeeded]; } - (void)prepareForRecycle @@ -522,7 +601,16 @@ - (RCTUIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS } for (RCTUIView *subview in [self.subviews reverseObjectEnumerator]) { // [macOS] - RCTUIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event]; // [macOS] + // Native macOS views require the point to be in the super view coordinate space for hit testing. [macOS] + CGPoint hitTestPoint = point; +#if TARGET_OS_OSX // [macOS + // Paper and Fabric components use the target view coordinate space for hit testing + if ([subview isKindOfClass:[RCTView class]] || [subview isKindOfClass:[RCTViewComponentView class]]) { + hitTestPoint = [subview convertPoint:point fromView:self]; + } +#endif // macOS] + + RCTUIView *hitView = [subview hitTest:hitTestPoint withEvent:event]; // [macOS] if (hitView) { return hitView; } @@ -597,13 +685,17 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle) } #if TARGET_OS_OSX // [macOS - static NSCursor *NSCursorFromCursor(Cursor cursor) +static NSCursor *NSCursorFromCursor(Cursor cursor) { switch (cursor) { - case Cursor::Auto: - return [NSCursor arrowCursor]; case Cursor::Alias: return [NSCursor dragLinkCursor]; + case Cursor::Arrow: + return [NSCursor arrowCursor]; + case Cursor::Auto: + return [NSCursor arrowCursor]; + case Cursor::ClosedHand: + return [NSCursor closedHandCursor]; case Cursor::ColumnResize: return [NSCursor resizeLeftRightCursor]; case Cursor::ContextualMenu: @@ -616,20 +708,34 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle) return [NSCursor arrowCursor]; case Cursor::DisappearingItem: return [NSCursor disappearingItemCursor]; + case Cursor::DragCopy: + return [NSCursor dragCopyCursor]; + case Cursor::DragLink: + return [NSCursor dragLinkCursor]; case Cursor::EastResize: return [NSCursor resizeRightCursor]; case Cursor::Grab: return [NSCursor openHandCursor]; case Cursor::Grabbing: return [NSCursor closedHandCursor]; + case Cursor::IBeam: + return [NSCursor IBeamCursor]; + case Cursor::IBeamCursorForVerticalLayout: + return [NSCursor IBeamCursorForVerticalLayout]; case Cursor::NorthResize: return [NSCursor resizeUpCursor]; case Cursor::NoDrop: return [NSCursor operationNotAllowedCursor]; case Cursor::NotAllowed: return [NSCursor operationNotAllowedCursor]; + case Cursor::OpenHand: + return [NSCursor openHandCursor]; + case Cursor::OperationNotAllowed: + return [NSCursor operationNotAllowedCursor]; case Cursor::Pointer: return [NSCursor pointingHandCursor]; + case Cursor::PointingHand: + return [NSCursor pointingHandCursor]; case Cursor::RowResize: return [NSCursor resizeUpDownCursor]; case Cursor::SouthResize: @@ -640,11 +746,22 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle) return [NSCursor IBeamCursorForVerticalLayout]; case Cursor::WestResize: return [NSCursor resizeLeftCursor]; + case Cursor::ResizeDown: + return [NSCursor resizeDownCursor]; + case Cursor::ResizeLeft: + return [NSCursor resizeLeftCursor]; + case Cursor::ResizeLeftRight: + return [NSCursor resizeLeftRightCursor]; + case Cursor::ResizeRight: + return [NSCursor resizeRightCursor]; + case Cursor::ResizeUp: + return [NSCursor resizeUpCursor]; + case Cursor::ResizeUpDown: + return [NSCursor resizeDownCursor]; } } #endif // macOS] - - (void)invalidateLayer { CALayer *layer = self.layer; @@ -653,6 +770,11 @@ - (void)invalidateLayer return; } +#if TARGET_OS_OSX // [macOS + // clipsToBounds is stubbed out on macOS because it's not part of NSView + layer.masksToBounds = self.clipsToBounds; +#endif // macOS] + const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics); // Stage 1. Shadow Path @@ -814,7 +936,7 @@ - (void)invalidateLayer #pragma mark - Accessibility -- (NSObject *)accessibilityElement +- (RCTPlatformView *)accessibilityElement { return self; } @@ -985,6 +1107,421 @@ - (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)acti } } +#if TARGET_OS_OSX // [macOS + +#pragma mark - Focus Events + +- (BOOL)becomeFirstResponder +{ + if (![super becomeFirstResponder]) { + return NO; + } + + if (_eventEmitter) { + _eventEmitter->onFocus(); + } + + return YES; +} + +- (BOOL)resignFirstResponder +{ + if (![super resignFirstResponder]) { + return NO; + } + + if (_eventEmitter) { + _eventEmitter->onBlur(); + } + + return YES; +} + + +#pragma mark - Keyboard Events + +- (BOOL)handleKeyboardEvent:(NSEvent *)event { + BOOL keyDown = event.type == NSEventTypeKeyDown; + BOOL hasHandler = keyDown ? _props->macOSViewEvents[MacOSViewEvents::Offset::KeyDown] + : _props->macOSViewEvents[MacOSViewEvents::Offset::KeyUp]; + if (hasHandler) { + auto validKeys = keyDown ? _props->validKeysDown : _props->validKeysUp; + + // If the view is focusable and the component didn't explicity set the validKeysDown or validKeysUp, + // allow enter/return and spacebar key events to mimic the behavior of native controls. + if (self.focusable && !validKeys.has_value()) { + validKeys = { { .key = "Enter" }, { .key = " " } }; + } + + // If there are no valid keys defined, no key event handling is required. + if (!validKeys.has_value()) { + return NO; + } + + // Convert the event to a KeyEvent + NSEventModifierFlags modifierFlags = event.modifierFlags; + facebook::react::KeyEvent keyEvent = { + .key = [[RCTViewKeyboardEvent keyFromEvent:event] UTF8String], + .altKey = static_cast(modifierFlags & NSEventModifierFlagOption), + .ctrlKey = static_cast(modifierFlags & NSEventModifierFlagControl), + .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), + .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), + .capsLockKey = static_cast(modifierFlags & NSEventModifierFlagCapsLock), + .numericPadKey = static_cast(modifierFlags & NSEventModifierFlagNumericPad), + .helpKey = static_cast(modifierFlags & NSEventModifierFlagHelp), + .functionKey = static_cast(modifierFlags & NSEventModifierFlagFunction), + }; + + BOOL shouldBlock = NO; + for (auto const &validKey : *validKeys) { + if (keyEvent == validKey) { + shouldBlock = YES; + break; + } + } + + if (_eventEmitter && shouldBlock) { + if (keyDown) { + _eventEmitter->onKeyDown(keyEvent); + } else { + _eventEmitter->onKeyUp(keyEvent); + } + return YES; + } + } + + return NO; +} + +- (void)keyDown:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyDown:event]; + } +} + +- (void)keyUp:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyUp:event]; + } +} + + +#pragma mark - Drag and Drop Events + +enum DragEventType { + DragEnter, + DragLeave, + Drop, +}; + +- (void)buildDataTransferItems:(std::vector &)dataTransferItems forPasteboard:(NSPasteboard *)pasteboard { + NSArray *fileNames = [pasteboard propertyListForType:NSFilenamesPboardType] ?: @[]; + for (NSString *file in fileNames) { + NSURL *fileURL = [NSURL fileURLWithPath:file]; + BOOL isDir = NO; + BOOL isValid = (![[NSFileManager defaultManager] fileExistsAtPath:fileURL.path isDirectory:&isDir] || isDir) ? NO : YES; + if (isValid) { + + NSString *MIMETypeString = nil; + if (fileURL.pathExtension) { + CFStringRef fileExtension = (__bridge CFStringRef)fileURL.pathExtension; + CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, NULL); + if (UTI != NULL) { + CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType); + CFRelease(UTI); + MIMETypeString = (__bridge_transfer NSString *)MIMEType; + } + } + + NSNumber *fileSizeValue = nil; + NSError *fileSizeError = nil; + BOOL success = [fileURL getResourceValue:&fileSizeValue + forKey:NSURLFileSizeKey + error:&fileSizeError]; + + NSNumber *width = nil; + NSNumber *height = nil; + if ([MIMETypeString hasPrefix:@"image/"]) { + NSImage *image = [[NSImage alloc] initWithContentsOfURL:fileURL]; + width = @(image.size.width); + height = @(image.size.height); + } + + DataTransferItem transferItem = { + .name = fileURL.lastPathComponent.UTF8String, + .kind = "file", + .type = MIMETypeString.UTF8String, + .uri = fileURL.path.UTF8String, + }; + + if (success) { + transferItem.size = fileSizeValue.intValue; + } + + if (width != nil) { + transferItem.width = width.intValue; + } + + if (height != nil) { + transferItem.height = height.intValue; + } + + dataTransferItems.push_back(transferItem); + } + } + + NSPasteboardType imageType = [pasteboard availableTypeFromArray:@[NSPasteboardTypePNG, NSPasteboardTypeTIFF]]; + if (imageType && fileNames.count == 0) { + NSString *MIMETypeString = imageType == NSPasteboardTypePNG ? @"image/png" : @"image/tiff"; + NSData *imageData = [pasteboard dataForType:imageType]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + DataTransferItem transferItem = { + .kind = "image", + .type = MIMETypeString.UTF8String, + .uri = RCTDataURL(MIMETypeString, imageData).absoluteString.UTF8String, + .size = imageData.length, + .width = image.size.width, + .height = image.size.height, + }; + + dataTransferItems.push_back(transferItem); + } +} + +- (void)sendDragEvent:(DragEventType)eventType withLocation:(NSPoint)locationInWindow pasteboard:(NSPasteboard *)pasteboard { + if (!_eventEmitter) { + return; + } + + std::vector dataTransferItems{}; + [self buildDataTransferItems:dataTransferItems forPasteboard:pasteboard]; + + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + NSEventModifierFlags modifierFlags = self.window.currentEvent.modifierFlags; + + DragEvent dragEvent = { + { + .clientX = locationInView.x, + .clientY = locationInView.y, + .screenX = locationInWindow.x, + .screenY = locationInWindow.y, + .altKey = static_cast(modifierFlags & NSEventModifierFlagOption), + .ctrlKey = static_cast(modifierFlags & NSEventModifierFlagControl), + .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), + .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), + }, + .dataTransferItems = dataTransferItems, + }; + + switch (eventType) { + case DragEnter: + _eventEmitter->onDragEnter(dragEvent); + break; + + case DragLeave: + _eventEmitter->onDragLeave(dragEvent); + break; + + case Drop: + _eventEmitter->onDrop(dragEvent); + break; + } +} + +- (NSDragOperation)draggingEntered:(id )sender +{ + NSPasteboard *pboard = sender.draggingPasteboard; + NSDragOperation sourceDragMask = sender.draggingSourceOperationMask; + + [self sendDragEvent:DragEnter withLocation:sender.draggingLocation pasteboard:pboard]; + + if ([pboard availableTypeFromArray:self.registeredDraggedTypes]) { + if (sourceDragMask & NSDragOperationLink) { + return NSDragOperationLink; + } else if (sourceDragMask & NSDragOperationCopy) { + return NSDragOperationCopy; + } + } + return NSDragOperationNone; +} + +- (void)draggingExited:(id)sender +{ + [self sendDragEvent:DragLeave withLocation:sender.draggingLocation pasteboard:sender.draggingPasteboard]; +} + +- (BOOL)performDragOperation:(id )sender +{ + [self sendDragEvent:Drop withLocation:sender.draggingLocation pasteboard:sender.draggingPasteboard]; + return YES; +} + + +#pragma mark - Mouse Events + +enum MouseEventType { + MouseEnter, + MouseLeave, + DoubleClick, +}; + +- (void)sendMouseEvent:(MouseEventType)eventType { + if (!_eventEmitter) { + return; + } + + NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream; + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + + NSEventModifierFlags modifierFlags = self.window.currentEvent.modifierFlags; + + MouseEvent mouseEvent = { + .clientX = locationInView.x, + .clientY = locationInView.y, + .screenX = locationInWindow.x, + .screenY = locationInWindow.y, + .altKey = static_cast(modifierFlags & NSEventModifierFlagOption), + .ctrlKey = static_cast(modifierFlags & NSEventModifierFlagControl), + .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), + .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), + }; + + switch (eventType) { + case MouseEnter: + _eventEmitter->onMouseEnter(mouseEvent); + break; + + case MouseLeave: + _eventEmitter->onMouseLeave(mouseEvent); + break; + + case DoubleClick: + _eventEmitter->onDoubleClick(mouseEvent); + break; + } +} + +- (void)updateMouseOverIfNeeded +{ + // 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. + + BOOL hasMouseOver = _hasMouseOver; + NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream; + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + BOOL insideBounds = NSPointInRect(locationInView, self.visibleRect); + + // On macOS 14.0 visibleRect can be larger than the view bounds + insideBounds &= NSPointInRect(locationInView, self.bounds); + + if (hasMouseOver && !insideBounds) { + hasMouseOver = NO; + } else if (!hasMouseOver && insideBounds) { + // The window's frame view must be used for hit testing against `locationInWindow` + NSView *hitView = [self.window.contentView.superview hitTest:locationInWindow]; + hasMouseOver = [hitView isDescendantOf:self]; + } + + if (hasMouseOver != _hasMouseOver) { + _hasMouseOver = hasMouseOver; + [self sendMouseEvent:hasMouseOver ? MouseEnter : MouseLeave]; + } +} + +- (void)updateClipViewBoundsObserverIfNeeded +{ + // 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. + + NSClipView *clipView = self.window ? self.enclosingScrollView.contentView : nil; + + + BOOL hasMouseEventHandler = _props->macOSViewEvents[MacOSViewEvents::Offset::MouseEnter] || + _props->macOSViewEvents[MacOSViewEvents::Offset::MouseLeave]; + + if (_hasClipViewBoundsObserver && (!clipView || !hasMouseEventHandler)) { + _hasClipViewBoundsObserver = NO; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSViewBoundsDidChangeNotification + object:nil]; + } else if (!_hasClipViewBoundsObserver && clipView && hasMouseEventHandler) { + _hasClipViewBoundsObserver = YES; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateMouseOverIfNeeded) + name:NSViewBoundsDidChangeNotification + object:clipView]; + [self updateMouseOverIfNeeded]; + } +} + +- (void)viewDidMoveToWindow +{ + [self updateClipViewBoundsObserverIfNeeded]; + [super viewDidMoveToWindow]; +} + +- (void)updateTrackingAreas +{ + if (_trackingArea) { + [self removeTrackingArea:_trackingArea]; + } + + if ( + _props->macOSViewEvents[MacOSViewEvents::Offset::MouseEnter] || + _props->macOSViewEvents[MacOSViewEvents::Offset::MouseLeave] + ) { + _trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds + options:NSTrackingActiveAlways | NSTrackingMouseEnteredAndExited + owner:self + userInfo:nil]; + [self addTrackingArea:_trackingArea]; + [self updateMouseOverIfNeeded]; + } + + [super updateTrackingAreas]; +} + +- (void)mouseUp:(NSEvent *)event +{ + BOOL hasDoubleClickEventHandler = _props->macOSViewEvents[MacOSViewEvents::Offset::DoubleClick]; + if (hasDoubleClickEventHandler && event.clickCount == 2) { + [self sendMouseEvent:DoubleClick]; + } else { + [super mouseUp:event]; + } +} + +- (void)mouseEntered:(NSEvent *)event +{ + if (_hasMouseOver) { + return; + } + + // The window's frame view must be used for hit testing against `locationInWindow` + NSView *hitView = [self.window.contentView.superview hitTest:event.locationInWindow]; + if (![hitView isDescendantOf:self]) { + return; + } + + _hasMouseOver = YES; + [self sendMouseEvent:MouseEnter]; +} + +- (void)mouseExited:(NSEvent *)event +{ + if (!_hasMouseOver) { + return; + } + + _hasMouseOver = NO; + [self sendMouseEvent:MouseLeave]; +} +#endif // macOS] + - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point { return _eventEmitter; @@ -995,6 +1532,11 @@ - (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN return RCTNSStringFromString([[self class] componentDescriptorProvider].name); } +- (BOOL)wantsUpdateLayer +{ + return YES; +} + @end #ifdef __cplusplus diff --git a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm index dd60be42d1e1f5..fdcae5e2820dc9 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm @@ -310,6 +310,7 @@ - (void)synchronouslyUpdateViewOnUIThread:(ReactTag)reactTag const auto &newViewProps = static_cast(*newProps); +#if !TARGET_OS_OSX // [macOS] if (props[@"transform"]) { auto layoutMetrics = LayoutMetrics(); layoutMetrics.frame.size.width = componentView.layer.bounds.size.width; @@ -318,7 +319,21 @@ - (void)synchronouslyUpdateViewOnUIThread:(ReactTag)reactTag if (!CATransform3DEqualToTransform(newTransform, componentView.layer.transform)) { componentView.layer.transform = newTransform; } +#else // [macOS + if (props[@"transform"]) { + CATransform3D transform = RCTCATransform3DFromTransformMatrix(newViewProps.transform); + + if (CGPointEqualToPoint(componentView.layer.anchorPoint, CGPointZero) && !CATransform3DEqualToTransform(transform, CATransform3DIdentity)) { + CATransform3D originAdjust = CATransform3DTranslate(CATransform3DIdentity, componentView.frame.size.width / 2, componentView.frame.size.height / 2, 0); + transform = CATransform3DConcat(CATransform3DConcat(CATransform3DInvert(originAdjust), transform), originAdjust); + } + + if (!CATransform3DEqualToTransform(transform, componentView.layer.transform)) { + componentView.layer.transform = transform; + } } +#endif // macOS] + if (props[@"opacity"] && componentView.layer.opacity != (float)newViewProps.opacity) { componentView.layer.opacity = newViewProps.opacity; } diff --git a/packages/react-native/React/Fabric/RCTConversions.h b/packages/react-native/React/Fabric/RCTConversions.h index 2fa35c998df8b9..e7468de4578bd8 100644 --- a/packages/react-native/React/Fabric/RCTConversions.h +++ b/packages/react-native/React/Fabric/RCTConversions.h @@ -13,6 +13,10 @@ #import #import +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] + NS_ASSUME_NONNULL_BEGIN inline NSString *RCTNSStringFromString( @@ -146,7 +150,99 @@ inline UIAccessibilityTraits RCTUIAccessibilityTraitsFromAccessibilityTraits( } return result; }; -#endif // [macOS] +#else // [macOS +inline NSAccessibilityRole RCTUIAccessibilityRoleFromAccessibilityTraits( + facebook::react::AccessibilityTraits accessibilityTraits) +{ + using AccessibilityTraits = facebook::react::AccessibilityTraits; + if ((accessibilityTraits & AccessibilityTraits::Button) != AccessibilityTraits::None) { + if ((accessibilityTraits & AccessibilityTraits::Bar) != AccessibilityTraits::None) { + return NSAccessibilityToolbarRole; + } + if ((accessibilityTraits & AccessibilityTraits::PopUp) != AccessibilityTraits::None) { + return NSAccessibilityPopUpButtonRole; + } + if ((accessibilityTraits & AccessibilityTraits::Menu) != AccessibilityTraits::None) { + return NSAccessibilityMenuButtonRole; + } + return NSAccessibilityButtonRole; + } + if ((accessibilityTraits & AccessibilityTraits::Link) != AccessibilityTraits::None) { + return NSAccessibilityLinkRole; + } + if ((accessibilityTraits & AccessibilityTraits::Image) != AccessibilityTraits::None) { + return NSAccessibilityImageRole; + } + if ((accessibilityTraits & AccessibilityTraits::KeyboardKey) != AccessibilityTraits::None) { + return NSAccessibilityButtonRole; + } + if ((accessibilityTraits & AccessibilityTraits::StaticText) != AccessibilityTraits::None) { + return NSAccessibilityStaticTextRole; + } + if ((accessibilityTraits & AccessibilityTraits::SummaryElement) != AccessibilityTraits::None) { + return NSAccessibilityStaticTextRole; + } + if ((accessibilityTraits & AccessibilityTraits::SearchField) != AccessibilityTraits::None) { + return NSAccessibilityTextFieldRole; + } + if ((accessibilityTraits & AccessibilityTraits::Adjustable) != AccessibilityTraits::None) { + return NSAccessibilitySliderRole; + } + if ((accessibilityTraits & AccessibilityTraits::Header) != AccessibilityTraits::None) { + return NSAccessibilityStaticTextRole; + } + if ((accessibilityTraits & AccessibilityTraits::Switch) != AccessibilityTraits::None) { + return NSAccessibilityCheckBoxRole; + } + if ((accessibilityTraits & AccessibilityTraits::UpdatesFrequently) != AccessibilityTraits::None) { + return NSAccessibilityProgressIndicatorRole; + } + if ((accessibilityTraits & AccessibilityTraits::ComboBox) != AccessibilityTraits::None) { + return NSAccessibilityComboBoxRole; + } + if ((accessibilityTraits & AccessibilityTraits::Menu) != AccessibilityTraits::None) { + if ((accessibilityTraits & AccessibilityTraits::Bar) != AccessibilityTraits::None) { + return NSAccessibilityMenuBarRole; + } + if ((accessibilityTraits & AccessibilityTraits::Item) != AccessibilityTraits::None) { + return NSAccessibilityMenuItemRole; + } + return NSAccessibilityMenuRole; + } + if ((accessibilityTraits & AccessibilityTraits::Radio) != AccessibilityTraits::None) { + if ((accessibilityTraits & AccessibilityTraits::Group) != AccessibilityTraits::None) { + return NSAccessibilityRadioGroupRole; + } + return NSAccessibilityRadioButtonRole; + } + if ((accessibilityTraits & AccessibilityTraits::ScrollBar) != AccessibilityTraits::None) { + return NSAccessibilityScrollBarRole; + } + if ((accessibilityTraits & AccessibilityTraits::SpinButton) != AccessibilityTraits::None) { + return NSAccessibilityIncrementorRole; + } + if ((accessibilityTraits & AccessibilityTraits::Tab) != AccessibilityTraits::None) { + if ((accessibilityTraits & AccessibilityTraits::List) != AccessibilityTraits::None) { + return NSAccessibilityTabGroupRole; + } + return NSAccessibilityButtonRole; + } + if ((accessibilityTraits & AccessibilityTraits::Disclosure) != AccessibilityTraits::None) { + return NSAccessibilityDisclosureTriangleRole; + } + if ((accessibilityTraits & AccessibilityTraits::Group) != AccessibilityTraits::None) { + return NSAccessibilityGroupRole; + } + if ((accessibilityTraits & AccessibilityTraits::List) != AccessibilityTraits::None) { + return NSAccessibilityListRole; + } + if ((accessibilityTraits & AccessibilityTraits::Table) != AccessibilityTraits::None) { + return NSAccessibilityTableRole; + } + + return NSAccessibilityUnknownRole; +}; +#endif // macOS] inline CATransform3D RCTCATransform3DFromTransformMatrix(const facebook::react::Transform &transformMatrix) { @@ -202,4 +298,29 @@ inline facebook::react::LayoutDirection RCTLayoutDirection(BOOL isRTL) return isRTL ? facebook::react::LayoutDirection::RightToLeft : facebook::react::LayoutDirection::LeftToRight; } +#if TARGET_OS_OSX // [macOS +inline NSArray *RCTPasteboardTypeArrayFromProps(const std::vector &pastedTypes) +{ + NSMutableArray *types = [NSMutableArray new]; + + for (const auto &type : pastedTypes) { + switch (type) { + case facebook::react::PastedTypesType::FileUrl: + [types addObjectsFromArray:@[NSFilenamesPboardType]]; + break; + case facebook::react::PastedTypesType::Image: + [types addObjectsFromArray:@[NSPasteboardTypePNG, NSPasteboardTypeTIFF]]; + break; + case facebook::react::PastedTypesType::String: + [types addObjectsFromArray:@[NSPasteboardTypeString]]; + break; + default: + break; + } + } + + return [types copy]; +} +#endif // macOS] + NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index 709e92d20c7c40..0c678c9a82a836 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -258,9 +258,11 @@ - (RCTPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS #if !TARGET_OS_OSX // [macOS] pointForHitTest = [subview convertPoint:point fromView:self]; #else // [macOS - if ([subview isKindOfClass:[RCTView class]]) { + // Paper and Fabric components use the target view coordinate space for hit testing + if ([subview isKindOfClass:[RCTView class]] || [subview respondsToSelector:@selector(updateProps:oldProps:)]) { pointForHitTest = [subview convertPoint:point fromView:self]; } else { + // Native macOS views require the point to be in the super view coordinate space for hit testing. pointForHitTest = point; } #endif // macOS] diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 153008c3228fe0..b0c086b4d84fc0 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -219,7 +219,7 @@ Pod::Spec.new do |s| sss.dependency "Yoga" sss.compiler_flags = folly_compiler_flags sss.source_files = "react/renderer/components/view/**/*.{m,mm,cpp,h}" - sss.exclude_files = "react/renderer/components/view/tests", "react/renderer/components/view/platform/android" + sss.exclude_files = "react/renderer/components/view/tests", "react/renderer/components/view/platform/android", "react/renderer/components/view/platform/cxx" sss.header_dir = "react/renderer/components/view" sss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/Headers/Private/Yoga\"" } end diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp index 88ae3f35a221c6..447fe8d760fd07 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.cpp @@ -67,6 +67,8 @@ static jsi::Value keyPressMetricsPayload( key = "Enter"; } else if (keyPressMetrics.text.front() == '\t') { key = "Tab"; + } else if (keyPressMetrics.text.front() == '\x1B') { + key = "Escape"; } else { key = keyPressMetrics.text.front(); } @@ -167,4 +169,39 @@ void TextInputEventEmitter::dispatchTextInputContentSizeChangeEvent( priority); } +#if TARGET_OS_OSX // [macOS +void TextInputEventEmitter::onAutoCorrectChange(OnAutoCorrectChange event) const { + dispatchEvent("autoCorrectChange", [event=std::move(event)](jsi::Runtime &runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "enabled", event.enabled); + return payload; + }); +} + +void TextInputEventEmitter::onSpellCheckChange(OnSpellCheckChange event) const { + dispatchEvent("spellCheckChange", [event=std::move(event)](jsi::Runtime &runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "enabled", event.enabled); + return payload; + }); +} + +void TextInputEventEmitter::onGrammarCheckChange(OnGrammarCheckChange event) const { + dispatchEvent("grammarCheckChange", [event=std::move(event)](jsi::Runtime &runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "enabled", event.enabled); + return payload; + }); +} + +void TextInputEventEmitter::onPaste(PasteEvent const &pasteEvent) const { + dispatchEvent("paste", [pasteEvent](jsi::Runtime &runtime) { + auto payload = jsi::Object(runtime); + auto dataTransfer= dataTransferPayload(runtime, pasteEvent.dataTransferItems); + payload.setProperty(runtime, "dataTransfer", dataTransfer); + return payload; + }); +} +#endif // macOS] + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h index 0ab2b18544fc34..aeb3413027cc0d 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputEventEmitter.h @@ -11,6 +11,9 @@ #include namespace facebook::react { +#if TARGET_OS_OSX // [macOS +#include +#endif // macOS] class TextInputMetrics { public: @@ -48,6 +51,28 @@ class TextInputEventEmitter : public ViewEventEmitter { void onKeyPressSync(const KeyPressMetrics& keyPressMetrics) const; void onScroll(const TextInputMetrics& textInputMetrics) const; +#if TARGET_OS_OSX // [macOS + struct OnAutoCorrectChange { + bool enabled; + }; + void onAutoCorrectChange(OnAutoCorrectChange value) const; + + struct OnSpellCheckChange { + bool enabled; + }; + void onSpellCheckChange(OnSpellCheckChange value) const; + + struct OnGrammarCheckChange { + bool enabled; + }; + void onGrammarCheckChange(OnGrammarCheckChange value) const; + + struct PasteEvent { + std::vector dataTransferItems; + }; + void onPaste(PasteEvent const &pasteEvent) const; +#endif // macOS] + private: void dispatchTextInputEvent( const std::string& name, diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/conversions.h index 93e30078534b30..cd9a37606b7888 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/conversions.h @@ -237,4 +237,26 @@ inline void fromRawValue( abort(); } +#ifdef TARGET_OS_OSX // [macOS +inline void fromRawValue( + const PropsParserContext &context, + const RawValue &value, + PastedTypesType &result) { + auto string = (std::string)value; + if (string == "fileUrl") { + result = PastedTypesType::FileUrl; + return; + } + if (string == "image") { + result = PastedTypesType::Image; + return; + } + if (string == "string") { + result = PastedTypesType::String; + return; + } + abort(); +} +#endif // macOS] + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h index 664c0ccf0496ac..93c87eebb2a6bc 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/primitives.h @@ -9,6 +9,7 @@ #include #include +#include namespace facebook::react { @@ -89,6 +90,24 @@ class Selection final { int end{0}; }; +#if TARGET_OS_OSX // [macOS +class SubmitKeyEvent final { + public: + std::string key{}; + bool altKey{false}; + bool shiftKey{false}; + bool ctrlKey{false}; + bool metaKey{false}; + bool functionKey{false}; +}; + +enum class PastedTypesType { + FileUrl, + Image, + String, +}; +#endif // macOS] + /* * Controls features of text inputs. */ @@ -231,6 +250,36 @@ class TextInputTraits final { * Default value: `empty` (`null`). */ std::optional smartInsertDelete{}; + +#ifdef TARGET_OS_OSX // [macOS + /* + * Can be empty (`null` in JavaScript) which means `default`. + * macOS + * Default value: `empty` (`null`). + */ + std::optional grammarCheck{}; + + /* + * List of key combinations that should submit. + * macOS-only + * Default value: `empty list` applies as 'Enter' key. + */ + std::vector submitKeyEvents{}; + + /* + * When set to `true`, the text will be cleared after the submit. + * macOS-only + * Default value: `false` + */ + bool clearTextOnSubmit{false}; + + /* + * List of pastable types + * macOS-only + * Default value: `empty list` + */ + std::vector pastedTypes{}; +#endif // macOS] }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/propsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/propsConversions.h index 109f5a1237be0a..fc614729cc9c08 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/propsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/propsConversions.h @@ -148,6 +148,21 @@ static TextInputTraits convertRawProp( sourceTraits.smartInsertDelete, defaultTraits.smartInsertDelete); +#ifdef TARGET_OS_OSX // [macOS + traits.grammarCheck = convertRawProp( + context, + rawProps, + "grammarCheck", + sourceTraits.grammarCheck, + defaultTraits.grammarCheck); + traits.pastedTypes = convertRawProp( + context, + rawProps, + "pastedTypes", + sourceTraits.pastedTypes, + defaultTraits.pastedTypes); +#endif // macOS] + return traits; } @@ -184,4 +199,50 @@ inline void fromRawValue( LOG(ERROR) << "Unsupported Selection type"; } } + +#if TARGET_OS_OSX // [macOS +static inline void fromRawValue( + const PropsParserContext& context, + const RawValue& value, + SubmitKeyEvent& result) { + auto map = (std::unordered_map)value; + + auto tmp_key = map.find("key"); + if (tmp_key != map.end()) { + fromRawValue(context, tmp_key->second, result.key); + } + auto tmp_altKey = map.find("altKey"); + if (tmp_altKey != map.end()) { + fromRawValue(context, tmp_altKey->second, result.altKey); + } + auto tmp_shiftKey = map.find("shiftKey"); + if (tmp_shiftKey != map.end()) { + fromRawValue(context, tmp_shiftKey->second, result.shiftKey); + } + auto tmp_ctrlKey = map.find("ctrlKey"); + if (tmp_ctrlKey != map.end()) { + fromRawValue(context, tmp_ctrlKey->second, result.ctrlKey); + } + auto tmp_metaKey = map.find("metaKey"); + if (tmp_metaKey != map.end()) { + fromRawValue(context, tmp_metaKey->second, result.metaKey); + } + auto tmp_functionKey = map.find("functionKey"); + if (tmp_functionKey != map.end()) { + fromRawValue(context, tmp_functionKey->second, result.functionKey); + } +} + +static inline void fromRawValue( + const PropsParserContext& context, + const RawValue& value, + std::vector& result) { + auto items = (std::vector)value; + for (const auto &item : items) { + SubmitKeyEvent newItem; + fromRawValue(context, item, newItem); + result.emplace_back(newItem); + } +} +#endif // macOS] } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPrimitives.h b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPrimitives.h index ce9f6eb4517e72..7fb3e909292135 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPrimitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/AccessibilityPrimitives.h @@ -34,6 +34,21 @@ enum class AccessibilityTraits : uint32_t { Header = (1 << 15), Switch = (1 << 16), TabBar = (1 << 17), +// [macOS + ComboBox = (1 << 18), + Menu = (1 << 19), + PopUp = (1 << 20), + Bar = (1 << 21), + Item = (1 << 22), + Group = (1 << 23), + List = (1 << 24), + Tab = (1 << 25), + Table = (1 << 26), + Disclosure = (1 << 27), + Radio = (1 << 28), + ScrollBar = (1 << 29), + SpinButton = (1 << 30), +// macOS] }; constexpr enum AccessibilityTraits operator|( diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index 12a9dd40370e34..911e74f81e0546 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -52,7 +52,14 @@ void ViewShadowNode::initialize() noexcept { viewProps.accessibilityViewIsModal || viewProps.importantForAccessibility != ImportantForAccessibility::Auto || viewProps.removeClippedSubviews || viewProps.cursor != Cursor::Auto || - HostPlatformViewTraitsInitializer::formsStackingContext(viewProps); + HostPlatformViewTraitsInitializer::formsStackingContext(viewProps) +#if TARGET_OS_OSX // [macOS + || viewProps.focusable + || viewProps.enableFocusRing + || viewProps.macOSViewEvents[MacOSViewEvents::Offset::MouseEnter] + || viewProps.macOSViewEvents[MacOSViewEvents::Offset::MouseLeave] +#endif // macOS] + ; bool formsView = formsStackingContext || isColorMeaningful(viewProps.backgroundColor) || diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/DraggedType.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/DraggedType.h new file mode 100644 index 00000000000000..5c47eb918b45b1 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/DraggedType.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +namespace facebook::react { + +enum class DraggedType { + FileUrl, + Image, + String, +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformTouch.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformTouch.h new file mode 100644 index 00000000000000..0d441117751c89 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformTouch.h @@ -0,0 +1,14 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { +using HostPlatformTouch = BaseTouch; +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp new file mode 100644 index 00000000000000..b2a23d6250e9df --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp @@ -0,0 +1,156 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "HostPlatformViewEventEmitter.h" + +namespace facebook::react { + +#pragma mark - Keyboard Events + +static jsi::Value keyEventPayload(jsi::Runtime &runtime, KeyEvent const &event) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "key", jsi::String::createFromUtf8(runtime, event.key)); + payload.setProperty(runtime, "ctrlKey", event.ctrlKey); + payload.setProperty(runtime, "shiftKey", event.shiftKey); + payload.setProperty(runtime, "altKey", event.altKey); + payload.setProperty(runtime, "metaKey", event.metaKey); + payload.setProperty(runtime, "capsLockKey", event.capsLockKey); + payload.setProperty(runtime, "numericPadKey", event.numericPadKey); + payload.setProperty(runtime, "helpKey", event.helpKey); + payload.setProperty(runtime, "functionKey", event.functionKey); + return payload; +}; + +void HostPlatformViewEventEmitter::onKeyDown(KeyEvent const &keyEvent) const { + dispatchEvent( + "keyDown", + [keyEvent](jsi::Runtime &runtime) { return keyEventPayload(runtime, keyEvent); }, + EventPriority::AsynchronousBatched); +} + +void HostPlatformViewEventEmitter::onKeyUp(KeyEvent const &keyEvent) const { + dispatchEvent( + "keyUp", + [keyEvent](jsi::Runtime &runtime) { return keyEventPayload(runtime, keyEvent); }, + EventPriority::AsynchronousBatched); +} + +#pragma mark - Mouse Events + +static jsi::Object mouseEventPayload(jsi::Runtime &runtime, MouseEvent const &event) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "clientX", event.clientX); + payload.setProperty(runtime, "clientY", event.clientY); + payload.setProperty(runtime, "screenX", event.screenX); + payload.setProperty(runtime, "screenY", event.screenY); + payload.setProperty(runtime, "altKey", event.altKey); + payload.setProperty(runtime, "ctrlKey", event.ctrlKey); + payload.setProperty(runtime, "shiftKey", event.shiftKey); + payload.setProperty(runtime, "metaKey", event.metaKey); + return payload; +}; + +void HostPlatformViewEventEmitter::onMouseEnter(MouseEvent const &mouseEvent) const { + dispatchEvent( + "mouseEnter", + [mouseEvent](jsi::Runtime &runtime) { return mouseEventPayload(runtime, mouseEvent); }, + EventPriority::AsynchronousBatched); +} + +void HostPlatformViewEventEmitter::onMouseLeave(MouseEvent const &mouseEvent) const { + dispatchEvent( + "mouseLeave", + [mouseEvent](jsi::Runtime &runtime) { return mouseEventPayload(runtime, mouseEvent); }, + EventPriority::AsynchronousBatched); +} + +void HostPlatformViewEventEmitter::onDoubleClick(MouseEvent const &mouseEvent) const { + dispatchEvent( + "doubleClick", + [mouseEvent](jsi::Runtime &runtime) { return mouseEventPayload(runtime, mouseEvent); }, + EventPriority::AsynchronousBatched); +} + +#pragma mark - Drag and Drop Events + +jsi::Value HostPlatformViewEventEmitter::dataTransferPayload(jsi::Runtime &runtime, std::vector const &dataTransferItems) { + auto filesArray = jsi::Array(runtime, dataTransferItems.size()); + auto itemsArray = jsi::Array(runtime, dataTransferItems.size()); + auto typesArray = jsi::Array(runtime, dataTransferItems.size()); + int i = 0; + for (auto const &transferItem : dataTransferItems) { + auto fileObject = jsi::Object(runtime); + fileObject.setProperty(runtime, "name", transferItem.name); + fileObject.setProperty(runtime, "type", transferItem.type); + fileObject.setProperty(runtime, "uri", transferItem.uri); + if (transferItem.size.has_value()) { + fileObject.setProperty(runtime, "size", *transferItem.size); + } + if (transferItem.width.has_value()) { + fileObject.setProperty(runtime, "width", *transferItem.width); + } + if (transferItem.height.has_value()) { + fileObject.setProperty(runtime, "height", *transferItem.height); + } + filesArray.setValueAtIndex(runtime, i, fileObject); + + auto itemObject = jsi::Object(runtime); + itemObject.setProperty(runtime, "kind", transferItem.kind); + itemObject.setProperty(runtime, "type", transferItem.type); + itemsArray.setValueAtIndex(runtime, i, itemObject); + + typesArray.setValueAtIndex(runtime, i, transferItem.type); + i++; + } + + auto dataTransferObject = jsi::Object(runtime); + dataTransferObject.setProperty(runtime, "files", filesArray); + dataTransferObject.setProperty(runtime, "items", itemsArray); + dataTransferObject.setProperty(runtime, "types", typesArray); + + return dataTransferObject; +} + +static jsi::Value dragEventPayload(jsi::Runtime &runtime, DragEvent const &event) { + auto payload = mouseEventPayload(runtime, event); + auto dataTransferObject = HostPlatformViewEventEmitter::dataTransferPayload(runtime, event.dataTransferItems); + payload.setProperty(runtime, "dataTransfer", dataTransferObject); + return payload; +} + +void HostPlatformViewEventEmitter::onDragEnter(DragEvent const &dragEvent) const { + dispatchEvent( + "dragEnter", + [dragEvent](jsi::Runtime &runtime) { return dragEventPayload(runtime, dragEvent); }, + EventPriority::AsynchronousBatched); +} + +void HostPlatformViewEventEmitter::onDragLeave(DragEvent const &dragEvent) const { + dispatchEvent( + "dragLeave", + [dragEvent](jsi::Runtime &runtime) { return dragEventPayload(runtime, dragEvent); }, + EventPriority::AsynchronousBatched); +} + +void HostPlatformViewEventEmitter::onDrop(DragEvent const &dragEvent) const { + dispatchEvent( + "drop", + [dragEvent](jsi::Runtime &runtime) { return dragEventPayload(runtime, dragEvent); }, + EventPriority::AsynchronousBatched); +} + +#pragma mark - Focus Events + +void HostPlatformViewEventEmitter::onFocus() const { + dispatchEvent("focus"); +} + +void HostPlatformViewEventEmitter::onBlur() const { + dispatchEvent("blur"); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h new file mode 100644 index 00000000000000..c6fbacea312e4b --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +class HostPlatformViewEventEmitter : public BaseViewEventEmitter { + public: + using BaseViewEventEmitter::BaseViewEventEmitter; + +#pragma mark - Keyboard Events + + void onKeyDown(KeyEvent const &keyEvent) const; + void onKeyUp(KeyEvent const &keyEvent) const; + +#pragma mark - Mouse Events + + void onMouseEnter(MouseEvent const &mouseEvent) const; + void onMouseLeave(MouseEvent const &mouseEvent) const; + void onDoubleClick(MouseEvent const &mouseEvent) const; + +#pragma mark - Drag and Drop Events + + void onDragEnter(DragEvent const &dragEvent) const; + void onDragLeave(DragEvent const &dragEvent) const; + void onDrop(DragEvent const &dragEvent) const; + +#pragma mark - Focus Events + + void onFocus() const; + void onBlur() const; + + static jsi::Value dataTransferPayload(jsi::Runtime &runtime, std::vector const &dataTransferItems); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp new file mode 100644 index 00000000000000..3f62b9b74a6b5f --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "HostPlatformViewProps.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace facebook::react { + +HostPlatformViewProps::HostPlatformViewProps( + const PropsParserContext& context, + const HostPlatformViewProps& sourceProps, + const RawProps& rawProps) + : BaseViewProps(context, sourceProps, rawProps), + macOSViewEvents( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.macOSViewEvents + : convertRawProp(context, rawProps, sourceProps.macOSViewEvents, {})), + focusable( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.focusable + : convertRawProp(context, rawProps, "focusable", sourceProps.focusable, {})), + enableFocusRing( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.enableFocusRing + : convertRawProp(context, rawProps, "enableFocusRing", sourceProps.enableFocusRing, true)), + validKeysDown( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.validKeysDown + : convertRawProp(context, rawProps, "validKeysDown", sourceProps.validKeysDown, {})), + validKeysUp( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.validKeysUp + : convertRawProp(context, rawProps, "validKeysUp", sourceProps.validKeysUp, {})), + draggedTypes( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.draggedTypes + : convertRawProp(context, rawProps, "draggedTypes", sourceProps.draggedTypes, {})), + tooltip( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.tooltip + : convertRawProp(context, rawProps, "tooltip", sourceProps.tooltip, {})){}; + +#define VIEW_EVENT_CASE_MACOS(eventType, eventString) \ + case CONSTEXPR_RAW_PROPS_KEY_HASH(eventString): { \ + MacOSViewEvents defaultViewEvents{}; \ + bool res = defaultViewEvents[eventType]; \ + if (value.hasValue()) { \ + fromRawValue(context, value, res); \ + } \ + macOSViewEvents[eventType] = res; \ + return; \ + } + +void HostPlatformViewProps::setProp( + const PropsParserContext& context, + RawPropsPropNameHash hash, + const char* propName, + RawValue const& value) { + // All Props structs setProp methods must always, unconditionally, + // call all super::setProp methods, since multiple structs may + // reuse the same values. + BaseViewProps::setProp(context, hash, propName, value); + + static auto defaults = HostPlatformViewProps{}; + + switch (hash) { + VIEW_EVENT_CASE_MACOS(MacOSViewEvents::Offset::KeyDown, "onKeyDown"); + VIEW_EVENT_CASE_MACOS(MacOSViewEvents::Offset::KeyUp, "onKeyUp"); + VIEW_EVENT_CASE_MACOS(MacOSViewEvents::Offset::MouseEnter, "onMouseEnter"); + VIEW_EVENT_CASE_MACOS(MacOSViewEvents::Offset::MouseLeave, "onMouseLeave"); + VIEW_EVENT_CASE_MACOS(MacOSViewEvents::Offset::DoubleClick, "onDoubleClick"); + RAW_SET_PROP_SWITCH_CASE_BASIC(focusable); + RAW_SET_PROP_SWITCH_CASE_BASIC(enableFocusRing); + RAW_SET_PROP_SWITCH_CASE_BASIC(validKeysDown); + RAW_SET_PROP_SWITCH_CASE_BASIC(validKeysUp); + RAW_SET_PROP_SWITCH_CASE_BASIC(draggedTypes); + RAW_SET_PROP_SWITCH_CASE_BASIC(tooltip); + RAW_SET_PROP_SWITCH_CASE_BASIC(cursor); + } +} + +inline void fromRawValue(const PropsParserContext &context, const RawValue &value, HandledKey &result) { + if (value.hasType>()) { + auto map = static_cast>(value); + for (const auto &pair : map) { + if (pair.first == "key") { + result.key = static_cast(pair.second); + } else if (pair.first == "altKey") { + result.altKey = static_cast(pair.second); + } else if (pair.first == "ctrlKey") { + result.ctrlKey = static_cast(pair.second); + } else if (pair.first == "shiftKey") { + result.shiftKey = static_cast(pair.second); + } else if (pair.first == "metaKey") { + result.metaKey = static_cast(pair.second); + } + } + } else if (value.hasType()) { + result.key = (std::string)value; + } +} + +inline void fromRawValue(const PropsParserContext &context, const RawValue &value, DraggedType &result) { + auto string = (std::string)value; + if (string == "fileUrl") { + result = DraggedType::FileUrl; + } else if (string == "image") { + result = DraggedType::Image; + } else if (string == "string") { + result = DraggedType::String; + } else { + abort(); + } +} + + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h new file mode 100644 index 00000000000000..2a39064a90374c --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::react { +class HostPlatformViewProps : public BaseViewProps { + public: + HostPlatformViewProps() = default; + HostPlatformViewProps( + const PropsParserContext &context, + const HostPlatformViewProps &sourceProps, + const RawProps &rawProps); + + void setProp( + const PropsParserContext& context, + RawPropsPropNameHash hash, + const char* propName, + const RawValue& value); + + MacOSViewEvents macOSViewEvents{}; + +#pragma mark - Props + + bool focusable{false}; + bool enableFocusRing{true}; + + std::optional> validKeysDown{}; + std::optional> validKeysUp{}; + + std::optional> draggedTypes{}; + + std::optional tooltip{}; +}; +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewTraitsInitializer.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewTraitsInitializer.h new file mode 100644 index 00000000000000..c9d8c782ad1bc5 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewTraitsInitializer.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react::HostPlatformViewTraitsInitializer { + +inline bool formsStackingContext(const ViewProps& props) { + return false; +} + +inline bool formsView(const ViewProps& props) { + return false; +} + +inline ShadowNodeTraits::Trait extraTraits() { + return ShadowNodeTraits::Trait::None; +} + +} // namespace facebook::react::HostPlatformViewTraitsInitializer diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/KeyEvent.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/KeyEvent.h new file mode 100644 index 00000000000000..cb3496218c0ab8 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/KeyEvent.h @@ -0,0 +1,108 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +/* + * Describes a request to handle a key input. + */ +struct HandledKey { + /** + * The key for the event aligned to https://www.w3.org/TR/uievents-key/. + */ + std::string key{}; + + /* + * A flag indicating if the alt key is pressed. + */ + std::optional altKey{}; + + /* + * A flag indicating if the control key is pressed. + */ + std::optional ctrlKey{}; + + /* + * A flag indicating if the shift key is pressed. + */ + std::optional shiftKey{}; + + /* + * A flag indicating if the meta key is pressed. + */ + std::optional metaKey{}; +}; + +inline static bool operator==(const HandledKey &lhs, const HandledKey &rhs) { + return lhs.key == rhs.key && lhs.altKey == rhs.altKey && lhs.ctrlKey == rhs.ctrlKey && + lhs.shiftKey == rhs.shiftKey && lhs.metaKey == rhs.metaKey; +} + +/** + * Key event emitted by handled key events. + */ +struct KeyEvent { + /** + * The key for the event aligned to https://www.w3.org/TR/uievents-key/. + */ + std::string key{}; + + /* + * A flag indicating if the alt key is pressed. + */ + bool altKey{false}; + + /* + * A flag indicating if the control key is pressed. + */ + bool ctrlKey{false}; + + /* + * A flag indicating if the shift key is pressed. + */ + bool shiftKey{false}; + + /* + * A flag indicating if the meta key is pressed. + */ + bool metaKey{false}; + + /* + * A flag indicating if the caps lock key is pressed. + */ + bool capsLockKey{false}; + + /* + * A flag indicating if the key on the numeric pad is pressed. + */ + bool numericPadKey{false}; + + /* + * A flag indicating if the help key is pressed. + */ + bool helpKey{false}; + + /* + * A flag indicating if a function key is pressed. + */ + bool functionKey{false}; +}; + +inline static bool operator==(const KeyEvent &lhs, const HandledKey &rhs) { + return lhs.key == rhs.key && + (!rhs.altKey.has_value() || lhs.altKey == *rhs.altKey) && + (!rhs.ctrlKey.has_value() || lhs.ctrlKey == *rhs.ctrlKey) && + (!rhs.shiftKey.has_value() || lhs.shiftKey == *rhs.shiftKey) && + (!rhs.metaKey.has_value() || lhs.metaKey == *rhs.metaKey); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MacOSViewEvents.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MacOSViewEvents.h new file mode 100644 index 00000000000000..a3506a41d75e30 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MacOSViewEvents.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include + +namespace facebook::react { + +struct MacOSViewEvents { + std::bitset<8> bits{}; + + enum class Offset : uint8_t { + // Keyboard Events + KeyDown = 1, + KeyUp = 2, + + // Mouse Events + MouseEnter = 3, + MouseLeave = 4, + DoubleClick = 5, + }; + + constexpr bool operator[](const Offset offset) const { + return bits[static_cast(offset)]; + } + + std::bitset<8>::reference operator[](const Offset offset) { + return bits[static_cast(offset)]; + } +}; + +inline static bool operator==(MacOSViewEvents const &lhs, MacOSViewEvents const &rhs) { + return lhs.bits == rhs.bits; +} + +inline static bool operator!=(MacOSViewEvents const &lhs, MacOSViewEvents const &rhs) { + return lhs.bits != rhs.bits; +} + +static inline MacOSViewEvents convertRawProp( + const PropsParserContext &context, + const RawProps &rawProps, + const MacOSViewEvents &sourceValue, + const MacOSViewEvents &defaultValue) { + MacOSViewEvents result{}; + using Offset = MacOSViewEvents::Offset; + + // Key Events + result[Offset::KeyDown] = + convertRawProp(context, rawProps, "onKeyDown", sourceValue[Offset::KeyDown], defaultValue[Offset::KeyDown]); + result[Offset::KeyUp] = + convertRawProp(context, rawProps, "onKeyUp", sourceValue[Offset::KeyUp], defaultValue[Offset::KeyUp]); + + // Mouse Events + result[Offset::MouseEnter] = + convertRawProp(context, rawProps, "onMouseEnter", sourceValue[Offset::MouseEnter], defaultValue[Offset::MouseEnter]); + result[Offset::MouseLeave] = + convertRawProp(context, rawProps, "onMouseLeave", sourceValue[Offset::MouseLeave], defaultValue[Offset::MouseLeave]); + result[Offset::DoubleClick] = + convertRawProp(context, rawProps, "onDoubleClick", sourceValue[Offset::DoubleClick], defaultValue[Offset::DoubleClick]); + + return result; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h new file mode 100644 index 00000000000000..aafa96e7917ec7 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +/* + * Describes a mouse enter/leave event. + */ +struct MouseEvent { + /** + * Pointer horizontal location in target view. + */ + Float clientX{0}; + + /** + * Pointer vertical location in target view. + */ + Float clientY{0}; + + /** + * Pointer horizontal location in window. + */ + Float screenX{0}; + + /** + * Pointer vertical location in window. + */ + Float screenY{0}; + + /* + * A flag indicating if the alt key is pressed. + */ + bool altKey{false}; + + /* + * A flag indicating if the control key is pressed. + */ + bool ctrlKey{false}; + + /* + * A flag indicating if the shift key is pressed. + */ + bool shiftKey{false}; + + /* + * A flag indicating if the meta key is pressed. + */ + bool metaKey{false}; +}; + +struct DataTransferItem { + std::string name{}; + std::string kind{}; + std::string type{}; + std::string uri{}; + std::optional size{}; + std::optional width{}; + std::optional height{}; +}; + +struct DragEvent : MouseEvent { + std::vector dataTransferItems; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h index b97035e7cde480..a1eab3f86e3015 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h @@ -92,22 +92,37 @@ enum class BorderCurve : uint8_t { Circular, Continuous }; enum class BorderStyle : uint8_t { Solid, Dotted, Dashed }; // [macOS [visionOS] -enum class Cursor : uint8_t { - Auto, +enum class Cursor : uint8_t { Alias, + Arrow, + Auto, + ClosedHand, ColumnResize, ContextualMenu, Copy, Crosshair, Default, DisappearingItem, + DragCopy, + DragLink, EastResize, Grab, Grabbing, - NorthResize, + IBeam, + IBeamCursorForVerticalLayout, NoDrop, + NorthResize, NotAllowed, + OpenHand, + OperationNotAllowed, Pointer, + PointingHand, + ResizeDown, + ResizeLeft, + ResizeLeftRight, + ResizeRight, + ResizeUp, + ResizeUpDown, RowResize, SouthResize, Text, diff --git a/packages/react-native/ReactCommon/react/renderer/telemetry/SurfaceTelemetry.cpp b/packages/react-native/ReactCommon/react/renderer/telemetry/SurfaceTelemetry.cpp index 6b4845f64688b7..64d46f8d475e8a 100644 --- a/packages/react-native/ReactCommon/react/renderer/telemetry/SurfaceTelemetry.cpp +++ b/packages/react-native/ReactCommon/react/renderer/telemetry/SurfaceTelemetry.cpp @@ -8,6 +8,7 @@ #include "SurfaceTelemetry.h" #include +#include namespace facebook::react { diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h index f94044409cdef5..58d06e3a14815b 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h @@ -62,6 +62,12 @@ using RCTTextLayoutFragmentEnumerationBlock = frame:(CGRect)frame usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block; +#if TARGET_OS_OSX // [macOS +- (NSTextStorage *)getTextStorageForAttributedString:(facebook::react::AttributedString)attributedString + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + frame:(CGRect)frame; +#endif // macOS] + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index d134df88154e47..e243fe1cc8ef8f 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -335,4 +335,15 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage return TextMeasurement{{size.width, size.height}, attachments}; } +#if TARGET_OS_OSX // [macOS +- (NSTextStorage *)getTextStorageForAttributedString:(AttributedString)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + frame:(CGRect)frame +{ + NSTextStorage *textStorage = [self textStorageForAttributesString:attributedString paragraphAttributes:paragraphAttributes size:frame.size]; + + return textStorage; +} +#endif // macOS] + @end diff --git a/packages/virtualized-lists/Lists/VirtualizedList.js b/packages/virtualized-lists/Lists/VirtualizedList.js index 2a5403ec96bb93..1fa23377ab6b56 100644 --- a/packages/virtualized-lists/Lists/VirtualizedList.js +++ b/packages/virtualized-lists/Lists/VirtualizedList.js @@ -1005,6 +1005,7 @@ class VirtualizedList extends StateSafePureComponent { cells.push( {React.cloneElement(element, { onLayout: (event: LayoutEvent) => { @@ -1061,6 +1062,7 @@ class VirtualizedList extends StateSafePureComponent { lastMetrics.offset + lastMetrics.length - firstMetrics.offset; cells.push( , @@ -1101,6 +1103,7 @@ class VirtualizedList extends StateSafePureComponent { cellKey={this._getFooterCellKey()} key="$footer"> = { CellRendererComponent?: ?React.ComponentType>, ItemSeparatorComponent: ?React.ComponentType< @@ -208,7 +210,7 @@ export default class CellRenderer extends React.Component< : horizontal ? [styles.row, inversionStyle] : inversionStyle; - const result = !CellRendererComponent ? ( + let result = !CellRendererComponent ? ( // [macOS] extends React.Component< ); + if (Platform.OS === 'macos') { // [macOS + result = React.cloneElement(result, {collapsable: false}); + } // macOS] + return ( {result}