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 7fe50d5b24d67a..6aaa1905c0c13d 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm @@ -398,17 +398,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 2a2f14e8d69770..a57420235dda61 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; @@ -88,6 +90,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 1d0b193c46a96f..ff30d49d7f7f28 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; @@ -58,7 +58,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 (nullable, nonatomic, copy) NSDictionary *typingAttributes; @property (nonatomic, assign) NSTextAlignment textAlignment; @property (nonatomic, getter=isAutomaticTextReplacementEnabled) BOOL automaticTextReplacementEnabled; @@ -69,6 +69,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 dd693c7b4f8574..f1d87a07ea71b3 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm @@ -99,6 +99,7 @@ @implementation RCTUITextField { #endif // [macOS] #if TARGET_OS_OSX // [macOS BOOL _isUpdatingPlaceholderText; + NSArray *_readablePasteboardTypes; #endif // macOS] } @@ -216,6 +217,11 @@ - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset #if TARGET_OS_OSX // [macOS +- (NSResponder *)responder +{ + return self; +} + + (Class)cellClass { return RCTUITextFieldCell.class; @@ -705,5 +711,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 a58f4a52231326..2afbc38fac555d 100644 --- a/packages/react-native/React/Base/macOS/RCTUIKit.m +++ b/packages/react-native/React/Base/macOS/RCTUIKit.m @@ -182,6 +182,7 @@ @implementation RCTUIView BOOL _userInteractionEnabled; NSTrackingArea *_trackingArea; BOOL _mouseDownCanMoveWindow; + BOOL _respondsToDisplayLayer; } + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key @@ -211,6 +212,7 @@ @implementation RCTUIView self->_userInteractionEnabled = YES; self->_enableFocusRing = YES; self->_mouseDownCanMoveWindow = YES; + self->_respondsToDisplayLayer = [self respondsToSelector:@selector(displayLayer:)]; } return self; } @@ -474,7 +476,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/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index 6e3284b91c728e..4a1a4ec74ad9c8 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 @@ -29,6 +31,30 @@ 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] // ParagraphTextView is an auxiliary view we set as contentView so the drawing // can happen on top of the layers manipulated by RCTViewComponentView (the parent view) @@ -62,10 +88,17 @@ @implementation RCTParagraphComponentView { ParagraphShadowNode::ConcreteState::Shared _state; ParagraphAttributes _paragraphAttributes; RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; -#if !TARGET_OS_OSX // [macOS] UILongPressGestureRecognizer *_longPressGestureRecognizer; -#endif // macOS] - RCTParagraphTextView *_textView; +} +#else // [macOS +@interface RCTParagraphComponentView () +@end + +@implementation RCTParagraphComponentView { + ParagraphShadowNode::ConcreteState::Shared _state; + ParagraphAttributes _paragraphAttributes; + RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; + RCTParagraphComponentUnfocusableTextView *_textView; } - (instancetype)initWithFrame:(CGRect)frame @@ -75,8 +108,6 @@ - (instancetype)initWithFrame:(CGRect)frame #if !TARGET_OS_OSX // [macOS] self.opaque = NO; - _textView = [RCTParagraphTextView new]; - _textView.backgroundColor = RCTUIColor.clearColor; // [macOS] #else // [macOS // Make the RCTParagraphComponentView accessible and available in the a11y hierarchy. self.accessibilityElement = YES; @@ -84,7 +115,8 @@ - (instancetype)initWithFrame:(CGRect)frame // Fix blurry text on non-retina displays. self.canDrawSubviewsIntoLayer = YES; // The NSTextView is responsible for drawing text and managing selection. - _textView = [[RCTParagraphTextView alloc] initWithFrame:self.bounds]; + _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; @@ -97,7 +129,6 @@ - (instancetype)initWithFrame:(CGRect)frame self.contentView = _textView; self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; #endif // macOS] - self.contentView = _textView; } return self; @@ -185,6 +216,11 @@ - (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)layoutSubviews @@ -205,6 +241,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. @@ -213,7 +250,6 @@ - (BOOL)isAccessibilityElement return NO; } -#if !TARGET_OS_OSX // [macOS] - (NSArray *)accessibilityElements { const auto ¶graphProps = static_cast(*_props); @@ -325,14 +361,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); @@ -347,6 +462,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 f78ea35e3fc302..77c71330395fab 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -12,10 +12,20 @@ #import #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" @@ -86,7 +96,11 @@ - (instancetype)initWithFrame:(CGRect)frame const auto &defaultProps = TextInputShadowNode::defaultSharedProps(); _props = defaultProps; - _backedTextInputView = defaultProps->multiline ? [RCTUITextView new] : [RCTUITextField new]; +#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; @@ -127,8 +141,12 @@ - (void)didMoveToWindow if (props.autoFocus) { #if !TARGET_OS_OSX // [macOS] [_backedTextInputView becomeFirstResponder]; -#endif // [macOS] - [self scrollCursorIntoView]; +#else // [macOS + NSWindow *window = _backedTextInputView.window; + if (window) { + [window makeFirstResponder:_backedTextInputView.responder]; + } +#endif // macOS] } _didMoveToWindow = YES; #if TARGET_OS_IOS // [macOS] [visionOS] @@ -272,11 +290,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); } @@ -334,16 +356,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) { _backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID); } - - if (newTextInputProps.inputAccessoryViewButtonLabel != oldTextInputProps.inputAccessoryViewButtonLabel) { - _backedTextInputView.inputAccessoryViewButtonLabel = - RCTNSStringFromString(newTextInputProps.inputAccessoryViewButtonLabel); - } - - if (newTextInputProps.disableKeyboardShortcuts != oldTextInputProps.disableKeyboardShortcuts) { - _backedTextInputView.disableKeyboardShortcuts = newTextInputProps.disableKeyboardShortcuts; + +#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]; #if TARGET_OS_IOS // [macOS] [visionOS] @@ -543,12 +563,23 @@ - (void)textInputDidChangeSelection } #if TARGET_OS_OSX // [macOS +- (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({.autoCorrectEnabled = static_cast(enabled)}); + std::static_pointer_cast(_eventEmitter)->onAutoCorrectChange({.enabled = static_cast(enabled)}); } } + +- (void)continuousSpellCheckingDidChange:(BOOL)enabled {} + + - (void)continuousSpellCheckingDidChange:(BOOL)enabled { if (_eventEmitter) { @@ -564,39 +595,143 @@ - (void)grammarCheckingDidChange:(BOOL)enabled } - (BOOL)hasValidKeyDownOrValidKeyUp:(nonnull NSString *)key { - return YES; + std::string keyString = key.UTF8String; + + if (_props->validKeysDown.has_value()) { + for (auto const &validKey : *_props->validKeysDown) { + if (validKey.key == keyString) { + 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]); + } -- (void)textInputDidCancel {} + if (props.traits.clearTextOnSubmit) { + _backedTextInputView.attributedText = nil; + [self textInputDidChange]; + } + } +} + +- (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] @@ -606,7 +741,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] } } @@ -622,35 +778,24 @@ - (void)focus #if !TARGET_OS_OSX // [macOS] [_backedTextInputView becomeFirstResponder]; #else // [macOS - NSWindow *window = [_backedTextInputView window]; - [window makeFirstResponder:_backedTextInputView]; -#endif // macOS] - - const auto &props = static_cast(*_props); - - if (props.traits.clearTextOnFocus) { - _backedTextInputView.attributedText = nil; - [self textInputDidChange]; - } - - if (props.traits.selectTextOnFocus) { - [_backedTextInputView selectAll:nil]; - [self textInputDidChangeSelection]; + NSWindow *window = _backedTextInputView.window; + if (window) { + [window makeFirstResponder:_backedTextInputView.responder]; } - - [self scrollCursorIntoView]; +#endif // macOS] } - (void)blur { #if !TARGET_OS_OSX // [macOS] [_backedTextInputView resignFirstResponder]; -#else // [macOS - NSWindow *window = [_backedTextInputView window]; - if ([window firstResponder] == _backedTextInputView) { +#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] +#endif // macOS]; } - (void)setTextAndSelection:(NSInteger)eventCount @@ -679,7 +824,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; } @@ -835,8 +984,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] } @@ -857,16 +1006,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; - // Updating the UITextView attributedText, for example changing the lineHeight, the color or adding - // a new paragraph with \n, causes the cursor to move to the end of the Text and scroll. - // This is fixed by restoring the cursor position and scrolling to that position (iOS issue 652653). + +#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 @@ -882,7 +1042,16 @@ - (void)_setAttributedString:(NSAttributedString *)attributedString [self _restoreTextSelection]; [self _updateTypingAttributes]; _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] } // Ensure that newly typed text will inherit any custom attributes. We follow the logic of RN Android, where attributes @@ -925,7 +1094,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); @@ -933,22 +1102,24 @@ - (void)_setMultiline:(BOOL)multiline [self addSubview:_backedTextInputView]; } -#if !TARGET_OS_OSX // [macOS] -- (void)_setShowSoftInputOnFocus:(BOOL)showSoftInputOnFocus +#if TARGET_OS_OSX // [macOS +- (void)_setSecureTextEntry:(BOOL)secureTextEntry { - if (showSoftInputOnFocus) { - // Resets to default keyboard. - _backedTextInputView.inputView = nil; - - // Without the call to reloadInputViews, the keyboard will not change until the textInput field (the first - // responder) loses and regains focus. - if (_backedTextInputView.isFirstResponder) { - [_backedTextInputView reloadInputViews]; - } - } else { - // Hides keyboard, but keeps blinking cursor. - _backedTextInputView.inputView = [UIView new]; + [_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] 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 204dda8818cc04..3aea9fc83044d7 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 bc03fcd3763cec..ba52b3d670225e 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 76dc6d740d04c2..65612654a86d3b 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -79,6 +79,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 78e80e67d7a1e6..e168c13977605a 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -19,7 +19,11 @@ #import // [macOS] #import #import -#import +#import // [macOS] +#import // [macOS] +#if TARGET_OS_OSX // [macOS +#import // [macOS] +#endif // macOS] #import #import #import @@ -45,6 +49,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; RCTPlatformView *_containerView; // [macOS] @@ -310,7 +318,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] } // `cursor` @@ -579,6 +591,45 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & needsInvalidateLayer = YES; } +#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); @@ -641,6 +692,9 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask _needsInvalidateLayer = NO; [self invalidateLayer]; + + [self updateTrackingAreas]; + [self updateClipViewBoundsObserverIfNeeded]; } - (void)prepareForRecycle @@ -824,61 +878,59 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle) } #if TARGET_OS_OSX // [macOS -static RCTCursor RCTCursorFromCursor(Cursor cursor) +static NSCursor *NSCursorFromCursor(Cursor cursor) { switch (cursor) { - case Cursor::Auto: - return RCTCursorAuto; case Cursor::Alias: - return RCTCursorAlias; - case Cursor::AllScroll: - return RCTCursorAllScroll; - case Cursor::Cell: - return RCTCursorCell; - case Cursor::ColResize: - return RCTCursorColResize; - case Cursor::ContextMenu: - return RCTCursorContextMenu; + 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: + return [NSCursor contextualMenuCursor]; case Cursor::Copy: return RCTCursorCopy; case Cursor::Crosshair: return RCTCursorCrosshair; case Cursor::Default: - return RCTCursorDefault; - case Cursor::EResize: - return RCTCursorEResize; - case Cursor::EWResize: - return RCTCursorEWResize; + 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 RCTCursorGrab; case Cursor::Grabbing: - return RCTCursorGrabbing; - case Cursor::Help: - return RCTCursorHelp; - case Cursor::Move: - return RCTCursorMove; - case Cursor::NEResize: - return RCTCursorNEResize; - case Cursor::NESWResize: - return RCTCursorNESWResize; - case Cursor::NResize: - return RCTCursorNResize; - case Cursor::NSResize: - return RCTCursorNSResize; - case Cursor::NWResize: - return RCTCursorNWResize; - case Cursor::NWSEResize: - return RCTCursorNWSEResize; + 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 RCTCursorNoDrop; case Cursor::None: return RCTCursorNone; case Cursor::NotAllowed: - return RCTCursorNotAllowed; + return [NSCursor operationNotAllowedCursor]; + case Cursor::OpenHand: + return [NSCursor openHandCursor]; + case Cursor::OperationNotAllowed: + return [NSCursor operationNotAllowedCursor]; case Cursor::Pointer: - return RCTCursorPointer; - case Cursor::Progress: - return RCTCursorProgress; + return [NSCursor pointingHandCursor]; + case Cursor::PointingHand: + return [NSCursor pointingHandCursor]; case Cursor::RowResize: return RCTCursorRowResize; case Cursor::SResize: @@ -892,74 +944,25 @@ static RCTCursor RCTCursorFromCursor(Cursor cursor) case Cursor::Url: return RCTCursorUrl; case Cursor::VerticalText: - return RCTCursorVerticalText; - case Cursor::WResize: - return RCTCursorWResize; - case Cursor::Wait: - return RCTCursorWait; - case Cursor::ZoomIn: - return RCTCursorZoomIn; - case Cursor::ZoomOut: - return RCTCursorZoomOut; + 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] -static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle) -{ - switch (outlineStyle) { - case OutlineStyle::Solid: - return RCTBorderStyleSolid; - case OutlineStyle::Dotted: - return RCTBorderStyleDotted; - case OutlineStyle::Dashed: - return RCTBorderStyleDashed; - } -} - -- (BOOL)styleWouldClipOverflowInk -{ - const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics); - BOOL nonZeroBorderWidth = !(borderMetrics.borderWidths.isUniform() && borderMetrics.borderWidths.left == 0); - BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox(); - return _props->getClipsContentToBounds() && - ((!_props->boxShadow.empty() || (clipToPaddingBox && nonZeroBorderWidth)) || _props->outlineWidth != 0); -} - -// This UIView is the UIView that holds all subviews. It is sometimes not self -// because we want to render "overflow ink" that extends beyond the bounds of -// the view and is not affected by clipping. -- (RCTUIView *)currentContainerView // [macOS] -{ - if (_useCustomContainerView) { - if (!_containerView) { - _containerView = [[RCTPlatformView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)]; // [macOS] - for (RCTPlatformView *subview in self.subviews) { // [macOS] - [_containerView addSubview:subview]; - } - _containerView.clipsToBounds = self.clipsToBounds; - self.clipsToBounds = NO; - _containerView.layer.mask = self.layer.mask; - self.layer.mask = nil; - [self addSubview:_containerView]; - } - - return _containerView; - } else { - if (_containerView) { - for (RCTPlatformView *subview in _containerView.subviews) { // [macOS] - [self addSubview:subview]; - } - self.clipsToBounds = _containerView.clipsToBounds; - self.layer.mask = _containerView.layer.mask; - [_containerView removeFromSuperview]; - _containerView = nil; - } - - return self; - } -} - - (void)invalidateLayer { CALayer *layer = self.layer; @@ -1510,6 +1513,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; @@ -1520,6 +1938,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/RCTConversions.h b/packages/react-native/React/Fabric/RCTConversions.h index ed9221e59d29be..b27a4390fd0e92 100644 --- a/packages/react-native/React/Fabric/RCTConversions.h +++ b/packages/react-native/React/Fabric/RCTConversions.h @@ -14,6 +14,10 @@ #import #import +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] + NS_ASSUME_NONNULL_BEGIN inline NSString *RCTNSStringFromString( @@ -281,4 +285,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/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 1bff9d91b527c3..6caae1882e9322 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -180,8 +180,15 @@ Pod::Spec.new do |s| ss.source_files = "react/renderer/scheduler/**/*.{m,mm,cpp,h}" ss.header_dir = "react/renderer/scheduler" - ss.dependency "React-performancetimeline" - ss.dependency "React-Fabric/observers/events" + ss.subspec "view" do |sss| + sss.dependency folly_dep_name, folly_version + 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", "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 end s.subspec "imagemanager" do |ss| diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/conversions.h index d57d3f24064a82..18622478c2103b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/conversions.h @@ -217,4 +217,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/platform/ios/react/renderer/components/iostextinput/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h index d71d58e9bc26ac..5e334d6df5c99d 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h @@ -83,6 +83,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. */ @@ -223,10 +241,31 @@ class TextInputTraits final { #ifdef TARGET_OS_OSX // [macOS /* * Can be empty (`null` in JavaScript) which means `default`. - * maOS + * 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] }; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h index f4f23a3c74842f..46db6976fe2f5d 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h @@ -143,14 +143,13 @@ static TextInputTraits convertRawProp( "grammarCheck", sourceTraits.grammarCheck, defaultTraits.grammarCheck); -#endif // macOS] - - traits.dataDetectorTypes = convertRawProp( + traits.pastedTypes = convertRawProp( context, rawProps, - "dataDetectorTypes", - sourceTraits.dataDetectorTypes, - defaultTraits.dataDetectorTypes); + "pastedTypes", + sourceTraits.pastedTypes, + defaultTraits.pastedTypes); +#endif // macOS] return traits; } @@ -188,4 +187,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/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index bcc4ef045fcddf..0fedaeb2fc99ac 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -59,10 +59,16 @@ void ViewShadowNode::initialize() noexcept { viewProps.accessibilityViewIsModal || viewProps.importantForAccessibility != ImportantForAccessibility::Auto || viewProps.removeClippedSubviews || viewProps.cursor != Cursor::Auto || - !viewProps.filter.empty() || viewProps.mixBlendMode != BlendMode::Normal || viewProps.isolation == Isolation::Isolate || - HostPlatformViewTraitsInitializer::formsStackingContext(viewProps); +#if TARGET_OS_OSX // [macOS + viewProps.focusable || + viewProps.enableFocusRing || + viewProps.macOSViewEvents[MacOSViewEvents::Offset::MouseEnter] || + viewProps.macOSViewEvents[MacOSViewEvents::Offset::MouseLeave] || +#endif // macOS] + HostPlatformViewTraitsInitializer::formsStackingContext(viewProps) + ; bool formsView = formsStackingContext || isColorMeaningful(viewProps.backgroundColor) || hasBorder() || 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 8ec8e1ab29bec2..4cffd902a9a59c 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/primitives.h @@ -126,6 +126,8 @@ enum class Cursor : uint8_t { NoDrop, None, NotAllowed, + OpenHand, + OperationNotAllowed, Pointer, Progress, RowResize, 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 {