diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index b4796e1abc0e33..038758cfb6ff76 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -137,11 +137,9 @@ export type TextInputEditingEvent = // [macOS macOS-only export type SettingChangeEvent = NativeSyntheticEvent< - $ReadOnly<{| - autoCorrectEnabled: boolean, - spellCheckEnabled: boolean, - grammarCheckEnabled: boolean, - |}>, + $ReadOnly<{ + enabled: boolean, + }>, >; export type PasteEvent = NativeSyntheticEvent< @@ -150,14 +148,14 @@ export type PasteEvent = NativeSyntheticEvent< |}>, >; -export type SubmitKeyEvent = $ReadOnly<{| +export type SubmitKeyEvent = $ReadOnly<{ key: string, altKey?: ?boolean, ctrlKey?: ?boolean, metaKey?: ?boolean, shiftKey?: ?boolean, functionKey?: ?boolean, -|}>; +}>; // macOS] // [macOS @@ -443,7 +441,7 @@ export type TextInputIOSProps = $ReadOnly<{ }>; // [macOS -type MacOSProps = $ReadOnly<{| +export type TextInputMacOSProps = $ReadOnly<{ /** * If `true`, clears the text field synchronously before `onSubmitEditing` is emitted. * @@ -528,7 +526,7 @@ type MacOSProps = $ReadOnly<{| * @platform macos */ tooltip?: ?string, -|}>; +}>; // macOS] export type TextInputAndroidProps = $ReadOnly<{ @@ -624,7 +622,7 @@ export type PastedTypesType = PasteType | $ReadOnlyArray; // [macOS] export type TextInputProps = $ReadOnly<{ ...$Diff>, ...TextInputIOSProps, - ...MacOSProps, // [macOS] + ...TextInputMacOSProps, // [macOS] ...TextInputAndroidProps, /** @@ -2043,6 +2041,7 @@ const autoCompleteWebToTextContentTypeMap = { const ExportedForwardRef: component( ref: React.RefSetter, ...props: React.ElementConfig + // $FlowFixMe[incompatible-call] ) = React.forwardRef(function TextInput( { allowFontScaling = true, diff --git a/packages/react-native/Libraries/Components/View/DraggedType.js b/packages/react-native/Libraries/Components/View/DraggedType.js index 2e4ff89ca045d0..3d43a4e24816f4 100644 --- a/packages/react-native/Libraries/Components/View/DraggedType.js +++ b/packages/react-native/Libraries/Components/View/DraggedType.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. diff --git a/packages/react-native/Libraries/Image/RCTImageLoader.mm b/packages/react-native/Libraries/Image/RCTImageLoader.mm index 06ad222a6387f4..07e32e378321f6 100644 --- a/packages/react-native/Libraries/Image/RCTImageLoader.mm +++ b/packages/react-native/Libraries/Image/RCTImageLoader.mm @@ -629,6 +629,15 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req [self setUp]; } +#if TARGET_OS_OSX // [macOS + BOOL useDefaultLoading = [loadHandler respondsToSelector:@selector(defaultLoadingURL:)]; + if (useDefaultLoading) { + NSMutableURLRequest *updatedRequest = [request mutableCopy]; + updatedRequest.URL = [loadHandler defaultLoadingURL:request.URL]; + request = updatedRequest; + } +#endif // macOS] + __weak RCTImageLoader *weakSelf = self; dispatch_async(_URLRequestQueue, ^{ __typeof(self) strongSelf = weakSelf; @@ -636,7 +645,7 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req return; } - if (loadHandler) { + if (loadHandler && !useDefaultLoading) { // [macOS] dispatch_block_t cancelLoadLocal; if ([loadHandler conformsToProtocol:@protocol(RCTImageURLLoaderWithAttribution)]) { RCTImageURLLoaderRequest *loaderRequest = [(id)loadHandler diff --git a/packages/react-native/Libraries/Image/RCTImageURLLoader.h b/packages/react-native/Libraries/Image/RCTImageURLLoader.h index 755e1adc5ae561..da1a6541c8596e 100644 --- a/packages/react-native/Libraries/Image/RCTImageURLLoader.h +++ b/packages/react-native/Libraries/Image/RCTImageURLLoader.h @@ -78,6 +78,14 @@ typedef dispatch_block_t RCTImageLoaderCancellationBlock; */ - (BOOL)shouldCacheLoadedImages; +#if TARGET_OS_OSX // [macOS +/** + * If defined, the image loading of the RCTImageLoader will be used to load the image using + * the returned URL. This allows rewriting the URL before the image loader starts the request. + */ +- (NSURL *)defaultLoadingURL:(NSURL *)requestURL; +#endif // macOS] + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/Libraries/Image/RCTImageView.mm b/packages/react-native/Libraries/Image/RCTImageView.mm index 32b060c2b52d9c..7518bafecf95ad 100644 --- a/packages/react-native/Libraries/Image/RCTImageView.mm +++ b/packages/react-native/Libraries/Image/RCTImageView.mm @@ -144,6 +144,9 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _bridge = bridge; #if TARGET_OS_OSX // [macOS self.wantsLayer = YES; + _resizeMode = RCTResizeModeCover; + _imageView.contentMode = (UIViewContentMode)RCTResizeModeCover; + [_imageView unregisterDraggedTypes]; #endif // macOS] _imageView = [RCTUIImageViewAnimated new]; _imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; @@ -195,10 +198,6 @@ - (void)updateWithImage:(UIImage *)image #else // [macOS image.capInsets = _capInsets; image.resizingMode = NSImageResizingModeTile; - } else if (_resizeMode == RCTResizeModeCover) { - if (!NSEqualSizes(self.bounds.size, NSZeroSize)) { - image = RCTFillImagePreservingAspectRatio(image, self.bounds.size, self.window.backingScaleFactor ?: 1.0); - } #endif // macOS] } else if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, _capInsets)) { // Applying capInsets of 0 will switch the "resizingMode" of the image to "tile" which is undesired @@ -284,21 +283,9 @@ - (void)setResizeMode:(RCTResizeMode)resizeMode if (_resizeMode == RCTResizeModeRepeat) { // Repeat resize mode is handled by the UIImage. Use scale to fill // so the repeated image fills the UIImageView. -#if !TARGET_OS_OSX // [macOS] _imageView.contentMode = UIViewContentModeScaleToFill; -#else // [macOS - _imageView.imageScaling = NSImageScaleAxesIndependently; -#endif // macOS] } else { -#if !TARGET_OS_OSX // [macOS] _imageView.contentMode = (UIViewContentMode)resizeMode; -#else // [macOS - // This relies on having previously resampled the image to a size that exceeds the image view. - if (resizeMode == RCTResizeModeCover) { - resizeMode = RCTResizeModeCenter; - } - _imageView.imageScaling = (NSImageScaling)resizeMode; -#endif // macOS] } if ([self shouldReloadImageSourceAfterResize]) { @@ -566,7 +553,7 @@ - (void)reactSetFrame:(CGRect)frame !RCTShouldReloadImageForSizeChange(_targetSize, idealSize)) // [macOS #if TARGET_OS_OSX // Since macOS doen't support UIViewContentModeScaleAspectFill, we have to manually resample the image - // If we're in cover mode we need to ensure that the image is re-sampled to the correct size when the container size (shrinking + // If we're in cover mode we need to ensure that the image is re-sampled to the correct size when the container size (shrinking // being the most obvious case) otherwise we will end up in a state an image will not properly scale inside its container && (RCTResizeModeFromUIViewContentMode(_imageView.contentMode) != RCTResizeModeCover || (imageSize.width == idealSize.width && imageSize.height == idealSize.height)) @@ -617,13 +604,13 @@ - (void)didMoveToWindow [self reloadImage]; } } - + #if TARGET_OS_OSX // [macOS - (void)viewDidChangeBackingProperties { [self reloadImage]; } - + - (RCTPlatformView *)reactAccessibilityElement { return _imageView; diff --git a/packages/react-native/Libraries/Image/RCTResizeMode.h b/packages/react-native/Libraries/Image/RCTResizeMode.h index 53630523560fdf..2e4b09ccb14942 100644 --- a/packages/react-native/Libraries/Image/RCTResizeMode.h +++ b/packages/react-native/Libraries/Image/RCTResizeMode.h @@ -8,21 +8,12 @@ #import typedef NS_ENUM(NSInteger, RCTResizeMode) { -#if !TARGET_OS_OSX // [macOS] RCTResizeModeCover = UIViewContentModeScaleAspectFill, RCTResizeModeContain = UIViewContentModeScaleAspectFit, RCTResizeModeStretch = UIViewContentModeScaleToFill, RCTResizeModeCenter = UIViewContentModeCenter, - RCTResizeModeRepeat = -1, // Use negative values to avoid RCTResizeModeNone = UIViewContentModeTopLeft, -#else // [macOS - RCTResizeModeCover = -2, // Not supported by NSImageView - RCTResizeModeContain = NSImageScaleProportionallyUpOrDown, - RCTResizeModeStretch = NSImageScaleAxesIndependently, - RCTResizeModeCenter = -3, // assumes NSImageAlignmentCenter - RCTResizeModeRepeat = -1, - RCTResizeModeNone = NSImageScaleNone, -#endif // macOS] + RCTResizeModeRepeat = -1, // Use negative values to avoid conflicts with iOS enum values. }; static inline RCTResizeMode RCTResizeModeFromUIViewContentMode(UIViewContentMode mode) diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js index 84f5db15f92a37..4947177c6a20d9 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js @@ -34,6 +34,9 @@ const bubblingEventTypes = { const directEventTypes = { ...PlatformBaseViewConfigIos.directEventTypes, + topDoubleClick: { + registrationName: 'onDoubleClick', + }, topDragEnter: { registrationName: 'onDragEnter', }, @@ -67,6 +70,7 @@ const validAttributesForNonEventProps = { // Props for bubbling and direct events const validAttributesForEventProps = ConditionallyIgnoredEventHandlers({ + onDoubleClick: true, onBlur: true, onDragEnter: true, onDragLeave: true, diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.js b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.js index 9b9506e8a45f2f..3c14681eea8161 100644 --- a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.js +++ b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.js @@ -1,5 +1,5 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.macos.js b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.macos.js index d0890a335d4903..3c14681eea8161 100644 --- a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.macos.js +++ b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.macos.js @@ -1,5 +1,5 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. @@ -14,11 +14,6 @@ import type {ColorValue} from './StyleSheet'; -import { - ColorWithSystemEffectMacOSPrivate, - DynamicColorMacOSPrivate, -} from './PlatformColorValueTypes.macos'; - export type DynamicColorMacOSTuple = { light: ColorValue, dark: ColorValue, @@ -29,12 +24,7 @@ export type DynamicColorMacOSTuple = { export const DynamicColorMacOS = ( tuple: DynamicColorMacOSTuple, ): ColorValue => { - return DynamicColorMacOSPrivate({ - light: tuple.light, - dark: tuple.dark, - highContrastLight: tuple.highContrastLight, - highContrastDark: tuple.highContrastDark, - }); + throw new Error('DynamicColorMacOS is not available on this platform.'); }; export type SystemEffectMacOS = @@ -48,5 +38,7 @@ export const ColorWithSystemEffectMacOS = ( color: ColorValue, effect: SystemEffectMacOS, ): ColorValue => { - return ColorWithSystemEffectMacOSPrivate(color, effect); + throw new Error( + 'ColorWithSystemEffectMacOS is not available on this platform.', + ); }; diff --git a/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm b/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm index 73fa04e10da85b..d5669bd8ba7853 100644 --- a/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm +++ b/packages/react-native/Libraries/Text/BaseText/RCTBaseTextViewManager.mm @@ -60,6 +60,7 @@ - (RCTShadowView *)shadowView #if TARGET_OS_OSX // [macOS RCT_REMAP_SHADOW_PROPERTY(cursor, textAttributes.cursor, RCTCursor) +RCT_REMAP_SHADOW_PROPERTY(href, textAttributes.href, NSString) #endif // macOS] @end diff --git a/packages/react-native/Libraries/Text/RCTTextAttributes.h b/packages/react-native/Libraries/Text/RCTTextAttributes.h index 8d39cde1f64753..e2f7573780f430 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.h +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.h @@ -61,6 +61,7 @@ extern NSString *const RCTTextAttributesTagAttributeName; #if TARGET_OS_OSX // [macOS @property (nonatomic, assign) RCTCursor cursor; +@property (nonatomic, copy, nullable) NSString *href; #endif // macOS] #pragma mark - Inheritance diff --git a/packages/react-native/Libraries/Text/RCTTextAttributes.mm b/packages/react-native/Libraries/Text/RCTTextAttributes.mm index 9b9622bad08e72..8578b21eec2c33 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.mm +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.mm @@ -111,6 +111,7 @@ - (void)applyTextAttributes:(RCTTextAttributes *)textAttributes textAttributes->_textTransform != RCTTextTransformUndefined ? textAttributes->_textTransform : _textTransform; #if TARGET_OS_OSX // [macOS _cursor = textAttributes->_cursor != RCTCursorAuto ? textAttributes->_cursor : _cursor; + _href = textAttributes->_href ?: _href; #endif // macOS] } @@ -237,6 +238,9 @@ - (NSParagraphStyle *)effectiveParagraphStyle if (_cursor != RCTCursorAuto) { attributes[NSCursorAttributeName] = NSCursorFromRCTCursor(_cursor); } + if (_href) { + attributes[NSLinkAttributeName] = [RCTConvert NSURL:_href]; + } #endif // macOS] return [attributes copy]; diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.h b/packages/react-native/Libraries/Text/Text/RCTTextView.h index 3de368b73b6bba..d9cba3dbbb1eac 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.h +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.h @@ -12,7 +12,11 @@ NS_ASSUME_NONNULL_BEGIN +#if !TARGET_OS_OSX // [macOS] @interface RCTTextView : RCTUIView // [macOS] +#else // [macOS +@interface RCTTextView : RCTUIView +#endif // macOS] - (instancetype)initWithEventDispatcher:(id)eventDispatcher; // [macOS] diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.mm b/packages/react-native/Libraries/Text/Text/RCTTextView.mm index abff88e692bd8e..4860bd55a8bab2 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.mm @@ -22,6 +22,8 @@ #import #if TARGET_OS_OSX // [macOS +#import +#import // We are managing the key view loop using the RCTTextView. // Disable key view for backed NSTextView so we don't get double focus. @@ -35,6 +37,16 @@ - (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] @@ -64,6 +76,10 @@ @implementation RCTTextView { CGRect _contentFrame; } +#if TARGET_OS_OSX // [macOS +@synthesize additionalMenuItems = _additionalMenuItems; +#endif // macOS] + // [macOS - (instancetype)initWithEventDispatcher:(id)eventDispatcher { @@ -82,12 +98,16 @@ - (instancetype)initWithFrame:(CGRect)frame self.accessibilityTraits |= UIAccessibilityTraitStaticText; self.opaque = NO; #else // [macOS + // Make the RCTTextView accessible and available in the a11y hierarchy. + self.accessibilityElement = YES; self.accessibilityRole = NSAccessibilityStaticTextRole; // Fix blurry text on non-retina displays. self.canDrawSubviewsIntoLayer = YES; // The NSTextView is responsible for drawing text and managing selection. _textView = [[RCTUnfocusableTextView alloc] initWithFrame:self.bounds]; _textView.delegate = self; + // The RCTUnfocusableTextView is only used for rendering and should not appear in the a11y hierarchy. + _textView.accessibilityElement = NO; _textView.usesFontPanel = NO; _textView.drawsBackground = NO; _textView.linkTextAttributes = @{}; @@ -134,9 +154,6 @@ - (void)setSelectable:(BOOL)selectable } #else // [macOS _textView.selectable = _selectable; - if (_selectable) { - [self setFocusable:YES]; - } #endif // macOS] } @@ -253,8 +270,8 @@ - (void)drawRect:(CGRect)rect usingBlock:^(CGRect enclosingRect, __unused BOOL *anotherStop) { // [macOS UIBezierPath *path = UIBezierPathWithRoundedRect( - CGRectInset(enclosingRect, -2, -2), - 2); + CGRectInset(enclosingRect, -2, -2), + 2); // [macOS] if (highlightPath) { #if !TARGET_OS_OSX // [macOS] @@ -403,17 +420,38 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture #else // [macOS +- (NSMenu *)textView:(NSTextView *)view menu:(NSMenu *)menu forEvent:(NSEvent *)event atIndex:(NSUInteger)charIndex +{ + [[RCTTouchHandler touchHandlerForView:self] willShowMenuWithEvent:event]; + + [menu setAutoenablesItems:NO]; + + RCTHideMenuItemsWithFilterPredicate(menu, ^bool(NSMenuItem *item) { + // Remove items not applicable for readonly text. + return (item.action == @selector(cut:) || item.action == @selector(paste:) || RCTMenuItemHasSubmenuItemWithAction(item, @selector(checkSpelling:)) || RCTMenuItemHasSubmenuItemWithAction(item, @selector(orderFrontSubstitutionsPanel:))); + }); + + if (_additionalMenuItems && _additionalMenuItems.count > 0) { + [menu insertItem:[NSMenuItem separatorItem] atIndex:0]; + for (NSMenuItem* item in [_additionalMenuItems reverseObjectEnumerator]) { + [menu insertItem:item atIndex:0]; + } + } + + return menu; +} + - (NSView *)hitTest:(NSPoint)point { // We will forward mouse click events to the NSTextView ourselves to prevent NSTextView from swallowing events that may be handled in JS (e.g. long press). NSView *hitView = [super hitTest:point]; - + 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; } @@ -483,21 +521,6 @@ - (BOOL)canBecomeFirstResponder return _selectable; } #else // [macOS -- (BOOL)canBecomeKeyView -{ - return self.focusable; -} - -- (void)drawFocusRingMask { - if (self.focusable && self.enableFocusRing) { - NSRectFill([self bounds]); - } -} - -- (NSRect)focusRingMaskBounds { - return [self bounds]; -} - - (BOOL)becomeFirstResponder { if (![super becomeFirstResponder]) { @@ -510,16 +533,6 @@ - (BOOL)becomeFirstResponder return YES; } -- (BOOL)resignFirstResponder -{ - // Don't relinquish first responder while selecting text. - if (_selectable && NSRunLoop.currentRunLoop.currentMode == NSEventTrackingRunLoopMode) { - return NO; - } - - return [super resignFirstResponder]; -} - - (BOOL)canBecomeFirstResponder { return self.focusable; diff --git a/packages/react-native/Libraries/Text/Text/RCTTextViewManager.mm b/packages/react-native/Libraries/Text/Text/RCTTextViewManager.mm index 6afe1b7a288bee..1b17eab11bf055 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextViewManager.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextViewManager.mm @@ -35,6 +35,8 @@ @implementation RCTTextViewManager { RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL) +RCT_EXPORT_OSX_VIEW_PROPERTY(focusable, BOOL) + - (void)setBridge:(RCTBridge *)bridge { [super setBridge:bridge]; diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h index 9be114752fa489..4b7067b04405a7 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h @@ -5,66 +5,64 @@ * LICENSE file in the root directory of this source tree. */ - #import // [macOS] - - #import "RCTTextUIKit.h" // [macOS] - - #import - #import - - NS_ASSUME_NONNULL_BEGIN - - /* - * Just regular UITextView... but much better! - */ - @interface RCTUITextView : UITextView - - - (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer NS_UNAVAILABLE; - - (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE; - - @property (nonatomic, weak) id textInputDelegate; - - @property (nonatomic, assign) BOOL contextMenuHidden; - #if !TARGET_OS_OSX // [macOS] - @property (nonatomic, assign, readonly) BOOL textWasPasted; - #else // [macOS - @property (nonatomic, assign) BOOL textWasPasted; - #endif // macOS] - @property (nonatomic, assign, readonly) BOOL dictationRecognizing; - @property (nonatomic, copy, nullable) NSString *placeholder; - @property (nonatomic, strong, nullable) RCTUIColor *placeholderColor; // [macOS] - - @property (nonatomic, assign) CGFloat preferredMaxLayoutWidth; - - #if !TARGET_OS_OSX // [macOS] - // The `clearButtonMode` property actually is not supported yet; - // it's declared here only to conform to the interface. - @property (nonatomic, assign) UITextFieldViewMode clearButtonMode; - #endif // [macOS] - - @property (nonatomic, assign) BOOL caretHidden; - - @property (nonatomic, strong, nullable) NSString *inputAccessoryViewID; - @property (nonatomic, strong, nullable) NSString *inputAccessoryViewButtonLabel; - - @property (nonatomic, assign) BOOL disableKeyboardShortcuts; - - #if TARGET_OS_OSX // [macOS - @property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled; - @property (nonatomic, strong, nullable) RCTUIColor *selectionColor; - @property (nonatomic, strong, nullable) RCTUIColor *cursorColor; - @property (nonatomic, assign) UIEdgeInsets textContainerInsets; - @property (nonatomic, copy) NSString *text; - @property (nonatomic, assign) NSTextAlignment textAlignment; - @property (nonatomic, copy, nullable) NSAttributedString *attributedText; - @property (nonatomic, assign) CGFloat pointScaleFactor; - - (NSSize)sizeThatFits:(NSSize)size; - - (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes; - #endif // macOS] - - @property (nonatomic, getter=isGhostTextChanging) BOOL ghostTextChanging; // [macOS] - - @end - - NS_ASSUME_NONNULL_END - \ No newline at end of file +#import // [macOS] +#import // [macOS] + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/* + * Just regular UITextView... but much better! + */ +@interface RCTUITextView : UITextView + +- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer NS_UNAVAILABLE; +- (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE; + +@property (nonatomic, weak) id textInputDelegate; + +@property (nonatomic, assign) BOOL contextMenuHidden; +#if !TARGET_OS_OSX // [macOS] +@property (nonatomic, assign, readonly) BOOL textWasPasted; +#else // [macOS +@property (nonatomic, assign) BOOL textWasPasted; +#endif // macOS] +@property (nonatomic, assign, readonly) BOOL dictationRecognizing; +@property (nonatomic, copy, nullable) NSString *placeholder; +@property (nonatomic, strong, nullable) RCTUIColor *placeholderColor; // [macOS] + +@property (nonatomic, assign) CGFloat preferredMaxLayoutWidth; + +#if !TARGET_OS_OSX // [macOS] +// The `clearButtonMode` property actually is not supported yet; +// it's declared here only to conform to the interface. +@property (nonatomic, assign) UITextFieldViewMode clearButtonMode; +#endif // [macOS] + +@property (nonatomic, assign) BOOL caretHidden; + +@property (nonatomic, strong, nullable) NSString *inputAccessoryViewID; +@property (nonatomic, strong, nullable) NSString *inputAccessoryViewButtonLabel; + +@property (nonatomic, assign) BOOL disableKeyboardShortcuts; + +#if TARGET_OS_OSX // [macOS +@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled; +@property (nonatomic, strong, nullable) RCTUIColor *selectionColor; +@property (nonatomic, strong, nullable) RCTUIColor *cursorColor; +@property (nonatomic, assign) UIEdgeInsets textContainerInsets; +@property (nonatomic, copy) NSString *text; +@property (nonatomic, assign) NSTextAlignment textAlignment; +@property (nonatomic, copy, nullable) NSAttributedString *attributedText; +@property (nonatomic, assign) CGFloat pointScaleFactor; +- (NSSize)sizeThatFits:(NSSize)size; +- (void)setReadablePasteBoardTypes:(NSArray *)readablePasteboardTypes; +#endif // macOS] + +@property (nonatomic, getter=isGhostTextChanging) BOOL ghostTextChanging; // [macOS] + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm index 52a89ddcb681f7..ce79fbbf5998cc 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm @@ -13,6 +13,10 @@ #import #import +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] + @implementation RCTUITextView { #if !TARGET_OS_OSX // [macOS] UILabel *_placeholderView; @@ -130,6 +134,25 @@ - (NSString *)accessibilityLabel return accessibilityLabel; } +#pragma mark - Context menu + +#if TARGET_OS_OSX // [macOS +- (NSMenu *)menuForEvent:(NSEvent *)event +{ + NSMenu *menu = [super menuForEvent:event]; + if (menu) { + [[RCTTouchHandler touchHandlerForView:self] willShowMenuWithEvent:event]; + } + + RCTHideMenuItemsWithFilterPredicate(menu, ^bool(NSMenuItem *item) { + // hide font & layout orientation menu options + return (RCTMenuItemHasSubmenuItemWithAction(item, @selector(orderFrontFontPanel:)) || RCTMenuItemHasSubmenuItemWithAction(item, @selector(changeLayoutOrientation:))); + }); + + return menu; +} +#endif // macOS] + #pragma mark - Properties - (void)setPlaceholder:(NSString *)placeholder @@ -234,12 +257,8 @@ - (BOOL)becomeFirstResponder - (BOOL)resignFirstResponder { - if (self.selectable) { - self.selectedRange = NSMakeRange(NSNotFound, 0); - } - BOOL success = [super resignFirstResponder]; - + if (success) { // Break undo coalescing when losing focus. [self breakUndoCoalescing]; @@ -395,7 +414,7 @@ - (NSTouchBar *)makeTouchBar - (void)paste:(id)sender { #if TARGET_OS_OSX // [macOS - if ([self.textInputDelegate textInputShouldHandlePaste:self]) + if ([self.textInputDelegate textInputShouldHandlePaste:self]) { #endif // macOS] _textWasPasted = YES; @@ -429,19 +448,19 @@ - (NSAttributedString*)placeholderTextAttributedString - (void)drawRect:(NSRect)dirtyRect { [super drawRect:dirtyRect]; - + if (self.text.length == 0 && self.placeholder) { NSAttributedString *attributedPlaceholderString = self.placeholderTextAttributedString; - + if (attributedPlaceholderString) { NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedPlaceholderString]; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:self.textContainer.containerSize]; NSLayoutManager *layoutManager = [NSLayoutManager new]; - + textContainer.lineFragmentPadding = self.textContainer.lineFragmentPadding; [layoutManager addTextContainer:textContainer]; [textStorage addLayoutManager:layoutManager]; - + NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:self.textContainerOrigin]; } @@ -633,7 +652,7 @@ - (void)keyDown:(NSEvent *)event { } // textInputShouldHandleKeyEvent represents if native should handle the event instead of JS. - // textInputShouldHandleKeyEvent also sends keyDown event to JS internally, so we only call this once + // textInputShouldHandleKeyEvent also sends keyDown event to JS internally, so we only call this once if ([self.textInputDelegate textInputShouldHandleKeyEvent:event]) { [super keyDown:event]; [self.textInputDelegate submitOnKeyDownIfNeeded:event]; diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.mm new file mode 100644 index 00000000000000..ecb1064213f679 --- /dev/null +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.mm @@ -0,0 +1,204 @@ +/* + * 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. + */ + +#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 = (id) self; + + _forwardingTextView.verticallyResizable = YES; + _forwardingTextView.horizontallyResizable = YES; + _forwardingTextView.textContainer.containerSize = NSMakeSize(FLT_MAX, FLT_MAX); + _forwardingTextView.textContainer.widthTracksTextView = YES; + _forwardingTextView.textInputDelegate = (id) 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. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(scrollViewDidScroll:) + name:NSViewBoundsDidChangeNotification + object:_scrollView.contentView]; + } + + 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]; +} + +#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 + +- (void)scrollViewDidScroll:(NSNotification *)notification +{ + [self.textInputDelegate scrollViewDidScroll:_scrollView]; +} + +- (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 diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h index bf4b24c1bd99e7..675b25ff2ca25d 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h @@ -12,6 +12,10 @@ NS_ASSUME_NONNULL_BEGIN +@interface RCTBackedTextFieldDelegateAdapterUtility : NSObject ++ (BOOL)isShiftOrOptionKeyDown; +@end + #pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField) @protocol RCTBackedTextInputViewProtocol; // [macOS] diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm index 6aaa1905c0c13d..38cae8f1ab2e1e 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm @@ -14,6 +14,16 @@ static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingContext; +@implementation RCTBackedTextFieldDelegateAdapterUtility ++ (BOOL)isShiftOrOptionKeyDown +{ + NSEvent* event = [NSApp currentEvent]; + auto isShiftKeyDown = (event.modifierFlags & NSEventModifierFlagShift) == NSEventModifierFlagShift; + auto isOptionKeyDown = (event.modifierFlags & NSEventModifierFlagOption) == NSEventModifierFlagOption; + return isShiftKeyDown || isOptionKeyDown; +} +@end + @interface RCTBackedTextFieldDelegateAdapter () #if !TARGET_OS_OSX // [macOS] @@ -191,17 +201,38 @@ - (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor return [self textFieldShouldEndEditing:_backedTextInputView]; } +// This delegate method is almost idential to the NSTextView delegate in this same file. +// We are not combining them due to the side effects of each implementation. +// The commands they handle are the same and should continue to stay in sync. - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector { - id textInputDelegate = [_backedTextInputView textInputDelegate]; BOOL commandHandled = NO; + id textInputDelegate = [_backedTextInputView textInputDelegate]; // enter/return if (commandSelector == @selector(insertNewline:) || commandSelector == @selector(insertNewlineIgnoringFieldEditor:)) { + #if TARGET_OS_OSX // [macOS + if (![RCTBackedTextFieldDelegateAdapterUtility isShiftOrOptionKeyDown]) { + #endif // macOS] [self textFieldDidEndEditingOnExit]; - if ([textInputDelegate textInputShouldSubmitOnReturn]) { - [[_backedTextInputView window] makeFirstResponder:nil]; + if (textInputDelegate.textInputShouldReturn) { + [_backedTextInputView.window makeFirstResponder:nil]; + commandHandled = YES; } - commandHandled = YES; + #if TARGET_OS_OSX // [macOS + } + #endif // macOS] + // tab + } else if (commandSelector == @selector(insertTab:) ) { + // noop + // NSTextField does not use tab character. + // insertTab should select next view in key view loop which is default behavior + + // shift-tab + } else if (commandSelector == @selector(insertBacktab:)) { + // noop + // NSTextField does not use tab character. + // insertBacktab should select previous view in key view loop which is default behavior + //backspace } else if (commandSelector == @selector(deleteBackward:)) { if (textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteBackward:_backedTextInputView]) { @@ -211,7 +242,6 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doComman } //deleteForward } else if (commandSelector == @selector(deleteForward:)) { - id textInputDelegate = [_backedTextInputView textInputDelegate]; if (textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteForward:_backedTextInputView]) { commandHandled = YES; } else { @@ -219,7 +249,6 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doComman } //paste } else if (commandSelector == @selector(paste:)) { - id textInputDelegate = [_backedTextInputView textInputDelegate]; if (textInputDelegate != nil && ![textInputDelegate textInputShouldHandlePaste:_backedTextInputView]) { commandHandled = YES; } else { @@ -375,7 +404,7 @@ - (BOOL)textView:(__unused UITextView *)textView shouldChangeTextInRange:(NSRang - (void)textViewDidChange:(__unused UITextView *)textView { - if (_ignoreNextTextInputCall && [_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) { + if (_ignoreNextTextInputCall) { _ignoreNextTextInputCall = NO; return; } @@ -438,22 +467,50 @@ - (void)textDidEndEditing:(NSNotification *)notification [self textViewDidEndEditing:_backedTextInputView]; } +// This delegate method is almost idential to the NSTextField delegate in this same file. +// We are not combining them due to the side effects of each implementation. +// The commands they handle are the same and should continue to stay in sync. - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector { BOOL commandHandled = NO; id textInputDelegate = [_backedTextInputView textInputDelegate]; // enter/return - if ((commandSelector == @selector(insertNewline:) || commandSelector == @selector(insertNewlineIgnoringFieldEditor:))) { - if ([textInputDelegate textInputShouldSubmitOnReturn]) { - [_backedTextInputView.window makeFirstResponder:nil]; + if (commandSelector == @selector(insertNewline:) || commandSelector == @selector(insertNewlineIgnoringFieldEditor:)) { + #if TARGET_OS_OSX // [macOS + if (![RCTBackedTextFieldDelegateAdapterUtility isShiftOrOptionKeyDown]) { + #endif // macOS] + if (textInputDelegate.textInputShouldReturn) { + [_backedTextInputView.window makeFirstResponder:nil]; + } commandHandled = YES; + #if TARGET_OS_OSX // [macOS } + #endif // macOS] + // tab + } else if (commandSelector == @selector(insertTab:) ) { + [_backedTextInputView.window selectNextKeyView:nil]; + commandHandled = YES; + // shift-tab + } else if (commandSelector == @selector(insertBacktab:)) { + [_backedTextInputView.window selectPreviousKeyView:nil]; + commandHandled = YES; //backspace } else if (commandSelector == @selector(deleteBackward:)) { - commandHandled = textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteBackward:_backedTextInputView]; + if (textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteBackward:_backedTextInputView]) { + commandHandled = YES; + } //deleteForward } else if (commandSelector == @selector(deleteForward:)) { - commandHandled = textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteForward:_backedTextInputView]; + if (textInputDelegate != nil && ![textInputDelegate textInputShouldHandleDeleteForward:_backedTextInputView]) { + commandHandled = YES; + } + //paste + } else if (commandSelector == @selector(paste:)) { + if (textInputDelegate != nil && ![textInputDelegate textInputShouldHandlePaste:_backedTextInputView]) { + commandHandled = YES; + } else { + _backedTextInputView.textWasPasted = YES; + } //escape } else if (commandSelector == @selector(cancelOperation:)) { [textInputDelegate textInputDidCancel]; @@ -461,7 +518,6 @@ - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector [[_backedTextInputView window] makeFirstResponder:nil]; } commandHandled = YES; - } return commandHandled; diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm index 207b5d51986d79..3612663b3533a1 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm @@ -221,6 +221,13 @@ - (void)setAttributedText:(NSAttributedString *)attributedText textNeedsUpdate = ([self textOf:attributedTextCopy equals:backedTextInputViewTextCopy] == NO); +#if TARGET_OS_OSX // [macOS + // If we are in a language that uses conversion (e.g. Japanese), ignore updates if we have unconverted text. + if ([self.backedTextInputView hasMarkedText]) { + textNeedsUpdate = NO; + } +#endif // [macOS + if ((eventLag == 0 || self.backedTextInputView.ghostTextChanging) && textNeedsUpdate) { // [macOS] #if !TARGET_OS_OSX // [macOS] UITextRange *selection = self.backedTextInputView.selectedTextRange; @@ -229,6 +236,8 @@ - (void)setAttributedText:(NSAttributedString *)attributedText #endif // macOS] NSAttributedString *oldAttributedText = [self.backedTextInputView.attributedText copy]; NSInteger oldTextLength = oldAttributedText.string.length; + NSInteger oldSelectionStart = selection.location; // [macOS] + NSInteger oldSelectionEnd = selection.location + selection.length; // [macOS] // Ghost text changes should not be part of the undo stack if (!self.backedTextInputView.ghostTextChanging) { @@ -238,6 +247,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText [self.backedTextInputView.undoManager registerUndoWithTarget:self handler:^(RCTBaseTextInputView *strongSelf) { strongSelf.attributedText = oldAttributedTextWithoutGhostText; [strongSelf textInputDidChange]; + [strongSelf setSelectionStart:oldSelectionStart selectionEnd:oldSelectionEnd]; // [macOS] }]; } @@ -315,7 +325,7 @@ - (void)setSelection:(RCTTextSelection *)selection NSInteger length = end - selection.start; NSRange selectedTextRange = NSMakeRange(start, length); #endif // macOS] - + NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; if (eventLag == 0 && !RCTTextSelectionEqual(previousSelectedTextRange, selectedTextRange)) { // [macOS] [backedTextInputView setSelectedTextRange:selectedTextRange notifyDelegate:NO]; @@ -341,7 +351,7 @@ - (void)setSelectionStart:(NSInteger)start selectionEnd:(NSInteger)end #else // [macOS NSInteger startPosition = MIN(start, end); NSInteger endPosition = MAX(start, end); - [self.backedTextInputView setSelectedTextRange:NSMakeRange(startPosition, endPosition - startPosition) notifyDelegate:NO]; + [self.backedTextInputView setSelectedTextRange:NSMakeRange(startPosition, endPosition - startPosition) notifyDelegate:YES]; #endif // macOS] } @@ -605,7 +615,7 @@ - (void)submitOnKeyDownIfNeeded:(NSEvent *)event } } } - + if (shouldSubmit) { if (_onSubmitEditing) { _onSubmitEditing(@{}); @@ -706,7 +716,7 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range [backedTextInputView setSelectedTextRange:NSMakeRange(range.location + allowedLength, 0) notifyDelegate:YES]; #endif // macOS] - + [self textInputDidChange]; } @@ -761,17 +771,20 @@ - (void)textInputDidChangeSelection { self.ghostText = nil; // [macOS] - if (!_onSelectionChange || self.backedTextInputView.ghostTextChanging) { // [macOS] - return; - } + // Run this async to match iOS order of events where we get the onChange first and then onSelectionChange. + dispatch_async(dispatch_get_main_queue(), ^{ // [macOS] + if (!_onSelectionChange || self.backedTextInputView.ghostTextChanging) { + return; + } - RCTTextSelection *selection = self.selection; + RCTTextSelection *selection = self.selection; - _onSelectionChange(@{ - @"selection" : @{ - @"start" : @(selection.start), - @"end" : @(selection.end), - }, + _onSelectionChange(@{ + @"selection": @{ + @"start": @(selection.start), + @"end": @(selection.end), + }, + }); }); } @@ -834,7 +847,7 @@ - (BOOL)textInputShouldHandlePaste:(__unused id)sender NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; NSPasteboardType fileType = [pasteboard availableTypeFromArray:@[NSFilenamesPboardType, NSPasteboardTypePNG, NSPasteboardTypeTIFF]]; NSArray* pastedTypes = ((RCTUITextView*) self.backedTextInputView).readablePasteboardTypes; - + // If there's a fileType that is of interest, notify JS. Also blocks notifying JS if it's a text paste if (_onPaste && fileType != nil && [pastedTypes containsObject:fileType]) { _onPaste([self dataTransferInfoFromPasteboard:pasteboard]); diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm index dbfc8d69869d85..e1ae19d0c5f855 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm @@ -161,6 +161,10 @@ - (void)setBridge:(RCTBridge *)bridge if (eventLag != 0) { return; } + if (!value) { // [macOS] + [view setSelectionStart:start selectionEnd:end]; + return; + } RCTExecuteOnUIManagerQueue(^{ RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[self.bridge.uiManager shadowViewForReactTag:viewTag]; diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h index ff30d49d7f7f28..64d53b0086abc7 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h @@ -6,8 +6,7 @@ */ #import // [macOS] - -#import "RCTTextUIKit.h" // [macOS] +#import // [macOS] #import #import diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm index f1d87a07ea71b3..ac9d56a4e4f82d 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm @@ -13,9 +13,8 @@ #import #import -#import // [macOS] - #if TARGET_OS_OSX // [macOS +#import // [macOS] #if RCT_SUBCLASS_SECURETEXTFIELD #define RCTUITextFieldCell RCTUISecureTextFieldCell @@ -576,6 +575,11 @@ - (NSMenu *)textView:(NSTextView *)view menu:(NSMenu *)menu forEvent:(NSEvent *) [[RCTTouchHandler touchHandlerForView:self] willShowMenuWithEvent:event]; } + RCTHideMenuItemsWithFilterPredicate(menu, ^bool(NSMenuItem *item) { + // hide font menu option + return RCTMenuItemHasSubmenuItemWithAction(item, @selector(orderFrontFontPanel:)); + }); + return menu; } diff --git a/packages/react-native/Libraries/Text/TextNativeComponent.js b/packages/react-native/Libraries/Text/TextNativeComponent.js index 72e60ccf80f35e..f502c085126953 100644 --- a/packages/react-native/Libraries/Text/TextNativeComponent.js +++ b/packages/react-native/Libraries/Text/TextNativeComponent.js @@ -48,7 +48,9 @@ const textViewConfig = { dataDetectorType: true, android_hyphenationFrequency: true, lineBreakStrategyIOS: true, + focusable: true, // [macOS] tooltip: true, // [macOS] + href: true, // [macOS] }, directEventTypes: { topTextLayout: { diff --git a/packages/react-native/Libraries/Types/CoreEventTypes.js b/packages/react-native/Libraries/Types/CoreEventTypes.js index 30609b5c9d3259..9d61b32ebfc61b 100644 --- a/packages/react-native/Libraries/Types/CoreEventTypes.js +++ b/packages/react-native/Libraries/Types/CoreEventTypes.js @@ -364,8 +364,9 @@ export type DragEvent = NativeSyntheticEvent< }>, >; +// [macOS export type KeyEvent = NativeSyntheticEvent< - $ReadOnly<{| + $ReadOnly<{ // Modifier keys capsLockKey: boolean, shiftKey: boolean, @@ -381,7 +382,7 @@ export type KeyEvent = NativeSyntheticEvent< ArrowUp: boolean, ArrowDown: boolean, key: string, - |}>, + }>, >; /** @@ -395,11 +396,11 @@ export type KeyEvent = NativeSyntheticEvent< * * @platform macos */ -export type HandledKeyEvent = $ReadOnly<{| +export type HandledKeyEvent = $ReadOnly<{ ctrlKey?: ?boolean, altKey?: ?boolean, shiftKey?: ?boolean, metaKey?: ?boolean, key: string, -|}>; +}>; // macOS] diff --git a/packages/react-native/Libraries/Utilities/Platform.macos.js b/packages/react-native/Libraries/Utilities/Platform.macos.js index 902bf5c115b248..2f9694acf932af 100644 --- a/packages/react-native/Libraries/Utilities/Platform.macos.js +++ b/packages/react-native/Libraries/Utilities/Platform.macos.js @@ -26,21 +26,21 @@ const Platform: PlatformType = { return this.constants.osVersion; }, // $FlowFixMe[unsafe-getters-setters] - get constants(): {| + get constants(): { forceTouchAvailable: boolean, interfaceIdiom: string, isTesting: boolean, isDisableAnimations?: boolean, osVersion: string, - reactNativeVersion: {| + reactNativeVersion: { major: number, minor: number, patch: number, - prerelease: ?number, - |}, + prerelease: ?string, + }, systemName: string, isMacCatalyst?: boolean, - |} { + } { // $FlowFixMe[object-this-reference] if (this.__constants == null) { // $FlowFixMe[object-this-reference] @@ -84,11 +84,7 @@ const Platform: PlatformType = { }, select: (spec: PlatformSelectSpec): T => // $FlowFixMe[incompatible-return] - 'macos' in spec - ? spec.macos - : 'native' in spec - ? spec.native - : spec.default, + 'macos' in spec ? spec.macos : 'native' in spec ? spec.native : spec.default, }; export default Platform; diff --git a/packages/react-native/React/Base/RCTRootView.m b/packages/react-native/React/Base/RCTRootView.m index 9ac9afb245b84b..89dd5276873977 100644 --- a/packages/react-native/React/Base/RCTRootView.m +++ b/packages/react-native/React/Base/RCTRootView.m @@ -204,6 +204,13 @@ - (BOOL)canBecomeFirstResponder #endif // macOS] } +#if TARGET_OS_OSX // [macOS +- (void)viewDidEndLiveResize { + [super viewDidEndLiveResize]; + [self setNeedsLayout]; +} +#endif // macOS] + - (void)setLoadingView:(RCTUIView *)loadingView // [macOS] { _loadingView = loadingView; diff --git a/packages/react-native/React/Base/RCTTouchHandler.h b/packages/react-native/React/Base/RCTTouchHandler.h index 93c4e8c3deb469..331d9a53ace684 100644 --- a/packages/react-native/React/Base/RCTTouchHandler.h +++ b/packages/react-native/React/Base/RCTTouchHandler.h @@ -9,14 +9,20 @@ #import +#if TARGET_OS_OSX // [macOS +static NSString *const RCTTouchHandlerOutsideViewMouseUpNotification = @"RCTTouchHandlerOutsideViewMouseUpNotification"; +#endif // macOS] + @class RCTBridge; @interface RCTTouchHandler : UIGestureRecognizer +@property (class, nonatomic, assign) BOOL notifyOutsideViewEvents; // [macOS] - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; - (void)attachToView:(RCTUIView *)view; // [macOS] - (void)detachFromView:(RCTUIView *)view; // [macOS] ++ (void)notifyOutsideViewMouseUp:(NSEvent *) event; // [macOS] - (void)cancel; @@ -26,6 +32,7 @@ - (void)willShowMenuWithEvent:(NSEvent *)event; - (void)cancelTouchWithEvent:(NSEvent *)event; +- (void)willShowMenu; #endif // macOS] @end diff --git a/packages/react-native/React/Base/RCTTouchHandler.m b/packages/react-native/React/Base/RCTTouchHandler.m index da95ebd4ae7e77..07583af742b892 100644 --- a/packages/react-native/React/Base/RCTTouchHandler.m +++ b/packages/react-native/React/Base/RCTTouchHandler.m @@ -11,6 +11,8 @@ #import #endif // [macOS] #import // [macOS] +#import // [macOS] + #import "RCTAssert.h" #import "RCTBridge.h" @@ -22,6 +24,54 @@ #import "RCTUtils.h" #import "UIView+React.h" +#if TARGET_OS_OSX // [macOS +@interface NSApplication (RCTTouchHandlerOverride) +- (NSEvent*)override_nextEventMatchingMask:(NSEventMask)mask + untilDate:(NSDate*)expiration + inMode:(NSRunLoopMode)mode + dequeue:(BOOL)dequeue; +@end + +@implementation NSApplication (RCTTouchHandlerOverride) + ++ (void)load +{ + RCTSwapInstanceMethods(self, @selector(nextEventMatchingMask:untilDate:inMode:dequeue:), @selector(override_nextEventMatchingMask:untilDate:inMode:dequeue:)); +} + +- (NSEvent*)override_nextEventMatchingMask:(NSEventMask)mask + untilDate:(NSDate*)expiration + inMode:(NSRunLoopMode)mode + dequeue:(BOOL)dequeue +{ + NSEvent* event = [self override_nextEventMatchingMask:mask + untilDate:expiration + inMode:mode + dequeue:dequeue]; + if (dequeue && (event.type == NSEventTypeLeftMouseUp || event.type == NSEventTypeRightMouseUp || event.type == NSEventTypeOtherMouseUp)) { + RCTTouchHandler *targetTouchHandler = [RCTTouchHandler touchHandlerForEvent:event]; + if (!targetTouchHandler) { + [RCTTouchHandler notifyOutsideViewMouseUp:event]; + } else if ([mode isEqualTo:NSEventTrackingRunLoopMode]) { + // A tracking loop will deque an event, thereby not submitting it to the touch handler. + if (event.type == NSEventTypeLeftMouseUp) { + // NSTextField uses a tracking loop when clicking inside the view bounds. If a view + // is located above the NSTextField, the mouseUp won't reach the view and break the + // pressability. This submits the mouse up event on the next run loop to let it go + // through the touch handler. + dispatch_async(dispatch_get_main_queue (), ^{ + [targetTouchHandler mouseUp:event]; + }); + } + } + } + + return event; +} + +@end +#endif // macOS] + @interface RCTTouchHandler () @end @@ -40,12 +90,24 @@ @implementation RCTTouchHandler { NSMutableArray *_reactTouches; NSMutableArray *_touchViews; // [macOS] +#if TARGET_OS_OSX // TODO(macOS ISS#2323203) + NSEvent* _lastRightMouseDown; + NSEvent* _lastEvent; +#endif + __weak RCTPlatformView *_cachedRootView; // [macOS] uint16_t _coalescingKey; -#if TARGET_OS_OSX// [macOS - BOOL _shouldSendMouseUpOnSystemBehalf; -#endif// macOS] +} + +static BOOL _notifyOutsideViewEvents = NO; + ++ (BOOL)notifyOutsideViewEvents { + return _notifyOutsideViewEvents; +} + ++ (void)setNotifyOutsideViewEvents:(BOOL)newNotifyOutsideViewEvents { + _notifyOutsideViewEvents = newNotifyOutsideViewEvents; } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -70,6 +132,10 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge self.delaysPrimaryMouseButtonEvents = NO; // default is NO. self.delaysSecondaryMouseButtonEvents = NO; // default is NO. self.delaysOtherMouseButtonEvents = NO; // default is NO. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(endOutsideViewMouseUp:) + name:RCTTouchHandlerOutsideViewMouseUpNotification + object:[RCTTouchHandler class]]; #endif // macOS] self.delegate = self; @@ -83,6 +149,10 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)coder) #endif // macOS] +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + - (void)attachToView:(RCTUIView *)view // [macOS] { RCTAssert(self.view == nil, @"RCTTouchHandler already has attached view."); @@ -109,9 +179,28 @@ - (void)_recordNewTouches:(NSSet *)touches #endif // macOS] RCTAssert(![_nativeTouches containsObject:touch], @"Touch is already recorded. This is a critical bug."); +#if TARGET_OS_OSX // [macOS] + // We're starting a new interaction while there is an unterminated RightMouseDown touch. This can + // happen for example after a right click on secure text fields when not the RightMouseUp nor + // willShowMenu event can be intercepted + // (see https://github.com/microsoft/react-native-macos/issues/1209). + + // This means the state machine in Pressability.js on JS side is in a stuck state. Best we can do + // to get it unstuck is to send touch cancellation. + if (_lastRightMouseDown != NULL && [_nativeTouches containsObject:_lastRightMouseDown]) { + if (![RCTTouchHandler notifyOutsideViewEvents]) { + [self cancelTouchWithEvent:_lastRightMouseDown]; + } + _lastRightMouseDown = NULL; + } + // Keep track of any active RightMouseDown touches. We reset it to NULL if interaction ends correctly + if (touch.type == NSEventTypeRightMouseDown) { + _lastRightMouseDown = touch; + } +#endif // Find closest React-managed touchable view - + #if !TARGET_OS_OSX // [macOS] UIView *targetView = touch.view; while (targetView) { @@ -135,20 +224,7 @@ - (void)_recordNewTouches:(NSSet *)touches if ([targetView isKindOfClass:[NSScroller class]]) { continue; } - // Pair the mouse down events with mouse up events so our _nativeTouches cache doesn't get stale - if ([targetView isKindOfClass:[NSControl class]]) { - _shouldSendMouseUpOnSystemBehalf = [(NSControl*)targetView isEnabled]; - } else if ([targetView isKindOfClass:[NSTabView class]]) { - // NSTabView sends click events for tab buttons but doesn't inherit from NSControl - _shouldSendMouseUpOnSystemBehalf = YES; - } else if ([targetView isKindOfClass:[NSText class]]) { - _shouldSendMouseUpOnSystemBehalf = [(NSText*)targetView isSelectable]; - } - else if ([targetView.superview isKindOfClass:[RCTUITextField class]]) { - _shouldSendMouseUpOnSystemBehalf = [(RCTUITextField*)targetView.superview isSelectable]; - } else { - _shouldSendMouseUpOnSystemBehalf = NO; - } + touchLocation = [targetView convertPoint:touchLocation fromView:self.view.superview]; while (targetView) { BOOL isUserInteractionEnabled = NO; @@ -212,6 +288,11 @@ - (void)_recordRemovedTouches:(NSSet *)touches if (index == NSNotFound) { continue; } +#if TARGET_OS_OSX + if (_lastRightMouseDown != NULL && _lastRightMouseDown.eventNumber == touch.eventNumber) { + _lastRightMouseDown = NULL; + } +#endif [_touchViews removeObjectAtIndex:index]; [_nativeTouches removeObjectAtIndex:index]; @@ -270,7 +351,7 @@ - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex if (modifierFlags & NSEventModifierFlagCommand) { reactTouch[@"metaKey"] = @YES; } - + NSEventType type = nativeTouch.type; if (type == NSEventTypeLeftMouseDown || type == NSEventTypeLeftMouseUp || type == NSEventTypeLeftMouseDragged) { reactTouch[@"button"] = @0; @@ -308,11 +389,11 @@ - (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSStrin return touch.eventNumber == event.eventNumber; }]; #endif // macOS] - + if (index == NSNotFound) { continue; } - + #if TARGET_OS_OSX // [macOS _nativeTouches[index] = touch; #endif // macOS] @@ -457,10 +538,10 @@ - (void)interactionsCancelled:(NSSet *)touches withEvent:(UIEvent*)event // [mac #else // [macOS self.state = UIGestureRecognizerStateCancelled; #endif // macOS] - + [self _recordRemovedTouches:touches]; } - + #if !TARGET_OS_OSX // [macOS] - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { @@ -487,6 +568,16 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event } #else // [macOS +- (BOOL)isDuplicateEvent:(NSEvent *)event +{ + if (_lastEvent && (event == _lastEvent || (event.eventNumber == _lastEvent.eventNumber && event.type == _lastEvent.type && NSEqualPoints(event.locationInWindow, _lastEvent.locationInWindow )))) { + return YES; + } + + _lastEvent = event; + return NO; +} + - (BOOL)acceptsFirstMouse:(NSEvent *)event { // This will only be called if the hit-tested view returns YES for acceptsFirstMouse, @@ -496,56 +587,64 @@ - (BOOL)acceptsFirstMouse:(NSEvent *)event - (void)mouseDown:(NSEvent *)event { + if ([self isDuplicateEvent:event]) { + return; + } + [super mouseDown:event]; [self interactionsBegan:[NSSet setWithObject:event]]; - // [macOS - if (_shouldSendMouseUpOnSystemBehalf) { - _shouldSendMouseUpOnSystemBehalf = NO; - - NSEvent *newEvent = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp - location:[event locationInWindow] - modifierFlags:[event modifierFlags] - timestamp:[event timestamp] - windowNumber:[event windowNumber] - context:nil - eventNumber:[event eventNumber] - clickCount:[event clickCount] - pressure:[event pressure]]; - [self interactionsEnded:[NSSet setWithObject:newEvent] withEvent:newEvent]; - // macOS] - } -} - +} + - (void)rightMouseDown:(NSEvent *)event { + if ([self isDuplicateEvent:event]) { + return; + } + [super rightMouseDown:event]; [self interactionsBegan:[NSSet setWithObject:event]]; } - + - (void)mouseDragged:(NSEvent *)event { + if ([self isDuplicateEvent:event]) { + return; + } + [super mouseDragged:event]; [self interactionsMoved:[NSSet setWithObject:event]]; } - + - (void)rightMouseDragged:(NSEvent *)event { + if ([self isDuplicateEvent:event]) { + return; + } + [super rightMouseDragged:event]; [self interactionsMoved:[NSSet setWithObject:event]]; } - (void)mouseUp:(NSEvent *)event { + if ([self isDuplicateEvent:event]) { + return; + } + [super mouseUp:event]; [self interactionsEnded:[NSSet setWithObject:event] withEvent:event]; } - + - (void)rightMouseUp:(NSEvent *)event { + if ([self isDuplicateEvent:event]) { + return; + } + [super rightMouseUp:event]; [self interactionsEnded:[NSSet setWithObject:event] withEvent:event]; } - + #endif // macOS] - (BOOL)canPreventGestureRecognizer:(__unused UIGestureRecognizer *)preventedGestureRecognizer @@ -557,7 +656,7 @@ - (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestu { // We fail in favour of other external gesture recognizers. // iOS will ask `delegate`'s opinion about this gesture recognizer little bit later. - return !RCTUIViewIsDescendantOfView(preventingGestureRecognizer.view, self.view); // macOS + return !RCTUIViewIsDescendantOfView(preventingGestureRecognizer.view, self.view); // macOS } - (void)reset @@ -607,13 +706,70 @@ + (instancetype)touchHandlerForView:(NSView *)view { return nil; } ++ (void)notifyOutsideViewMouseUp:(NSEvent *) event { + if (![RCTTouchHandler notifyOutsideViewEvents]) { + return; + } + [[NSNotificationCenter defaultCenter] postNotificationName:RCTTouchHandlerOutsideViewMouseUpNotification + object:self + userInfo:@{@"event": event}]; +} + +- (void)endOutsideViewMouseUp:(NSNotification *)notification { + NSEvent *event = notification.userInfo[@"event"]; + + NSInteger index = [_nativeTouches indexOfObjectPassingTest:^BOOL(NSEvent *touch, __unused NSUInteger idx, __unused BOOL *stop) { + return touch.eventNumber == event.eventNumber; + }]; + if (index == NSNotFound) { + // A contextual menu click would generate a mouse up with a diffrent event + // and leave a touchable/pressable session open. This would cause touch end + // events from a modal window to end the touchable/pressable session and + // potentially trigger an onPress event. Hence the need to reset and cancel + // that session when a mouse up event was detected outside the touch handler + // view bounds. + [self reset]; + return; + } + + if ([self isDuplicateEvent:event]) { + return; + } + + [self interactionsEnded:[NSSet setWithObject:event] withEvent:event]; +} + +// Showing a context menu via RightMouseDown prevents receiving RightMouseUp event +// and propagating touchEnd event to JS side, leaving the Responder state machine +// on JS side (in Pressabity.js) in an intermediate state, that will not be able to +// process the next interaction correctly. + +// To avoid this, we end the interaction proactively on RightMouseDown if we know it +// triggers a context menu. + +// (Note this is not an issue for left clicks: context menu on left clicks is only shown +// on LeftMouseUp) - (void)willShowMenuWithEvent:(NSEvent *)event { + if ([RCTTouchHandler notifyOutsideViewEvents]) { + return; + } + if (event.type == NSEventTypeRightMouseDown) { [self interactionsEnded:[NSSet setWithObject:event] withEvent:event]; } } - + +- (void)willShowMenu +{ + for (NSEvent* event in _nativeTouches) { + if (event.type == NSEventTypeRightMouseDown) { + [self willShowMenuWithEvent:event]; + break; + } + } +} + - (void)cancelTouchWithEvent:(NSEvent *)event { [self interactionsCancelled:[NSSet setWithObject:event] withEvent:event]; diff --git a/packages/react-native/React/Base/RCTUIKit.h b/packages/react-native/React/Base/RCTUIKit.h index 9df93b735199c8..7589db8a2197a4 100644 --- a/packages/react-native/React/Base/RCTUIKit.h +++ b/packages/react-native/React/Base/RCTUIKit.h @@ -635,8 +635,18 @@ typedef void (^RCTUIGraphicsImageDrawingActions)(RCTUIGraphicsImageRendererConte - (instancetype)initWithSize:(CGSize)size; - (instancetype)initWithSize:(CGSize)size format:(RCTUIGraphicsImageRendererFormat *)format; -- (NSImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions; - +- (NSImage *)imageWithActions:(RCTUIGraphicsImageDrawingActions)actions; +@property (nonatomic, copy) RCTUIGraphicsImageDrawingActions actions; @end NS_ASSUME_NONNULL_END #endif + +#if TARGET_OS_OSX // [macOS +// TextViews implementing this protocol can extend the native text menu +@protocol ExtensibleNativeMenuProtocol + +@required +@property (nonatomic, strong) NSArray * _Nullable additionalMenuItems; + +@end +#endif // macOS] diff --git a/packages/react-native/React/Base/RCTUtils.h b/packages/react-native/React/Base/RCTUtils.h index 276fe1dc4b9ddb..a4517eb5904deb 100644 --- a/packages/react-native/React/Base/RCTUtils.h +++ b/packages/react-native/React/Base/RCTUtils.h @@ -209,4 +209,10 @@ RCT_EXTERN BOOL RCTValidateTypeOfViewCommandArgument( RCT_EXTERN BOOL RCTIsAppActive(void); +#if TARGET_OS_OSX // [macOS +typedef bool (^RCTMenuItemFilterPredicate)(NSMenuItem *_Nonnull item); +RCT_EXTERN void RCTHideMenuItemsWithFilterPredicate(NSMenu *_Nonnull menu, RCTMenuItemFilterPredicate shouldFilter); +RCT_EXTERN BOOL RCTMenuItemHasSubmenuItemWithAction(NSMenuItem *_Nonnull item, SEL action); +#endif // macOS] + NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Base/Surface/RCTSurfaceRootShadowView.h b/packages/react-native/React/Base/Surface/RCTSurfaceRootShadowView.h index 4e0ed8458d2ce1..3ef1166cf03bb9 100644 --- a/packages/react-native/React/Base/Surface/RCTSurfaceRootShadowView.h +++ b/packages/react-native/React/Base/Surface/RCTSurfaceRootShadowView.h @@ -11,7 +11,7 @@ @interface RCTSurfaceRootShadowView : RCTShadowView -@property (nonatomic, assign, readonly) CGSize minimumSize; +@property (nonatomic, assign, readwrite) CGSize minimumSize; @property (nonatomic, assign, readonly) CGSize maximumSize; - (void)setMinimumSize:(CGSize)size maximumSize:(CGSize)maximumSize; diff --git a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h index c61fa33383924b..5138389b096c8b 100644 --- a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h +++ b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.h @@ -38,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) NSTimeInterval loadingViewFadeDelay; @property (nonatomic, assign) NSTimeInterval loadingViewFadeDuration; @property (nonatomic, assign) CGSize minimumSize; +@property (nonatomic, assign) CGSize intrinsicContentSize; // [macOS] - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; diff --git a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm index 44d8390e2243f6..82f6c3e322188e 100644 --- a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm +++ b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm @@ -43,8 +43,10 @@ - (instancetype)initWithSurface:(id)surface _stage = surface.stage; [self _updateViews]; +#if !TARGET_OS_OSX // [macOS] // For backward compatibility with RCTRootView, set a color here instead of transparent (OS default). self.backgroundColor = [RCTUIColor whiteColor]; // [macOS] +#endif // [macOS] } return self; @@ -139,6 +141,15 @@ - (void)disableActivityIndicatorAutoHide:(BOOL)disabled _autoHideDisabled = disabled; } +#pragma mark - NSView + +#if TARGET_OS_OSX // [macOS +- (void)viewDidEndLiveResize { + [super viewDidEndLiveResize]; + [self setNeedsLayout]; +} +#endif // macOS] + #pragma mark - isActivityIndicatorViewVisible - (void)setIsActivityIndicatorViewVisible:(BOOL)visible diff --git a/packages/react-native/React/Base/macOS/RCTUIKit.m b/packages/react-native/React/Base/macOS/RCTUIKit.m index 08e6c95545ac6d..02af4253728e38 100644 --- a/packages/react-native/React/Base/macOS/RCTUIKit.m +++ b/packages/react-native/React/Base/macOS/RCTUIKit.m @@ -31,26 +31,6 @@ CGContextRef UIGraphicsGetCurrentContext(void) return [[NSGraphicsContext currentContext] CGContext]; } -NSImage *UIGraphicsGetImageFromCurrentImageContext(void) -{ - NSImage *image = nil; - NSGraphicsContext *graphicsContext = [NSGraphicsContext currentContext]; - - NSValue *sizeValue = objc_getAssociatedObject(graphicsContext, &RCTGraphicsContextSizeKey); - if (sizeValue != nil) { - CGImageRef cgImage = CGBitmapContextCreateImage([graphicsContext CGContext]); - - if (cgImage != NULL) { - NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage]; - image = [[NSImage alloc] initWithSize:[sizeValue sizeValue]]; - [image addRepresentation:imageRep]; - CFRelease(cgImage); - } - } - - return image; -} - // // functionally equivalent types // @@ -62,7 +42,7 @@ CGFloat UIImageGetScale(NSImage *image) if (image == nil) { return 0.0; } - + RCTAssert(image.representations.count == 1, @"The scale can only be derived if the image has one representation."); NSImageRep *imageRep = image.representations.firstObject; @@ -273,7 +253,11 @@ - (void)drawRect:(CGRect)rect { if (_backgroundColor) { [_backgroundColor set]; - NSRectFill(rect); + // On macOS 14, views inside NSClipView can get a dirty rect that stretches + // outside the bounds of the view probably because of the changed behavior + // of visibleRect. To avoid weird background filling we clip to the bounds. + NSRect clippedToBoundsRect = NSIntersectionRect(self.bounds, rect); + NSRectFill(clippedToBoundsRect); } [super drawRect:rect]; } @@ -381,7 +365,7 @@ - (instancetype)initWithFrame:(CGRect)frame self.scrollEnabled = YES; self.drawsBackground = NO; } - + return self; } @@ -548,7 +532,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect self.constrainScrolling = NO; self.drawsBackground = NO; } - + return self; } @@ -557,7 +541,7 @@ - (NSRect)constrainBoundsRect:(NSRect)proposedBounds if (self.constrainScrolling) { return NSMakeRect(0, 0, 0, 0); } - + return [super constrainBoundsRect:proposedBounds]; } @@ -588,7 +572,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect [self setSelectable:NO]; [self setWantsLayer:YES]; } - + return self; } @@ -662,7 +646,7 @@ - (void)stopAnimation:(id)sender - (void)setActivityIndicatorViewStyle:(UIActivityIndicatorViewStyle)activityIndicatorViewStyle { _activityIndicatorViewStyle = activityIndicatorViewStyle; - + switch (activityIndicatorViewStyle) { case UIActivityIndicatorViewStyleLarge: self.controlSize = NSControlSizeLarge; @@ -692,14 +676,14 @@ - (void)updateLayer CIFilter *colorPoly = [CIFilter filterWithName:@"CIColorPolynomial"]; [colorPoly setDefaults]; - + CIVector *redVector = [CIVector vectorWithX:r Y:0 Z:0 W:0]; CIVector *greenVector = [CIVector vectorWithX:g Y:0 Z:0 W:0]; CIVector *blueVector = [CIVector vectorWithX:b Y:0 Z:0 W:0]; [colorPoly setValue:redVector forKey:@"inputRedCoefficients"]; [colorPoly setValue:greenVector forKey:@"inputGreenCoefficients"]; [colorPoly setValue:blueVector forKey:@"inputBlueCoefficients"]; - + [[self layer] setFilters:@[colorPoly]]; } else { [[self layer] setFilters:nil]; @@ -730,6 +714,7 @@ - (void)setHidden:(BOOL)hidden // RCTUIImageView @implementation RCTUIImageView { + UIImage *_image; CALayer *_tintingLayer; } @@ -739,7 +724,7 @@ - (instancetype)initWithFrame:(CGRect)frame [self setLayer:[[CALayer alloc] init]]; [self setWantsLayer:YES]; } - + return self; } @@ -753,65 +738,79 @@ - (void)setClipsToBounds:(BOOL)clipsToBounds [[self layer] setMasksToBounds:clipsToBounds]; } +- (UIImage *)image +{ + return _image; +} + +- (void)setImage:(UIImage *)image +{ + _image = image; + [self updateLayer]; +} + - (void)setContentMode:(UIViewContentMode)contentMode { _contentMode = contentMode; - + CALayer *layer = [self layer]; switch (contentMode) { case UIViewContentModeScaleAspectFill: [layer setContentsGravity:kCAGravityResizeAspectFill]; break; - + case UIViewContentModeScaleAspectFit: [layer setContentsGravity:kCAGravityResizeAspect]; break; - + case UIViewContentModeScaleToFill: [layer setContentsGravity:kCAGravityResize]; break; - + case UIViewContentModeCenter: [layer setContentsGravity:kCAGravityCenter]; break; - + default: break; } + + [self updateLayer]; } -- (UIImage *)image +- (void)setTintColor:(RCTUIColor *)tintColor { - return [[self layer] contents]; + _tintColor = tintColor; + [self updateLayer]; } -- (void)setImage:(UIImage *)image +- (void)updateLayer { CALayer *layer = [self layer]; - - if ([layer contents] != image || [layer backgroundColor] != nil) { - if (_tintColor) { - if (!_tintingLayer) { - _tintingLayer = [CALayer new]; - [_tintingLayer setFrame:self.bounds]; - [_tintingLayer setAutoresizingMask:kCALayerWidthSizable | kCALayerHeightSizable]; - [_tintingLayer setZPosition:1.0]; - CIFilter *sourceInCompositingFilter = [CIFilter filterWithName:@"CISourceInCompositing"]; - [sourceInCompositingFilter setDefaults]; - [_tintingLayer setCompositingFilter:sourceInCompositingFilter]; - [layer addSublayer:_tintingLayer]; - } - [_tintingLayer setBackgroundColor:_tintColor.CGColor]; - } else { - [_tintingLayer removeFromSuperlayer]; - _tintingLayer = nil; + + if (_tintColor) { + if (!_tintingLayer) { + _tintingLayer = [CALayer new]; + [_tintingLayer setFrame:self.bounds]; + [_tintingLayer setAutoresizingMask:kCALayerWidthSizable | kCALayerHeightSizable]; + [_tintingLayer setZPosition:1.0]; + CIFilter *sourceInCompositingFilter = [CIFilter filterWithName:@"CISourceInCompositing"]; + [sourceInCompositingFilter setDefaults]; + [_tintingLayer setCompositingFilter:sourceInCompositingFilter]; + [layer addSublayer:_tintingLayer]; } - - if (image != nil && [image resizingMode] == NSImageResizingModeTile) { + [_tintingLayer setBackgroundColor:_tintColor.CGColor]; + } else { + [_tintingLayer removeFromSuperlayer]; + _tintingLayer = nil; + } + + if ([layer contents] != _image || [layer backgroundColor] != nil) { + if (_image != nil && [_image resizingMode] == NSImageResizingModeTile) { [layer setContents:nil]; - [layer setBackgroundColor:[NSColor colorWithPatternImage:image].CGColor]; + [layer setBackgroundColor:[NSColor colorWithPatternImage:_image].CGColor]; } else { - [layer setContents:image]; + [layer setContents:_image]; [layer setBackgroundColor:nil]; } } @@ -849,12 +848,12 @@ - (nonnull instancetype)initWithSize:(CGSize)size format:(nonnull RCTUIGraphicsI return self; } -- (nonnull NSImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions { +- (nonnull NSImage *)imageWithActions:(RCTUIGraphicsImageDrawingActions)actions { NSImage *image = [NSImage imageWithSize:_size flipped:YES drawingHandler:^BOOL(NSRect dstRect) { - + RCTUIGraphicsImageRendererContext *context = [NSGraphicsContext currentContext]; if (self->_format.opaque) { CGContextSetAlpha([context CGContext], 1.0); diff --git a/packages/react-native/React/CoreModules/RCTActionSheetManager.mm b/packages/react-native/React/CoreModules/RCTActionSheetManager.mm index 3177b768543e10..40aa98bcf8698c 100644 --- a/packages/react-native/React/CoreModules/RCTActionSheetManager.mm +++ b/packages/react-native/React/CoreModules/RCTActionSheetManager.mm @@ -37,7 +37,7 @@ @implementation RCTActionSheetManager { /* Unlike UIAlertAction (which takes a block for it's action), NSMenuItem takes a selector. * That selector no longer has has access to the method argument `callback`, so we must save it - * as an instance variable, that we can access in `menuItemDidTap`. We must do this as well for + * as an instance variable, that we can access in `menuItemDidTap`. We must do this as well for * `failureCallback` and `successCallback`. */ NSMapTable *_callbacks; @@ -145,7 +145,9 @@ - (void)presentSharingServicePicker:(NSSharingServicePicker *)picker NSArray *disabledButtonIndices; NSInteger cancelButtonIndex = options.cancelButtonIndex() ? [RCTConvert NSInteger:@(*options.cancelButtonIndex())] : -1; +#if !TARGET_OS_OSX // [macOS] Unused on macOS NSArray *destructiveButtonIndices; +#endif // [macOS] if (options.disabledButtonIndices()) { disabledButtonIndices = RCTConvertVecToArray(*options.disabledButtonIndices(), ^id(double element) { return @(element); @@ -337,9 +339,13 @@ - (void)presentSharingServicePicker:(NSSharingServicePicker *)picker RCTConvertOptionalVecToArray(options.excludedActivityTypes(), ^id(NSString *element) { return element; }); +#if !TARGET_OS_OSX // [macOS] NSString *userInterfaceStyle = [RCTConvert NSString:options.userInterfaceStyle()]; +#endif // [macOS] NSNumber *anchorViewTag = [RCTConvert NSNumber:options.anchor() ? @(*options.anchor()) : nil]; +#if !TARGET_OS_OSX // [macOS] RCTUIColor *tintColor = [RCTConvert RCTUIColor:options.tintColor() ? @(*options.tintColor()) : nil]; // [macOS] +#endif // [macOS] dispatch_async(dispatch_get_main_queue(), ^{ if (message) { @@ -451,7 +457,7 @@ - (void)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker didC service.subject = _sharingSubject; } } - + - (void)sharingService:(NSSharingService *)sharingService didFailToShareItems:(NSArray *)items error:(NSError *)error { _failureCallback(@[RCTJSErrorFromNSError(error)]); @@ -469,7 +475,7 @@ - (void)sharingService:(NSSharingService *)sharingService didShareItems:(NSArray NSString *activityType = [sharingService.description substringWithRange:range]; _successCallback(@[@YES, RCTNullIfNil(activityType)]); } - + - (NSArray *)sharingServicePicker:(__unused NSSharingServicePicker *)sharingServicePicker sharingServicesForItems:(__unused NSArray *)items proposedSharingServices:(NSArray *)proposedServices { return [proposedServices filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSSharingService *service, __unused NSDictionary * _Nullable bindings) { @@ -477,7 +483,7 @@ - (void)sharingService:(NSSharingService *)sharingService didShareItems:(NSArray }]]; } #endif // macOS] - + - (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params { return std::make_shared(params); diff --git a/packages/react-native/React/CoreModules/RCTDevMenu.h b/packages/react-native/React/CoreModules/RCTDevMenu.h index 048438ff69ac03..9ab596f2dbe6f6 100644 --- a/packages/react-native/React/CoreModules/RCTDevMenu.h +++ b/packages/react-native/React/CoreModules/RCTDevMenu.h @@ -12,6 +12,10 @@ #import #import +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] + #if RCT_DEV_MENU RCT_EXTERN NSString *const RCTShowDevMenuNotification; @@ -45,6 +49,14 @@ RCT_EXTERN NSString *const RCTShowDevMenuNotification; */ @property (nonatomic, assign) BOOL hotkeysEnabled; +#if TARGET_OS_OSX // [macOS +/** + * Reference to debug menu hotkey for registration lifecycle + * CMD + Shift + I to match Chrome's devtools hotkey + */ +@property EventHotKeyRef hotKeyRef; +#endif // macOS] + /** * Presented items in development menu */ diff --git a/packages/react-native/React/CoreModules/RCTDevMenu.mm b/packages/react-native/React/CoreModules/RCTDevMenu.mm index 8e93ade394ae6a..f4117c7cdefcd2 100644 --- a/packages/react-native/React/CoreModules/RCTDevMenu.mm +++ b/packages/react-native/React/CoreModules/RCTDevMenu.mm @@ -120,6 +120,7 @@ @implementation RCTDevMenu { @synthesize moduleRegistry = _moduleRegistry; @synthesize callableJSModules = _callableJSModules; @synthesize bundleManager = _bundleManager; +@synthesize hotKeyRef = _hotKeyRef; RCT_EXPORT_MODULE() @@ -150,6 +151,26 @@ - (instancetype)init return self; } +#if TARGET_OS_OSX // [macOS +static OSStatus openDebuggerHandler(EventHandlerCallRef nextHandler, EventRef anEvent, void *userData) +{ + // Do nothing if the app isn't the key window. + if (![[NSApplication sharedApplication] keyWindow]) { + return noErr; + } + +#if RCT_ENABLE_INSPECTOR + NSURL *bundleURL = (__bridge NSURL *) userData; + [RCTInspectorDevServerHelper + openDebugger:bundleURL + withErrorMessage: + @"Failed to open debugger. Please check that the dev server is running and reload the app."]; +#endif + + return noErr; +} +#endif // macOS] + - (void)registerHotkeys { #if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST @@ -170,7 +191,32 @@ - (void)registerHotkeys [(RCTDevSettings *)[weakSelf.moduleRegistry moduleForName:"DevSettings"] toggleElementInspector]; }]; -#endif + + // Reload in normal mode + [commands registerKeyCommandWithInput:@"n" + modifierFlags:UIKeyModifierCommand + action:^(__unused UIKeyCommand *command) { + [(RCTDevSettings *)[weakSelf.moduleRegistry moduleForName:"DevSettings"] + setIsDebuggingRemotely:NO]; + }]; +#elif TARGET_OS_OSX // [macOS + EventHotKeyID hotKeyID; + hotKeyID.signature = 'mhk1'; + hotKeyID.id = 1; + + EventTypeSpec eventType; + eventType.eventClass = kEventClassKeyboard; + eventType.eventKind = kEventHotKeyPressed; + + InstallApplicationEventHandler(&openDebuggerHandler, 1, &eventType, (void *)CFBridgingRetain(self->_bundleManager.bundleURL), NULL); + RegisterEventHotKey(kVK_ANSI_I, + shiftKey | cmdKey, + hotKeyID, + GetApplicationEventTarget(), + 0, + &_hotKeyRef); + +#endif // macOS] } - (void)unregisterHotkeys @@ -181,7 +227,9 @@ - (void)unregisterHotkeys [commands unregisterKeyCommandWithInput:@"d" modifierFlags:UIKeyModifierCommand]; [commands unregisterKeyCommandWithInput:@"i" modifierFlags:UIKeyModifierCommand]; [commands unregisterKeyCommandWithInput:@"n" modifierFlags:UIKeyModifierCommand]; -#endif +#elif TARGET_OS_OSX // [macOS + UnregisterEventHotKey(_hotKeyRef); +#endif // macOS] } - (BOOL)isHotkeysRegistered @@ -247,12 +295,16 @@ - (void)toggle [self show]; } } +#endif // [macOS] - (BOOL)isActionSheetShown { +#if !TARGET_OS_OSX // [macOS return _actionSheet != nil; +#else + return NO; +#endif // macOS] } -#endif // [macOS] - (void)addItem:(NSString *)title handler:(void (^)(void))handler { @@ -277,7 +329,9 @@ - (void)setDefaultJSBundle // Add built-in items __weak RCTDevSettings *devSettings = [_moduleRegistry moduleForName:"DevSettings"]; +#if !TARGET_OS_OSX // [macOS] __weak RCTDevMenu *weakSelf = self; +#endif // [macOS] __weak RCTBundleManager *bundleManager = _bundleManager; [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload" @@ -612,6 +666,11 @@ + (NSString *)moduleName return @"DevMenu"; } +- (NSMenu *)menu +{ + return nil; +} + - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { diff --git a/packages/react-native/React/CoreModules/RCTDeviceInfo.mm b/packages/react-native/React/CoreModules/RCTDeviceInfo.mm index 3dce6f0b53060a..29b3d6979ec3b2 100644 --- a/packages/react-native/React/CoreModules/RCTDeviceInfo.mm +++ b/packages/react-native/React/CoreModules/RCTDeviceInfo.mm @@ -17,7 +17,7 @@ #import #import #import -#import "UIView+React.h" // [macOS] +#import // [macOS] #import #import "CoreModulesPlugins.h" @@ -121,11 +121,13 @@ - (void)invalidate - (void)_cleanupObservers { +#if !TARGET_OS_OSX // [macOS] [[NSNotificationCenter defaultCenter] removeObserver:self name:RCTAccessibilityManagerDidUpdateMultiplierNotification object:[_moduleRegistry moduleForName:"AccessibilityManager"]]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; +#endif [[NSNotificationCenter defaultCenter] removeObserver:self name:RCTUserInterfaceStyleDidChangeNotification object:nil]; @@ -139,9 +141,9 @@ - (void)_cleanupObservers static BOOL RCTIsIPhoneNotched() { static BOOL isIPhoneNotched = NO; +#if TARGET_OS_IOS static dispatch_once_t onceToken; -#if TARGET_OS_IOS dispatch_once(&onceToken, ^{ // 20pt is the top safeArea value in non-notched devices isIPhoneNotched = [RCTWindowSafeAreaProxy sharedInstance].currentSafeAreaInsets.top > 20; @@ -206,12 +208,12 @@ static BOOL RCTIsIPhoneNotched() - (NSDictionary *)_exportedDimensions { +#if !TARGET_OS_OSX // [macOS] RCTAssert(!_invalidated, @"Failed to get exported dimensions: RCTDeviceInfo has been invalidated"); RCTAssert(_moduleRegistry, @"Failed to get exported dimensions: RCTModuleRegistry is nil"); RCTAccessibilityManager *accessibilityManager = (RCTAccessibilityManager *)[_moduleRegistry moduleForName:"AccessibilityManager"]; RCTAssert(accessibilityManager, @"Failed to get exported dimensions: AccessibilityManager is nil"); -#if !TARGET_OS_OSX // [macOS] CGFloat fontScale = accessibilityManager ? accessibilityManager.multiplier : 1.0; #else // [macOS CGFloat fontScale = 1.0; diff --git a/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm b/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm index b0aecab697890c..6d5752595f68ac 100644 --- a/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm +++ b/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm @@ -85,6 +85,7 @@ -(void)EVENT : (NSNotification *)notification @end +#if !TARGET_OS_OSX // [macOS] NS_INLINE NSDictionary *RCTRectDictionaryValue(CGRect rect) { return @{ @@ -94,7 +95,6 @@ -(void)EVENT : (NSNotification *)notification @"height" : @(rect.size.height), }; } -#if !TARGET_OS_OSX // [macOS] static NSString *RCTAnimationNameForCurve(UIViewAnimationCurve curve) { switch (curve) { diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h index bc24d3fa3e20ab..3fdda39d243b06 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h @@ -54,6 +54,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL snapToEnd; @property (nonatomic, copy) NSArray *snapToOffsets; +#if TARGET_OS_OSX // [macOS +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; +- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated; +- (void)flashScrollIndicators; +#endif // macOS] + /* * Makes `setContentOffset:` method no-op when given `block` is executed. * The block is being executed synchronously. diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm index 4773f61b3db89d..07f7c59e90a02d 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm @@ -59,12 +59,55 @@ - (instancetype)initWithFrame:(CGRect)frame [weakSelf setPrivateDelegate:delegate]; }]; [_delegateSplitter addDelegate:self]; -#endif // [macOS] +#else // [macOS + self.hasHorizontalScroller = YES; + self.hasVerticalScroller = YES; + self.autohidesScrollers = YES; +#endif // macOS] } return self; } +#if TARGET_OS_OSX // [macOS +- (void)setFrame:(NSRect)frame +{ + // Preserving and revalidating `contentOffset`. + CGPoint originalOffset = self.contentOffset; + + [super setFrame:frame]; + + UIEdgeInsets contentInset = self.contentInset; + CGSize contentSize = self.contentSize; + + // If contentSize has not been measured yet we can't check bounds. + if (CGSizeEqualToSize(contentSize, CGSizeZero)) { + self.contentOffset = originalOffset; + } else { + CGSize boundsSize = self.bounds.size; + CGFloat xMaxOffset = contentSize.width - boundsSize.width + contentInset.right; + CGFloat yMaxOffset = contentSize.height - boundsSize.height + contentInset.bottom; + // Make sure offset doesn't exceed bounds. This can happen on screen rotation. + if ((originalOffset.x >= -contentInset.left) && (originalOffset.x <= xMaxOffset) && + (originalOffset.y >= -contentInset.top) && (originalOffset.y <= yMaxOffset)) { + return; + } + self.contentOffset = CGPointMake( + MAX(-contentInset.left, MIN(xMaxOffset, originalOffset.x)), + MAX(-contentInset.top, MIN(yMaxOffset, originalOffset.y))); + } +} + +- (NSSize)contentSize +{ + if (!self.documentView) { + return [super contentSize]; + } + + return self.documentView.frame.size; +} +#endif // macos] + - (void)preserveContentOffsetWithBlock:(void (^)())block { if (!block) { @@ -89,43 +132,56 @@ - (void)centerContentIfNeeded return; } - CGSize contentSize = self.contentSize; - CGSize boundsSize = self.bounds.size; - if (CGSizeEqualToSize(contentSize, CGSizeZero) || CGSizeEqualToSize(boundsSize, CGSizeZero)) { - return; + if (_centerContent && !CGSizeEqualToSize(self.contentSize, CGSizeZero)) { +#if !TARGET_OS_OSX // [macOS] + CGSize scrollViewSize = self.bounds.size; +#else // [macOS + CGSize scrollViewSize = self.contentView.bounds.size; +#endif // macOS] + if (self.contentSize.width <= scrollViewSize.width) { + contentOffset.x = -(scrollViewSize.width - self.contentSize.width) / 2.0; + } + if (self.contentSize.height <= scrollViewSize.height) { + contentOffset.y = -(scrollViewSize.height - self.contentSize.height) / 2.0; + } } - CGFloat top = 0, left = 0; - if (contentSize.width < boundsSize.width) { - left = (boundsSize.width - contentSize.width) * 0.5f; - } - if (contentSize.height < boundsSize.height) { - top = (boundsSize.height - contentSize.height) * 0.5f; +#if !TARGET_OS_OSX // [macOS] + super.contentOffset = CGPointMake( + RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"), + RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y")); +#else // [macOS + if (!NSEqualPoints(contentOffset, self.documentVisibleRect.origin)) { + [self.contentView scrollToPoint:contentOffset]; + [self reflectScrolledClipView:self.contentView]; } - self.contentInset = UIEdgeInsetsMake(top, left, top, left); +#endif // macOS] } -- (void)setContentOffset:(CGPoint)contentOffset + +#if TARGET_OS_OSX // [macOS +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated { - if (_isSetContentOffsetDisabled) { - return; + if (animated) { + [NSAnimationContext beginGrouping]; + [[NSAnimationContext currentContext] setDuration:0.3]; + [[self.contentView animator] setBoundsOrigin:contentOffset]; + [NSAnimationContext endGrouping]; + } else { + self.contentOffset = contentOffset; } - super.contentOffset = CGPointMake( - RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"), - RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y")); } -- (void)setFrame:(CGRect)frame +- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated { - [super setFrame:frame]; - [self centerContentIfNeeded]; + [self magnifyToFitRect:rect]; } -- (void)didAddSubview:(RCTPlatformView *)subview // [macOS] +- (void)flashScrollIndicators { - [super didAddSubview:subview]; - [self centerContentIfNeeded]; + [self flashScrollers]; } +#endif // macOS] #if !TARGET_OS_OSX // [macOS] - (BOOL)touchesShouldCancelInContentView:(UIView *)view diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h index 2eb12b85e2a4f4..caaf12dd958453 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h @@ -56,6 +56,10 @@ NS_ASSUME_NONNULL_BEGIN RCTGenericDelegateSplitter> *scrollViewDelegateSplitter; #endif // [macOS] +#if TARGET_OS_OSX // [macOS +@property (nonatomic, assign) UIEdgeInsets contentInset; +#endif // macOS] + @end /* diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index ec8136bb75a9fd..1f1c1445d027c3 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -147,7 +147,7 @@ - (instancetype)initWithFrame:(CGRect)frame _containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [_scrollView setDocumentView:_containerView]; #endif // macOS] - + #if !TARGET_OS_OSX // [macOS] [self.scrollViewDelegateSplitter addDelegate:self]; #endif // [macOS] @@ -275,6 +275,38 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu } #endif +#if TARGET_OS_OSX // [macOS +- (void)viewDidMoveToWindow +{ + [super viewDidMoveToWindow]; + + NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter]; + if (self.window == nil) { + // Unregister scrollview's clipview bounds change notifications + [defaultCenter removeObserver:self + name:NSViewBoundsDidChangeNotification + object:_scrollView.contentView]; + } else { + // Register for scrollview's clipview bounds change notifications so we can track scrolling + [defaultCenter addObserver:self + selector:@selector(scrollViewDocumentViewBoundsDidChange:) + name:NSViewBoundsDidChangeNotification + object:_scrollView.contentView]; // NSClipView + } +} + +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + if (UIEdgeInsetsEqualToEdgeInsets(contentInset, _contentInset)) { + return; + } + + _contentInset = contentInset; + _scrollView.contentInset = contentInset; + _scrollView.scrollIndicatorInsets = contentInset; +} +#endif // macOS] + #if !TARGET_OS_OSX // [macOS] - (RCTGenericDelegateSplitter> *)scrollViewDelegateSplitter { @@ -406,7 +438,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & MAP_SCROLL_VIEW_PROP(zoomScale); if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset) { +#if !TARGET_OS_OSX // [macOS] _scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset); +#else // [macOS + self.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset); +#endif // macOS] } RCTEnhancedScrollView *scrollView = (RCTEnhancedScrollView *)_scrollView; @@ -450,7 +486,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _shouldUpdateContentInsetAdjustmentBehavior = NO; } #endif // [macOS] - + MAP_SCROLL_VIEW_PROP(disableIntervalMomentum); MAP_SCROLL_VIEW_PROP(snapToInterval); @@ -636,6 +672,22 @@ - (void)prepareForRecycle _firstVisibleView = nil; } +#if TARGET_OS_OSX // [macOS +#pragma mark - NSScrollView scroll notification + +- (void)scrollViewDocumentViewBoundsDidChange:(__unused NSNotification *)notification +{ + RCTEnhancedScrollView *scrollView = _scrollView; + + if (scrollView.centerContent) { + // Update content centering through contentOffset setter + [scrollView setContentOffset:scrollView.contentOffset]; + } + + [self scrollViewDidScroll:scrollView]; +} +#endif // macOS] + #pragma mark - UIScrollViewDelegate #if !TARGET_OS_OSX // [macOS] @@ -859,9 +911,7 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args - (void)flashScrollIndicators { -#if !TARGET_OS_OSX // [macOS] - [_scrollView flashScrollIndicators]; -#endif // [macOS] + [(RCTEnhancedScrollView *)_scrollView flashScrollIndicators]; // [macOS] } - (void)scrollTo:(double)x y:(double)y animated:(BOOL)animated @@ -960,7 +1010,7 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated [self _forceDispatchNextScrollEvent]; - [_scrollView setContentOffset:offset animated:animated]; + [(RCTEnhancedScrollView *)_scrollView setContentOffset:offset animated:animated]; // [macOS] if (!animated) { // When not animated, the expected workflow in ``scrollViewDidEndScrollingAnimation`` after scrolling is not going @@ -971,9 +1021,7 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated - (void)zoomToRect:(CGRect)rect animated:(BOOL)animated { -#if !TARGET_OS_OSX // [macOS] - [_scrollView zoomToRect:rect animated:animated]; -#endif // [macOS] + [(RCTEnhancedScrollView *)_scrollView zoomToRect:rect animated:animated]; // [macOS] } #if !TARGET_OS_OSX // [macOS] diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.h index b02c6893772b9b..43be8df64b5eac 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.h @@ -14,7 +14,11 @@ NS_ASSUME_NONNULL_BEGIN /* * UIView class for component. */ +#if !TARGET_OS_OSX // [macOS] @interface RCTParagraphComponentView : RCTViewComponentView +#else // [macOS +@interface RCTParagraphComponentView : RCTViewComponentView +#endif // macOS] /* * Returns an `NSAttributedString` representing the content of the component. 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 4a237ee7f8227c..6089e57ca781da 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -24,8 +24,8 @@ #import #import #if TARGET_OS_OSX // [macOS -#import #import +#import #endif // macOS] #import "RCTConversions.h" @@ -1121,7 +1121,7 @@ - (void)_setSecureTextEntry:(BOOL)secureTextEntry 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; 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 af394b1535ddf7..64f27c09128d4e 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -21,6 +21,10 @@ #import #import #import +#if TARGET_OS_OSX // [macOS +#import +#import +#endif // macOS] #import #import #import @@ -53,11 +57,9 @@ @implementation RCTViewComponentView { BOOL _needsInvalidateLayer; BOOL _isJSResponder; BOOL _removeClippedSubviews; -#if TARGET_OS_OSX // [macOS - BOOL _hasMouseOver; - BOOL _hasClipViewBoundsObserver; - NSTrackingArea *_trackingArea; -#endif // macOS] + BOOL _hasMouseOver; // [macOS] + BOOL _hasClipViewBoundsObserver; // [macOS] + NSTrackingArea *_trackingArea; // [macOS] NSMutableArray *_reactSubviews; // [macOS] NSSet *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN; RCTPlatformView *_containerView; // [macOS] @@ -78,6 +80,9 @@ - (instancetype)initWithFrame:(CGRect)frame _reactSubviews = [NSMutableArray new]; #if !TARGET_OS_OSX // [macOS] self.multipleTouchEnabled = YES; +#else + // React views have their bounds clipping disabled by default + self.clipsToBounds = NO; #endif // [macOS] _useCustomContainerView = NO; _removeClippedSubviews = NO; @@ -156,7 +161,7 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection - (void)viewDidChangeEffectiveAppearance { [super viewDidChangeEffectiveAppearance]; - + [self invalidateLayer]; } #endif // macOS] @@ -320,7 +325,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & if (oldViewProps.backfaceVisibility != newViewProps.backfaceVisibility) { self.layer.doubleSided = newViewProps.backfaceVisibility == BackfaceVisibility::Visible; } - + // `cursor` if (oldViewProps.cursor != newViewProps.cursor) { needsInvalidateLayer = YES; @@ -593,14 +598,24 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & } #if TARGET_OS_OSX // [macOS - // `draggedTypes` + // `focusable` + if (oldViewProps.focusable != newViewProps.focusable) { + self.focusable = (bool)newViewProps.focusable; + } + + // `enableFocusRing` + if (oldViewProps.enableFocusRing != newViewProps.enableFocusRing) { + self.enableFocusRing = (bool)newViewProps.enableFocusRing; + } + + // `draggedTypes` if (oldViewProps.draggedTypes != newViewProps.draggedTypes) { if (!oldViewProps.draggedTypes.empty()) { [self unregisterDraggedTypes]; } if (!newViewProps.draggedTypes.empty()) { - NSMutableArray *pasteboardTypes = [NSMutableArray arrayWithCapacity:newViewProps.draggedTypes.size()]; + NSMutableArray *pasteboardTypes = [NSMutableArray new]; for (const auto &draggedType : newViewProps.draggedTypes) { if (draggedType == "fileUrl") { [pasteboardTypes addObject:NSFilenamesPboardType]; @@ -681,6 +696,10 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask { [super finalizeUpdates:updateMask]; _useCustomContainerView = [self styleWouldClipOverflowInk]; + + [self updateTrackingAreas]; + [self updateClipViewBoundsObserverIfNeeded]; + if (!_needsInvalidateLayer) { return; } @@ -942,8 +961,6 @@ static RCTCursor RCTCursorFromCursor(Cursor cursor) return RCTCursorText; case Cursor::Url: return RCTCursorUrl; - case Cursor::VerticalText: - return RCTCursorVerticalText; case Cursor::WResize: return RCTCursorWResize; case Cursor::Wait: @@ -1011,6 +1028,17 @@ - (RCTUIView *)currentContainerView // [macOS] } } +#if TARGET_OS_OSX // [macOS +- (void)setClipsToBounds:(BOOL)clipsToBounds +{ + // Set the property managed by RCTUIView + super.clipsToBounds = clipsToBounds; + + // Bounds clipping must also be configured on the view's layer + self.layer.masksToBounds = clipsToBounds; +} +#endif // macOS] + - (void)invalidateLayer { CALayer *layer = self.layer; @@ -1043,7 +1071,7 @@ - (void)invalidateLayer } else { layer.shadowPath = nil; } - + #if !TARGET_OS_OSX // [visionOS] // Stage 1.5. Cursor / Hover Effects if (@available(iOS 17.0, *)) { @@ -1064,7 +1092,7 @@ - (void)invalidateLayer UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:borderPath]; CGPathRelease(borderPath); UIShape *shape = [UIShape shapeWithBezierPath:bezierPath]; - + hoverStyle = [UIHoverStyle styleWithEffect:[UIHoverAutomaticEffect effect] shape:shape]; } [self setHoverStyle:hoverStyle]; @@ -1145,7 +1173,17 @@ - (void)invalidateLayer layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left; RCTUIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left); // [macOS] layer.borderColor = borderColor.CGColor; + +#if TARGET_OS_OSX // macOS] + // Setting the corner radius on view's layer enables back clipping to bounds. To + // avoid getting the native view out of sync with the component's props, we make + // sure that clipsToBounds stays unchanged after setting the corner radius. + BOOL clipsToBounds = self.clipsToBounds; +#endif layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft.horizontal; +#if TARGET_OS_OSX // macOS] + self.clipsToBounds = clipsToBounds; +#endif layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft); @@ -1244,9 +1282,24 @@ - (void)invalidateLayer const auto &linearGradient = std::get(backgroundImage); CALayer *backgroundImageLayer = [RCTLinearGradient gradientLayerWithSize:self.layer.bounds.size gradient:linearGradient]; - [self shapeLayerToMatchView:backgroundImageLayer borderMetrics:borderMetrics]; + backgroundImageLayer.frame = layer.bounds; backgroundImageLayer.masksToBounds = YES; + // To make border radius work with gradient layers + if (borderMetrics.borderRadii.isUniform()) { + backgroundImageLayer.cornerRadius = layer.cornerRadius; + backgroundImageLayer.cornerCurve = layer.cornerCurve; + backgroundImageLayer.mask = nil; + } else { + CAShapeLayer *maskLayer = + [self createMaskLayer:self.bounds + cornerInsets:RCTGetCornerInsets( + RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)]; + backgroundImageLayer.mask = maskLayer; + backgroundImageLayer.cornerRadius = 0; + } + backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION; + [self.layer addSublayer:backgroundImageLayer]; [_backgroundImageLayers addObject:backgroundImageLayer]; } @@ -1356,6 +1409,33 @@ - (void)clearExistingBackgroundImageLayers [_backgroundImageLayers removeAllObjects]; } +#if TARGET_OS_OSX // [macOS +#pragma mark - Native Commands + +- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args +{ + RCTComponentViewHandleCommand(self, commandName, args); +} + +- (void)focus +{ + NSWindow *window = self.window; + if (window && self.focusable) { + [window makeFirstResponder:self]; + } +} + +- (void)blur +{ + NSWindow *window = self.window; + if (window && window.firstResponder == self) { + // Calling makeFirstResponder with nil will call resignFirstResponder and make the window the first responder + [window makeFirstResponder:nil]; + } +} +#endif // macOS] + + #pragma mark - Accessibility #if !TARGET_OS_OSX // [macOS] @@ -1561,78 +1641,21 @@ - (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)acti } } -#if TARGET_OS_OSX // [macOS - -- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args -{ - if ([commandName isEqualToString:@"focus"]) { - [self focus]; - return; - } - - if ([commandName isEqualToString:@"blur"]) { - [self blur]; - return; - } -} - -# pragma mark - Focus Ring - -- (CGRect)focusRingMaskBounds -{ - return [self bounds]; -} - -- (void)drawFocusRingMask -{ - if (_props->enableFocusRing) { - CGContextRef context = NSGraphicsContext.currentContext.CGContext; - - const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics); - const RCTCornerInsets cornerInsets = - RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero); - CGPathRef path = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, NULL); - - CGContextAddPath(context, path); - CGContextFillPath(context); - CGPathRelease(path); - } -} +#if TARGET_OS_OSX // [macOS #pragma mark - Focus Events - -- (void)focus -{ - [[self window] makeFirstResponder:self]; -} - -- (void)blur -{ - [[self window] resignFirstResponder]; -} - -- (BOOL)needsPanelToBecomeKey -{ - // We need to override this so that mouse clicks don't move keyboard focus on focusable views by default. - return false; -} - -- (BOOL)acceptsFirstResponder -{ - return _props->focusable || [super acceptsFirstResponder]; -} - (BOOL)becomeFirstResponder { if (![super becomeFirstResponder]) { return NO; } - + if (_eventEmitter) { _eventEmitter->onFocus(); } - + return YES; } @@ -1641,56 +1664,68 @@ - (BOOL)resignFirstResponder if (![super resignFirstResponder]) { return NO; } - + if (_eventEmitter) { _eventEmitter->onBlur(); } - + return YES; } + #pragma mark - Keyboard Events - (BOOL)handleKeyboardEvent:(NSEvent *)event { - RCTAssert( - event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp, - @"Keyboard event must be keyDown, keyUp. Got type: %ld", (long)event.type); + BOOL keyDown = event.type == NSEventTypeKeyDown; + BOOL hasHandler = keyDown ? _props->hostPlatformEvents[HostPlatformViewEvents::Offset::KeyDown] + : _props->hostPlatformEvents[HostPlatformViewEvents::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 = " " } }; + } - // 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), - }; + // If there are no valid keys defined, no key event handling is required. + if (!validKeys.has_value()) { + return NO; + } - if (_eventEmitter) { - // Emit the event to JS only once. By default, events, will bubble up the respnder chain - // when we call super, so let's emit the event only at the first responder. It would be - // simpler to check `if (self == self.window.firstResponder), however, that does not account - // for cases like TextInputComponentView, where the first responder may be a subview. - static const char kRCTViewKeyboardEventEmittedKey = 0; - NSNumber *emitted = objc_getAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey); - BOOL alreadyEmitted = [emitted boolValue]; - if (!alreadyEmitted) { - if (event.type == NSEventTypeKeyDown) { + // Convert the event to a KeyEvent + NSEventModifierFlags modifierFlags = event.modifierFlags; + 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); } - objc_setAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + return YES; } } - // If keyDownEvents or keyUpEvents specifies the event, block native handling of the event - auto const& handledKeyEvents = event.type == NSEventTypeKeyDown ? _props->keyDownEvents : _props->keyUpEvents; - return std::find(handledKeyEvents.cbegin(), handledKeyEvents.cend(), keyEvent) != handledKeyEvents.cend(); + return NO; } - (void)keyDown:(NSEvent *)event { @@ -1714,19 +1749,14 @@ - (void)keyUp:(NSEvent *)event { Drop, }; -- (DataTransfer)dataTransferForPasteboard:(NSPasteboard *)pasteboard { - DataTransfer dataTransfer{}; - auto &files = dataTransfer.files; - auto &items = dataTransfer.items; - auto &types = dataTransfer.types; - +- (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; + 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; @@ -1744,73 +1774,63 @@ - (DataTransfer)dataTransferForPasteboard:(NSPasteboard *)pasteboard { forKey:NSURLFileSizeKey error:&fileSizeError]; - std::string typeString = MIMETypeString != nil ? [MIMETypeString UTF8String] : ""; + NSNumber *width = nil; + NSNumber *height = nil; + if ([MIMETypeString hasPrefix:@"image/"]) { + NSImage *image = [[NSImage alloc] initWithContentsOfURL:fileURL]; + width = @(image.size.width); + height = @(image.size.height); + } - DataTransferFile fileEntry = { - .name = fileURL.lastPathComponent ? fileURL.lastPathComponent.UTF8String : "", - .type = typeString, - .uri = fileURL.path ? fileURL.path.UTF8String : "", + DataTransferItem transferItem = { + .name = fileURL.lastPathComponent.UTF8String, + .kind = "file", + .type = MIMETypeString.UTF8String, + .uri = fileURL.path.UTF8String, }; if (success) { - fileEntry.size = fileSizeValue.intValue; + transferItem.size = fileSizeValue.intValue; } - if ([MIMETypeString hasPrefix:@"image/"]) { - NSImage *image = [[NSImage alloc] initWithContentsOfURL:fileURL]; - CGImageRef cgImage = [image CGImageForProposedRect:nil context:nil hints:nil]; - fileEntry.width = static_cast(CGImageGetWidth(cgImage)); - fileEntry.height = static_cast(CGImageGetHeight(cgImage)); + if (width != nil) { + transferItem.width = width.intValue; } - files.push_back(fileEntry); - items.push_back({ - .kind = "file", - .type = typeString, - }); - types.push_back(typeString); + 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 ?[UTTypePNG preferredMIMEType] : [UTTypeTIFF preferredMIMEType]; + NSString *MIMETypeString = imageType == NSPasteboardTypePNG ? @"image/png" : @"image/tiff"; NSData *imageData = [pasteboard dataForType:imageType]; NSImage *image = [[NSImage alloc] initWithData:imageData]; - CGImageRef cgImage = [image CGImageForProposedRect:nil context:nil hints:nil]; - - NSString *dataURLString = RCTDataURL(MIMETypeString, imageData).absoluteString; - std::string typeString = MIMETypeString != nil ? [MIMETypeString UTF8String] : ""; - - DataTransferFile fileEntry = { - .name = "", - .type = typeString, - .uri = dataURLString ? dataURLString.UTF8String : "", - }; - fileEntry.size = static_cast(imageData.length); - fileEntry.width = static_cast(CGImageGetWidth(cgImage)); - fileEntry.height = static_cast(CGImageGetHeight(cgImage)); + DataTransferItem transferItem = { + .kind = "image", + .type = MIMETypeString.UTF8String, + .uri = RCTDataURL(MIMETypeString, imageData).absoluteString.UTF8String, + .size = imageData.length, + .width = image.size.width, + .height = image.size.height, + }; - files.push_back(fileEntry); - items.push_back({ - .kind = "image", - .type = typeString, - }); - types.push_back(typeString); + dataTransferItems.push_back(transferItem); } - return dataTransfer; } -- (void)emitDragEvent:(DragEventType)eventType draggingInfo:(id)sender { +- (void)sendDragEvent:(DragEventType)eventType withLocation:(NSPoint)locationInWindow pasteboard:(NSPasteboard *)pasteboard { if (!_eventEmitter) { return; } - - NSPoint locationInWindow = sender.draggingLocation; - NSPasteboard *pasteboard = sender.draggingPasteboard; - - DataTransfer dataTransfer = [self dataTransferForPasteboard:pasteboard]; + + std::vector dataTransferItems{}; + [self buildDataTransferItems:dataTransferItems forPasteboard:pasteboard]; NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; NSEventModifierFlags modifierFlags = self.window.currentEvent.modifierFlags; @@ -1826,18 +1846,18 @@ - (void)emitDragEvent:(DragEventType)eventType draggingInfo:(id) .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), }, - .dataTransfer = dataTransfer, + .dataTransferItems = dataTransferItems, }; - + switch (eventType) { case DragEnter: _eventEmitter->onDragEnter(dragEvent); break; - + case DragLeave: _eventEmitter->onDragLeave(dragEvent); break; - + case Drop: _eventEmitter->onDrop(dragEvent); break; @@ -1849,7 +1869,7 @@ - (NSDragOperation)draggingEntered:(id )sender NSPasteboard *pboard = sender.draggingPasteboard; NSDragOperation sourceDragMask = sender.draggingSourceOperationMask; - [self emitDragEvent:DragEnter draggingInfo:sender]; + [self sendDragEvent:DragEnter withLocation:sender.draggingLocation pasteboard:pboard]; if ([pboard availableTypeFromArray:self.registeredDraggedTypes]) { if (sourceDragMask & NSDragOperationLink) { @@ -1863,26 +1883,32 @@ - (NSDragOperation)draggingEntered:(id )sender - (void)draggingExited:(id)sender { - [self emitDragEvent:DragLeave draggingInfo:sender]; + [self sendDragEvent:DragLeave withLocation:sender.draggingLocation pasteboard:sender.draggingPasteboard]; } - (BOOL)performDragOperation:(id )sender { - [self emitDragEvent:Drop draggingInfo:sender]; + [self sendDragEvent:Drop withLocation:sender.draggingLocation pasteboard:sender.draggingPasteboard]; return YES; } #pragma mark - Mouse Events -- (void)emitMouseEvent { +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 = { @@ -1895,11 +1921,17 @@ - (void)emitMouseEvent { .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), }; - - if (_hasMouseOver) { - _eventEmitter->onMouseEnter(mouseEvent); - } else { - _eventEmitter->onMouseLeave(mouseEvent); + + switch (eventType) { + case MouseEnter: + _eventEmitter->onMouseEnter(mouseEvent); + break; + case MouseLeave: + _eventEmitter->onMouseLeave(mouseEvent); + break; + case DoubleClick: + _eventEmitter->onDoubleClick(mouseEvent); + break; } } @@ -1915,7 +1947,7 @@ - (void)updateMouseOverIfNeeded NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; BOOL insideBounds = NSPointInRect(locationInView, self.visibleRect); - // On macOS 14+ visibleRect can be larger than the view bounds + // On macOS 14.0 visibleRect can be larger than the view bounds insideBounds &= NSPointInRect(locationInView, self.bounds); if (hasMouseOver && !insideBounds) { @@ -1928,7 +1960,7 @@ - (void)updateMouseOverIfNeeded if (hasMouseOver != _hasMouseOver) { _hasMouseOver = hasMouseOver; - [self emitMouseEvent]; + [self sendMouseEvent:hasMouseOver ? MouseEnter : MouseLeave]; } } @@ -1939,9 +1971,8 @@ - (void)updateClipViewBoundsObserverIfNeeded // both of which would not cause the mouseExited to be invoked. NSClipView *clipView = self.window ? self.enclosingScrollView.contentView : nil; - - BOOL hasMouseEventHandler = - _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] || + + BOOL hasMouseEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] || _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave]; if (_hasClipViewBoundsObserver && (!clipView || !hasMouseEventHandler)) { @@ -1967,27 +1998,35 @@ - (void)viewDidMoveToWindow - (void)updateTrackingAreas { - BOOL hasMouseEventHandler = - _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] || - _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave]; - BOOL wouldRecreateIdenticalTrackingArea = - hasMouseEventHandler && _trackingArea && NSEqualRects(self.bounds, [_trackingArea rect]); - - if (!wouldRecreateIdenticalTrackingArea) { + if (_trackingArea) { [self removeTrackingArea:_trackingArea]; - if (hasMouseEventHandler) { - _trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds - options:NSTrackingActiveAlways|NSTrackingMouseEnteredAndExited - owner:self - userInfo:nil]; - [self addTrackingArea:_trackingArea]; - [self updateMouseOverIfNeeded]; - } + } + + if ( + _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] || + _props->hostPlatformEvents[HostPlatformViewEvents::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->hostPlatformEvents[HostPlatformViewEvents::Offset::DoubleClick]; + if (hasDoubleClickEventHandler && event.clickCount == 2) { + [self sendMouseEvent:DoubleClick]; + } else { + [super mouseUp:event]; + } +} + - (void)mouseEntered:(NSEvent *)event { if (_hasMouseOver) { @@ -2001,7 +2040,7 @@ - (void)mouseEntered:(NSEvent *)event } _hasMouseOver = YES; - [self emitMouseEvent]; + [self sendMouseEvent:MouseEnter]; } - (void)mouseExited:(NSEvent *)event @@ -2011,7 +2050,7 @@ - (void)mouseExited:(NSEvent *)event } _hasMouseOver = NO; - [self emitMouseEvent]; + [self sendMouseEvent:MouseLeave]; } #endif // macOS] diff --git a/packages/react-native/React/Fabric/Mounting/RCTComponentViewProtocol.h b/packages/react-native/React/Fabric/Mounting/RCTComponentViewProtocol.h index 69213be2dbc1eb..7c44e1b09f433b 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTComponentViewProtocol.h +++ b/packages/react-native/React/Fabric/Mounting/RCTComponentViewProtocol.h @@ -6,6 +6,7 @@ */ #import // [macOS] +#import // [macOS] #import #import @@ -122,6 +123,11 @@ typedef NS_OPTIONS(NSInteger, RNComponentViewUpdateMask) { - (NSNumber *)reactTag; // [macOS] - (void)setReactTag:(NSNumber *)reactTag; // [macOS] +#if TARGET_OS_OSX // [macOS +- (void)focus; +- (void)blur; +#endif // macOS] + /* * This is broken. Do not use. */ @@ -130,4 +136,40 @@ typedef NS_OPTIONS(NSInteger, RNComponentViewUpdateMask) { @end +#if TARGET_OS_OSX // [macOS +RCT_EXTERN inline void +RCTComponentViewHandleCommand(id componentView, NSString const *commandName, NSArray const *args) +{ + if ([commandName isEqualToString:@"focus"]) { +#if RCT_DEBUG + if ([args count] != 0) { + RCTLogError( + @"%@ command %@ received %d arguments, expected %d.", @"View", commandName, (int)[args count], 0); + return; + } +#endif + + [componentView focus]; + return; + } + + if ([commandName isEqualToString:@"blur"]) { +#if RCT_DEBUG + if ([args count] != 0) { + RCTLogError( + @"%@ command %@ received %d arguments, expected %d.", @"View", commandName, (int)[args count], 0); + return; + } +#endif + + [componentView blur]; + return; + } + +#if RCT_DEBUG + RCTLogError(@"%@ received command %@, which is not a supported command.", @"View", commandName); +#endif +} +#endif // macOS] + NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h index f3f802d256592d..cebbb1c2872ecb 100644 --- a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h +++ b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h @@ -9,6 +9,10 @@ NS_ASSUME_NONNULL_BEGIN +#if TARGET_OS_OSX // [macOS +static NSString *const RCTSurfaceTouchHandlerOutsideViewMouseUpNotification = @"RCTSurfaceTouchHandlerOutsideViewMouseUpNotification"; +#endif // macOS] + @interface RCTSurfaceTouchHandler : UIGestureRecognizer /* @@ -23,6 +27,15 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) CGPoint viewOriginOffset; +#if TARGET_OS_OSX // [macOS ++ (instancetype)surfaceTouchHandlerForEvent:(NSEvent *)event; ++ (instancetype)surfaceTouchHandlerForView:(NSView *)view; ++ (void)notifyOutsideViewMouseUp:(NSEvent *)event; + +- (void)cancelTouchWithEvent:(NSEvent *)event; +- (void)reset; +#endif // macOS] + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm index fc83e173dc51ae..bf5f041bc3b8b1 100644 --- a/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm +++ b/packages/react-native/React/Fabric/RCTSurfaceTouchHandler.mm @@ -8,14 +8,70 @@ #import "RCTSurfaceTouchHandler.h" #import +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] #import #import #import +#if TARGET_OS_OSX // [macOS +#import "React/RCTSurfaceHostingView.h" +#endif // macOS] + #import "RCTConversions.h" #import "RCTSurfacePointerHandler.h" #import "RCTTouchableComponentViewProtocol.h" +#if TARGET_OS_OSX // [macOS +@interface RCTSurfaceTouchHandler (Private) +- (void)endFromEventTrackingLeftMouseUp:(NSEvent *)event; +- (void)endFromEventTrackingRightMouseUp:(NSEvent *)event; +@end + +@interface NSApplication (RCTSurfaceTouchHandlerOverride) +- (NSEvent*)override_surface_nextEventMatchingMask:(NSEventMask)mask + untilDate:(NSDate*)expiration + inMode:(NSRunLoopMode)mode + dequeue:(BOOL)dequeue; +@end + +@implementation NSApplication (RCTSurfaceTouchHandlerOverride) + ++ (void)load +{ + RCTSwapInstanceMethods(self, @selector(nextEventMatchingMask:untilDate:inMode:dequeue:), @selector(override_surface_nextEventMatchingMask:untilDate:inMode:dequeue:)); +} + +- (NSEvent*)override_surface_nextEventMatchingMask:(NSEventMask)mask + untilDate:(NSDate*)expiration + inMode:(NSRunLoopMode)mode + dequeue:(BOOL)dequeue +{ + NSEvent* event = [self override_surface_nextEventMatchingMask:mask + untilDate:expiration + inMode:mode + dequeue:dequeue]; + if (dequeue && (event.type == NSEventTypeLeftMouseUp || event.type == NSEventTypeRightMouseUp || event.type == NSEventTypeOtherMouseUp)) { + RCTSurfaceTouchHandler *targetSurfaceTouchHandler = [RCTSurfaceTouchHandler surfaceTouchHandlerForEvent:event]; + if (!targetSurfaceTouchHandler) { + [RCTSurfaceTouchHandler notifyOutsideViewMouseUp:event]; + } else if (event.type == NSEventTypeRightMouseUp && [mode isEqualTo:NSEventTrackingRunLoopMode]) { + // If the event is consumed by an event tracking loop, we won't get the mouse up event + if (event.type == NSEventTypeLeftMouseUp) { + [targetSurfaceTouchHandler endFromEventTrackingLeftMouseUp:event]; + } else if (event.type == NSEventTypeRightMouseUp) { + [targetSurfaceTouchHandler endFromEventTrackingRightMouseUp:event]; + } + } + } + + return event; +} + +@end +#endif // macOS] + using namespace facebook::react; typedef NS_ENUM(NSInteger, RCTTouchEventType) { @@ -207,7 +263,15 @@ - (instancetype)init self.cancelsTouchesInView = NO; self.delaysTouchesBegan = NO; // This is default value. self.delaysTouchesEnded = NO; -#endif // [macOS] +#else // [macOS + self.delaysPrimaryMouseButtonEvents = NO; // default is NO. + self.delaysSecondaryMouseButtonEvents = NO; // default is NO. + self.delaysOtherMouseButtonEvents = NO; // default is NO. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(endOutsideViewMouseUp:) + name:RCTSurfaceTouchHandlerOutsideViewMouseUpNotification + object:[RCTSurfaceTouchHandler class]]; +#endif // macOS] self.delegate = self; @@ -221,6 +285,12 @@ - (instancetype)init RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)action) +#if TARGET_OS_OSX // [macOS +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} +#endif // macOS] + - (void)attachToView:(RCTUIView *)view // [macOS] { RCTAssert(self.view == nil, @"RCTTouchHandler already has attached view."); @@ -586,4 +656,219 @@ - (void)_cancelTouches [self setEnabled:YES]; } +#if TARGET_OS_OSX // [macOS ++ (instancetype)surfaceTouchHandlerForEvent:(NSEvent *)event { + RCTPlatformView *hitView = [event.window.contentView.superview hitTest:event.locationInWindow]; + return [self surfaceTouchHandlerForView:hitView]; +} + ++ (instancetype)surfaceTouchHandlerForView:(RCTPlatformView *)view { + if ([view isKindOfClass:[RCTSurfaceHostingView class]]) { + // The RCTSurfaceTouchHandler is attached to surface's view. + view = (RCTPlatformView *)(((RCTSurfaceHostingView *)view).surface.view); + } + + while (view) { + for (NSGestureRecognizer *gestureRecognizer in view.gestureRecognizers) { + if ([gestureRecognizer isKindOfClass:[self class]]) { + return (RCTSurfaceTouchHandler *)gestureRecognizer; + } + } + + view = view.superview; + } + + return nil; +} + ++ (void)notifyOutsideViewMouseUp:(NSEvent *)event { + [[NSNotificationCenter defaultCenter] postNotificationName:RCTSurfaceTouchHandlerOutsideViewMouseUpNotification + object:self + userInfo:@{@"event": event}]; +} + +- (void)endOutsideViewMouseUp:(NSNotification *)notification { + NSEvent *event = notification.userInfo[@"event"]; + + auto iterator = _activeTouches.find(event.eventNumber); + if (iterator == _activeTouches.end()) { + // A contextual menu click would generate a mouse up with a diffrent event + // and leave a touchable/pressable session open. This would cause touch end + // events from a modal window to end the touchable/pressable session and + // potentially trigger an onPress event. Hence the need to reset and cancel + // that session when a mouse up event was detected outside the touch handler + // view bounds. + [self reset]; + return; + } + + [self cancelTouchWithEvent:event]; +} + +- (void)endFromEventTrackingRightMouseUp:(NSEvent *)event +{ + auto iterator = _activeTouches.find(event.eventNumber); + if (iterator == _activeTouches.end()) { + return; + } + + [self cancelTouchWithEvent:event]; +} + +- (void)cancelTouchWithEvent:(NSEvent *)event +{ + NSSet *touches = [NSSet setWithObject:event]; + [self _updateTouches:touches]; + [self _dispatchActiveTouches:[self _activeTouchesFromTouches:touches] eventType:RCTTouchEventTypeTouchCancel]; + [self _unregisterTouches:touches]; + + self.state = NSGestureRecognizerStateCancelled; +} +#endif // macOS] + +#if !TARGET_OS_OSX +- (void)hovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0)) +{ + RCTUIView *listenerView = recognizer.view; // [macOS] + CGPoint clientLocation = [recognizer locationInView:listenerView]; + CGPoint screenLocation = [listenerView convertPoint:clientLocation + toCoordinateSpace:listenerView.window.screen.coordinateSpace]; + + RCTUIView *targetView = [listenerView hitTest:clientLocation withEvent:nil]; // [macOS] + targetView = FindClosestFabricManagedTouchableView(targetView); + + CGPoint offsetLocation = [recognizer locationInView:targetView]; + + UIKeyModifierFlags modifierFlags; + if (@available(iOS 13.4, *)) { + modifierFlags = recognizer.modifierFlags; + } else { + modifierFlags = 0; + } + + PointerEvent event = + CreatePointerEventFromIncompleteHoverData(clientLocation, screenLocation, offsetLocation, modifierFlags); + + NSOrderedSet *eventPathViews = [self handleIncomingPointerEvent:event onView:targetView]; + SharedTouchEventEmitter eventEmitter = GetTouchEmitterFromView(targetView, offsetLocation); + bool hasMoveEventListeners = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMove) || + IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerMoveCapture); + if (eventEmitter != nil && hasMoveEventListeners) { + eventEmitter->onPointerMove(event); + } +} +#endif + +/** + * Private method which is used for tracking the location of pointer events to manage the entering/leaving events. + * The primary idea is that a pointer's presence & movement is dicated by a variety of underlying events such as down, + * move, and up — and they should all be treated the same when it comes to tracking the entering & leaving of pointers + * to views. This method accomplishes that by recieving the pointer event, the target view (can be null in cases when + * the event indicates that the pointer has left the screen entirely), and a block/callback where the underlying event + * should be fired. + */ +#if !TARGET_OS_OSX +- (NSOrderedSet *)handleIncomingPointerEvent:(PointerEvent)event + onView:(nullable RCTUIView *)targetView // [macOS] +{ + int pointerId = event.pointerId; + CGPoint clientLocation = CGPointMake(event.clientPoint.x, event.clientPoint.y); + + NSOrderedSet *currentlyHoveredViews = + [_currentlyHoveredViewsPerPointer objectForKey:@(pointerId)]; + if (currentlyHoveredViews == nil) { + currentlyHoveredViews = [NSOrderedSet orderedSet]; + } + + RCTReactTaggedView *targetTaggedView = [RCTReactTaggedView wrap:targetView]; + RCTReactTaggedView *prevTargetTaggedView = [currentlyHoveredViews firstObject]; + RCTUIView *prevTargetView = prevTargetTaggedView.view; // [macOS] + + NSOrderedSet *eventPathViews = GetTouchableViewsInPathToRoot(targetView); + + // Out + if (prevTargetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) { + BOOL shouldEmitOutEvent = IsAnyViewInPathListeningToEvent(currentlyHoveredViews, ViewEvents::Offset::PointerOut); + SharedTouchEventEmitter eventEmitter = + GetTouchEmitterFromView(prevTargetView, [_rootComponentView convertPoint:clientLocation toView:prevTargetView]); + if (shouldEmitOutEvent && eventEmitter != nil) { + eventEmitter->onPointerOut(event); + } + } + + // Leaving + + // pointerleave events need to be emited from the deepest target to the root but + // we also need to efficiently keep track of if a view has a parent which is listening to the leave events, + // so we first iterate from the root to the target, collecting the views which need events fired for, of which + // we reverse iterate (now from target to root), actually emitting the events. + NSMutableOrderedSet *viewsToEmitLeaveEventsTo = [NSMutableOrderedSet orderedSet]; // [macOS] + + BOOL hasParentLeaveListener = NO; + for (RCTReactTaggedView *taggedView in [currentlyHoveredViews reverseObjectEnumerator]) { + RCTUIView *componentView = taggedView.view; // [macOS] + + BOOL shouldEmitEvent = componentView != nil && + (hasParentLeaveListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerLeave)); + + if (shouldEmitEvent && ![eventPathViews containsObject:taggedView]) { + [viewsToEmitLeaveEventsTo addObject:componentView]; + } + + if (shouldEmitEvent && !hasParentLeaveListener) { + hasParentLeaveListener = YES; + } + } + + for (RCTUIView *componentView in [viewsToEmitLeaveEventsTo reverseObjectEnumerator]) { // [macOS] + SharedTouchEventEmitter eventEmitter = + GetTouchEmitterFromView(componentView, [_rootComponentView convertPoint:clientLocation toView:componentView]); + if (eventEmitter != nil) { + eventEmitter->onPointerLeave(event); + } + } + + // Over + if (targetView != nil && prevTargetTaggedView.tag != targetTaggedView.tag) { + BOOL shouldEmitOverEvent = IsAnyViewInPathListeningToEvent(eventPathViews, ViewEvents::Offset::PointerOver); + SharedTouchEventEmitter eventEmitter = + GetTouchEmitterFromView(targetView, [_rootComponentView convertPoint:clientLocation toView:targetView]); + if (shouldEmitOverEvent && eventEmitter != nil) { + eventEmitter->onPointerOver(event); + } + } + + // Entering + + // We only want to emit events to JS if there is a view that is currently listening to said event + // so we only send those event to the JS side if the element which has been entered is itself listening, + // or if one of its parents is listening in case those listeners care about the capturing phase. Adding the ability + // for native to distingusih between capturing listeners and not could be an optimization to futher reduce the number + // of events we send to JS + BOOL hasParentEnterListener = NO; + for (RCTReactTaggedView *taggedView in [eventPathViews reverseObjectEnumerator]) { + RCTUIView *componentView = taggedView.view; // [macOS] + + BOOL shouldEmitEvent = componentView != nil && + (hasParentEnterListener || IsViewListeningToEvent(taggedView, ViewEvents::Offset::PointerEnter)); + + if (shouldEmitEvent && ![currentlyHoveredViews containsObject:taggedView]) { + SharedTouchEventEmitter eventEmitter = + GetTouchEmitterFromView(componentView, [_rootComponentView convertPoint:clientLocation toView:componentView]); + if (eventEmitter != nil) { + eventEmitter->onPointerEnter(event); + } + } + + if (shouldEmitEvent && !hasParentEnterListener) { + hasParentEnterListener = YES; + } + } + + [_currentlyHoveredViewsPerPointer setObject:eventPathViews forKey:@(pointerId)]; + + return eventPathViews; +} +#endif + @end diff --git a/packages/react-native/React/Fabric/Utils/RCTLinearGradient.h b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.h index 5dd3b121fc3394..f5725264a80674 100644 --- a/packages/react-native/React/Fabric/Utils/RCTLinearGradient.h +++ b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.h @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import +#import // [macOS] #import NS_ASSUME_NONNULL_BEGIN diff --git a/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm index d73f505263a56e..efbedf6a318d8d 100644 --- a/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm +++ b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm @@ -7,10 +7,7 @@ #import "RCTLinearGradient.h" -#import #import -#include -#import using namespace facebook::react; @@ -18,9 +15,25 @@ @implementation RCTLinearGradient + (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const LinearGradient &)gradient { - RCTUIGraphicsImageRenderer *renderer = [[RCTUIGraphicsImageRenderer alloc] initWithSize:size]; // [macOS] +#if !TARGET_OS_OSX // macos does not support linear gradients + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size]; const auto &direction = gradient.direction; - UIImage *gradientImage = [renderer imageWithActions:^(RCTUIGraphicsImageRendererContext *_Nonnull rendererContext) { // [macOS] + const auto &colorStops = gradient.colorStops; + + UIImage *gradientImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { + CGContextRef context = rendererContext.CGContext; + NSMutableArray *colors = [NSMutableArray array]; + CGFloat locations[colorStops.size()]; + + for (size_t i = 0; i < colorStops.size(); ++i) { + const auto &colorStop = colorStops[i]; + CGColorRef cgColor = RCTCreateCGColorRefFromSharedColor(colorStop.color); + [colors addObject:(__bridge id)cgColor]; + locations[i] = colorStop.position; + } + + CGGradientRef cgGradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations); + CGPoint startPoint; CGPoint endPoint; @@ -37,25 +50,6 @@ + (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const LinearGradient & endPoint = CGPointMake(0.0, size.height); } - CGFloat dx = endPoint.x - startPoint.x; - CGFloat dy = endPoint.y - startPoint.y; - CGFloat gradientLineLength = sqrt(dx * dx + dy * dy); - const auto processedStops = getFixedColorStops(gradient.colorStops, gradientLineLength); - const auto colorStops = processColorTransitionHints(processedStops); - - CGContextRef context = rendererContext.CGContext; - NSMutableArray *colors = [NSMutableArray array]; - CGFloat locations[colorStops.size()]; - - for (size_t i = 0; i < colorStops.size(); ++i) { - const auto &colorStop = colorStops[i]; - CGColorRef cgColor = RCTCreateCGColorRefFromSharedColor(colorStop.color); - [colors addObject:(__bridge id)cgColor]; - locations[i] = std::max(std::min(colorStop.position.value(), 1.0), 0.0); - } - - CGGradientRef cgGradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations); - CGContextDrawLinearGradient(context, cgGradient, startPoint, endPoint, 0); for (id color in colors) { @@ -63,13 +57,12 @@ + (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const LinearGradient & } CGGradientRelease(cgGradient); }]; +#endif CALayer *gradientLayer = [CALayer layer]; -#if !TARGET_OS_OSX // [macOS] +#if !TARGET_OS_OSX // macos does not support linear gradients gradientLayer.contents = (__bridge id)gradientImage.CGImage; -#else // [macOS - gradientLayer.contents = (__bridge id)UIImageGetCGImageRef(gradientImage); -#endif // macOS] +#endif return gradientLayer; } diff --git a/packages/react-native/React/Views/RCTCursor.h b/packages/react-native/React/Views/RCTCursor.h index 2afacddbef70c4..48a6412bc8d363 100644 --- a/packages/react-native/React/Views/RCTCursor.h +++ b/packages/react-native/React/Views/RCTCursor.h @@ -7,6 +7,9 @@ #import #import // [macOS] +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] typedef NS_ENUM(NSInteger, RCTCursor) { // [macOS @@ -53,4 +56,3 @@ typedef NS_ENUM(NSInteger, RCTCursor) { #if TARGET_OS_OSX // [macOS RCT_EXTERN NSCursor *__nullable NSCursorFromRCTCursor(RCTCursor cursor); #endif // macOS] - diff --git a/packages/react-native/React/Views/RCTFont.mm b/packages/react-native/React/Views/RCTFont.mm index 37c13eb9ef5389..2e00bfd116b96e 100644 --- a/packages/react-native/React/Views/RCTFont.mm +++ b/packages/react-native/React/Views/RCTFont.mm @@ -143,12 +143,6 @@ struct __attribute__((__packed__)) CacheKey { if (defaultFontHandler) { NSString *fontWeightDescription = FontWeightDescriptionFromUIFontWeight(weight); font = defaultFontHandler(size, fontWeightDescription); -#pragma clang diagnostic push // [macOS] -#pragma clang diagnostic ignored "-Wunguarded-availability" // [macOS] - } else if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) { - // Only supported on iOS8.2/macOS10.11 and above - font = [UIFont systemFontOfSize:size weight:weight]; -#pragma clang diagnostic pop // [macOS] } else { font = [UIFont systemFontOfSize:size weight:weight]; } @@ -472,11 +466,8 @@ + (UIFont *)updateFont:(UIFont *)font } else { // Not a valid font or family RCTLogInfo(@"Unrecognized font family '%@'", familyName); -#pragma clang diagnostic push // [macOS] -#pragma clang diagnostic ignored "-Wunguarded-availability" // [macOS] if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) { font = [UIFont systemFontOfSize:fontSize weight:fontWeight]; -#pragma clang diagnostic pop // [macOS] } else if (fontWeight > UIFontWeightRegular) { font = [UIFont boldSystemFontOfSize:fontSize]; } else { diff --git a/packages/react-native/React/Views/RCTView.h b/packages/react-native/React/Views/RCTView.h index 8c346c2d0f757b..fab2ec1f449d3e 100644 --- a/packages/react-native/React/Views/RCTView.h +++ b/packages/react-native/React/Views/RCTView.h @@ -174,6 +174,7 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @property (nonatomic, copy) RCTDirectEventBlock onDragEnter; @property (nonatomic, copy) RCTDirectEventBlock onDragLeave; @property (nonatomic, copy) RCTDirectEventBlock onDrop; +@property (nonatomic, copy) RCTDirectEventBlock onDoubleClick; // Keyboarding events // NOTE does not properly work with single line text inputs (most key downs). This is because those are diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index 8d4353d8d5b131..39160d3acba695 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -197,6 +197,20 @@ - (instancetype)initWithFrame:(CGRect)frame RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : unused) +#if TARGET_OS_OSX // [macOS +- (void)setFrame:(NSRect)frame +{ + [super setFrame:frame]; + + // On macOS Sonoma, assigning the tool tip will set up a tracking rect based on the view's current frame. Since the + // tool tip can be assigned before it was layed out, the tracking rect would stay at NSZeroRect. We fix this by + // clearing and reassigning the tool tip to force update the internally created tracking rect with the new frame. + NSString *toolTip = self.toolTip; + self.toolTip = nil; + self.toolTip = toolTip; +} +#endif // macOS] + - (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection { if (_reactLayoutDirection != layoutDirection) { @@ -1222,7 +1236,7 @@ - (void)displayLayer:(CALayer *)layer #else // [macOS const RCTBorderColors borderColors = [self borderColors]; #endif // macOS] - BOOL useIOSBorderRendering = + BOOL useIOSBorderRendering = RCTCornerRadiiAreEqualAndSymmetrical(cornerRadii) && RCTBorderInsetsAreEqual(borderInsets) && RCTBorderColorsAreEqual(borderColors) && @@ -1501,9 +1515,11 @@ - (void)resetCursorRects } } +#pragma mark - Responder Chain + - (BOOL)needsPanelToBecomeKey { - // We need to override this so that mouse clicks don't move keyboard focus on focusable views by default. + // We need to override this so that mouse clicks don't move keyboard focus on focusable views by default. return false; } @@ -1552,6 +1568,15 @@ - (void)mouseExited:(NSEvent *)event additionalData:nil]; } +- (void)mouseUp:(NSEvent *)event +{ + if (_onDoubleClick && event.clickCount == 2){ + _onDoubleClick(nil); + } else { + [super mouseUp:event]; + } +} + - (BOOL)mouseDownCanMoveWindow { return _mouseDownCanMoveWindow; @@ -1625,7 +1650,7 @@ - (NSDictionary*)dataTransferInfoFromPasteboard:(NSPasteboard*)pasteboard 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; diff --git a/packages/react-native/React/Views/RCTViewManager.m b/packages/react-native/React/Views/RCTViewManager.m index 1181a95ad97374..fcaa1cbf95ea42 100644 --- a/packages/react-native/React/Views/RCTViewManager.m +++ b/packages/react-native/React/Views/RCTViewManager.m @@ -254,7 +254,7 @@ - (RCTShadowView *)shadowView RCT_REMAP_VIEW_PROPERTY(shadowOpacity, layer.shadowOpacity, float) RCT_REMAP_VIEW_PROPERTY(shadowRadius, layer.shadowRadius, CGFloat) #else // [macOS -RCT_EXPORT_VIEW_PROPERTY(shadowColor, CGColor) +RCT_EXPORT_VIEW_PROPERTY(shadowColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(shadowOffset, CGSize) RCT_EXPORT_VIEW_PROPERTY(shadowOpacity, CGFloat) RCT_EXPORT_VIEW_PROPERTY(shadowRadius, CGFloat) @@ -335,7 +335,7 @@ - (void)updateAccessibilityTraitsForRole:(RCTView *)view withDefaultView:(RCTVie view.reactAccessibilityElement.accessibilityTraits |= view.reactAccessibilityElement.role ? view.reactAccessibilityElement.roleTraits : view.reactAccessibilityElement.accessibilityRole ? view.reactAccessibilityElement.accessibilityRoleTraits - : (defaultView.accessibilityTraits & AccessibilityRolesMask); + : (defaultView.accessibilityTraits & AccessibilityRolesMask); } #else // [macOS - (void) updateAccessibilityRole:(RCTView *)view withDefaultView:(RCTView *)defaultView @@ -638,9 +638,7 @@ - (void) updateAccessibilityRole:(RCTView *)view withDefaultView:(RCTView *)defa #pragma mark - macOS properties -RCT_EXPORT_VIEW_PROPERTY(onFocus, RCTBubblingEventBlock) -RCT_EXPORT_VIEW_PROPERTY(onBlur, RCTBubblingEventBlock) - +RCT_EXPORT_VIEW_PROPERTY(onDoubleClick, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMouseEnter, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMouseLeave, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onDragEnter, RCTDirectEventBlock) diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollContentView.m b/packages/react-native/React/Views/ScrollView/RCTScrollContentView.m index 134d7217f47450..7853b09e81f76d 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollContentView.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollContentView.m @@ -50,16 +50,18 @@ - (void)reactSetFrame:(CGRect)frame // In such cases the content view layout must shrink accordingly otherwise // the contents will overflow causing the scroll indicators to appear unnecessarily. NSScrollView *platformScrollView = [scrollView scrollView]; + + CGFloat horizontalScrollerHeight = 0; + CGFloat verticalScrollerWidth = 0; if ([platformScrollView scrollerStyle] == NSScrollerStyleLegacy) { BOOL contentHasHeight = platformScrollView.contentSize.height > 0; - CGFloat horizontalScrollerHeight = ([platformScrollView hasHorizontalScroller] && contentHasHeight) ? NSHeight([[platformScrollView horizontalScroller] frame]) : 0; - CGFloat verticalScrollerWidth = [platformScrollView hasVerticalScroller] ? NSWidth([[platformScrollView verticalScroller] frame]) : 0; - - RCTScrollContentLocalData *localData = [[RCTScrollContentLocalData alloc] initWithVerticalScrollerWidth:verticalScrollerWidth horizontalScrollerHeight:horizontalScrollerHeight]; - - [[[scrollView bridge] uiManager] setLocalData:localData forView:self]; + horizontalScrollerHeight = ([platformScrollView hasHorizontalScroller] && contentHasHeight) ? NSHeight([[platformScrollView horizontalScroller] frame]) : 0; + verticalScrollerWidth = [platformScrollView hasVerticalScroller] ? NSWidth([[platformScrollView verticalScroller] frame]) : 0; } + RCTScrollContentLocalData *localData = [[RCTScrollContentLocalData alloc] initWithVerticalScrollerWidth:verticalScrollerWidth horizontalScrollerHeight:horizontalScrollerHeight]; + [[[scrollView bridge] uiManager] setLocalData:localData forView:self]; + if ([platformScrollView accessibilityRole] == NSAccessibilityTableRole) { NSMutableArray *subViews = [[NSMutableArray alloc] initWithCapacity:[[self subviews] count]]; for (NSView *view in [self subviews]) { diff --git a/packages/react-native/React/Views/ScrollView/RCTScrollView.m b/packages/react-native/React/Views/ScrollView/RCTScrollView.m index 9d66522a198101..d351073884c978 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollView.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollView.m @@ -328,6 +328,7 @@ @implementation RCTScrollView { BOOL _allowNextScrollNoMatterWhat; #if TARGET_OS_OSX // [macOS BOOL _notifyDidScroll; + BOOL _disableScrollEvents; NSPoint _lastScrollPosition; #endif // macOS] CGRect _lastClippedToRect; @@ -460,6 +461,7 @@ - (instancetype)initWithEventDispatcher:(id)eventDis #else // [macOS _scrollView.postsBoundsChangedNotifications = YES; _lastScrollPosition = NSZeroPoint; + _hasOverlayStyleIndicator = NO; #endif // macOS] #if !TARGET_OS_OSX // [macOS] @@ -526,7 +528,7 @@ - (void)setAccessibilityRole:(NSAccessibilityRole)accessibilityRole - (void)setInverted:(BOOL)inverted { BOOL changed = _inverted != inverted; - _inverted = inverted; + _inverted = inverted; if (changed && _onInvertedDidChange) { _onInvertedDidChange(@{}); } @@ -536,8 +538,10 @@ - (void)setHasOverlayStyleIndicator:(BOOL)hasOverlayStyle { if (hasOverlayStyle == true) { self.scrollView.scrollerStyle = NSScrollerStyleOverlay; + _hasOverlayStyleIndicator = YES; } else { self.scrollView.scrollerStyle = NSScrollerStyleLegacy; + _hasOverlayStyleIndicator = NO; } } #endif // macOS] @@ -570,7 +574,18 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews - (void)setFrame:(CGRect)frame { + /** [macOS + * Setting the frame on the scroll view will randomly generate between 0 and 4 scroll events. These events happen + * during the layout phase of the view which generates layout notifications that are sent through the bridge. + * Because the bridge is heavily used, the scroll events are throttled and reach the JS thread with a random delay. + * Because the scroll event stores the clip and content view size, delayed scroll events will submit stale layout + * information that can break virtual list implemenations. + * By disabling scroll events during the execution of the setFrame method and scheduling one notification on + * the next run loop, we can mitigate the delayed scroll event by sending it at a time where the bridge is not busy. + */ + _disableScrollEvents = YES; // macOS] [super setFrame:frame]; + _disableScrollEvents = NO; // [macOS] [self centerContentIfNeeded]; } @@ -867,6 +882,10 @@ - (void)flashScrollIndicators #if TARGET_OS_OSX // [macOS - (void)scrollViewDocumentViewBoundsDidChange:(__unused NSNotification *)notification { + if (_disableScrollEvents) { + return; + } + if (_scrollView.centerContent) { // contentOffset setter dynamically centers content when _centerContent == YES [_scrollView setContentOffset:_scrollView.contentOffset]; @@ -956,11 +975,11 @@ - (void)scrollViewDidScroll:(RCTCustomScrollView *)scrollView // [macOS] { NSTimeInterval now = CACurrentMediaTime(); [self updateClippedSubviews]; - + #if TARGET_OS_OSX // [macOS /** * To check for effective scroll position changes, the comparison with lastScrollPosition should happen - * after updateClippedSubviews. updateClippedSubviews will update the display of the vertical/horizontal + * after updateClippedSubviews. updateClippedSubviews will update the display of the vertical/horizontal * scrollers which can change the clipview bounds. * This change also ensures that no onScroll events are sent when the React setFrame call is running, * which could submit onScroll events while the content view was not setup yet. @@ -971,7 +990,7 @@ - (void)scrollViewDidScroll:(RCTCustomScrollView *)scrollView // [macOS] } _lastScrollPosition = scrollView.contentView.bounds.origin; #endif // macOS] - + /** * TODO: this logic looks wrong, and it may be because it is. Currently, if _scrollEventThrottle * is set to zero (the default), the "didScroll" event is only sent once per scroll, instead of repeatedly @@ -1282,14 +1301,30 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager #endif // macOS] BOOL hasNewView = NO; if (horz) { +#if !TARGET_OS_OSX // [macOS] CGFloat leftInset = self.inverted ? self->_scrollView.contentInset.right : self->_scrollView.contentInset.left; +#else // [macOS + CGFloat leftInset = self.inverted ? self->_scrollView.contentInset.right : self->_scrollView.contentInset.left; +#endif // macOS] CGFloat x = self->_scrollView.contentOffset.x + leftInset; +#if !TARGET_OS_OSX // [macOS] hasNewView = subview.frame.origin.x + subview.frame.size.width > x; +#else // [macOS + hasNewView = subview.frame.origin.x + subview.frame.size.width >= x; +#endif // macOS] } else { CGFloat bottomInset = +#if !TARGET_OS_OSX // [macOS] self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom; +#else // [macOS + self.inverted ? self->_scrollView.contentInset.bottom : self->_scrollView.contentInset.top; +#endif // macOS] CGFloat y = self->_scrollView.contentOffset.y + bottomInset; +#if !TARGET_OS_OSX // [macOS] hasNewView = subview.frame.origin.y + subview.frame.size.height > y; +#else // [macOS + hasNewView = subview.frame.origin.y + subview.frame.size.height >= y; +#endif // macOS] } #if !TARGET_OS_OSX // [macOS] if (hasNewView || ii == self->_contentView.subviews.count - 1) { @@ -1372,7 +1407,7 @@ - (BOOL)handleKeyboardEvent:(NSEvent *)event { - (void)keyDown:(NSEvent *)event { if (![self handleKeyboardEvent:event]) { [super keyDown:event]; - + // AX: if a tab key was pressed and the first responder is currently clipped by the scroll view, // automatically scroll to make the view visible to make it navigable via keyboard. NSString *key = [RCTViewKeyboardEvent keyFromEvent:event]; @@ -1405,6 +1440,10 @@ - (void)keyUp:(NSEvent *)event { } - (void)preferredScrollerStyleDidChange:(__unused NSNotification *)notification { + if (_hasOverlayStyleIndicator == YES) { + self.scrollView.scrollerStyle = NSScrollerStyleOverlay; + } + RCT_SEND_SCROLL_EVENT(onPreferredScrollerStyleDidChange, (@{ @"preferredScrollerStyle": RCTStringForScrollerStyle([NSScroller preferredScrollerStyle])})); } #endif // macOS] 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 92a13cf4c157cc..3a04912afbf6ef 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 @@ -8,9 +8,10 @@ #pragma once #include +#include // [macOS] #include #include -#include +#include // [macOS] 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 index ef3b407272250e..11ef6cf03c4d53 100644 --- 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 @@ -12,5 +12,65 @@ #include namespace facebook::react { -using HostPlatformTouch = BaseTouch; + +class HostPlatformTouch : public BaseTouch { + public: + /* + * The button indicating which pointer is used. + */ + int button; + + /* + * The pointer type indicating the device type (e.g., mouse, pen, touch) + */ + std::string pointerType; + + /* + * A flag indicating if the alt key is pressed. + */ + bool altKey; + + /* + * A flag indicating if the control key is pressed. + */ + bool ctrlKey; + + /* + * A flag indicating if the shift key is pressed. + */ + bool shiftKey; + + /* + * A flag indicating if the shift key is pressed. + */ + bool metaKey; + + /* + * Windows-specific timestamp field. We can't use the shared BaseTouch + * timestamp field beacuse it's a float and lacks sufficient resolution. + */ + double pointerTimestamp; +}; + +inline static void setTouchPayloadOnObject( + jsi::Object& object, + jsi::Runtime& runtime, + const HostPlatformTouch& touch) { + object.setProperty(runtime, "locationX", touch.offsetPoint.x); + object.setProperty(runtime, "locationY", touch.offsetPoint.y); + object.setProperty(runtime, "pageX", touch.pagePoint.x); + object.setProperty(runtime, "pageY", touch.pagePoint.y); + object.setProperty(runtime, "screenX", touch.screenPoint.x); + object.setProperty(runtime, "screenY", touch.screenPoint.y); + object.setProperty(runtime, "identifier", touch.identifier); + object.setProperty(runtime, "target", touch.target); + object.setProperty(runtime, "timestamp", touch.pointerTimestamp); + object.setProperty(runtime, "force", touch.force); + object.setProperty(runtime, "button", touch.button); + object.setProperty(runtime, "altKey", touch.altKey); + object.setProperty(runtime, "ctrlKey", touch.ctrlKey); + object.setProperty(runtime, "shiftKey", touch.shiftKey); + object.setProperty(runtime, "metaKey", touch.metaKey); +}; + } // 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 index eb0ea08daa61d2..e76878c9a768cd 100644 --- 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 @@ -67,20 +67,27 @@ static jsi::Object mouseEventPayload(jsi::Runtime& runtime, const MouseEvent& ev }; void HostPlatformViewEventEmitter::onMouseEnter(const MouseEvent& mouseEvent) const { - dispatchEvent("mouseEnter", [mouseEvent](jsi::Runtime &runtime) { - return mouseEventPayload(runtime, mouseEvent); + dispatchEvent("mouseEnter", [mouseEvent](jsi::Runtime& runtime) { + return mouseEventPayload(runtime, mouseEvent); }); } void HostPlatformViewEventEmitter::onMouseLeave(const MouseEvent& mouseEvent) const { - dispatchEvent("mouseLeave", [mouseEvent](jsi::Runtime &runtime) { - return mouseEventPayload(runtime, mouseEvent); + dispatchEvent("mouseLeave", [mouseEvent](jsi::Runtime& runtime) { + return mouseEventPayload(runtime, mouseEvent); + }); +} + +void HostPlatformViewEventEmitter::onDoubleClick( + const MouseEvent& mouseEvent) const { + dispatchEvent("doubleClick", [mouseEvent](jsi::Runtime& runtime) { + return mouseEventPayload(runtime, mouseEvent); }); } #pragma mark - Drag and Drop Events -jsi::Value HostPlatformViewEventEmitter::dataTransferPayload( +static jsi::Value dataTransferPayload( jsi::Runtime& runtime, DataTransfer const& dataTransfer) { const auto& files = dataTransfer.files; @@ -163,27 +170,25 @@ static jsi::Value dragEventPayload( jsi::Runtime& runtime, const DragEvent& event) { auto payload = mouseEventPayload(runtime, event); - auto dataTransferObject = HostPlatformViewEventEmitter::dataTransferPayload( - runtime, - event.dataTransfer); + auto dataTransferObject = dataTransferPayload(runtime, event.dataTransferItems); payload.setProperty(runtime, "dataTransfer", dataTransferObject); return payload; } -void HostPlatformViewEventEmitter::onDragEnter(DragEvent const& dragEvent) const { - dispatchEvent("dragEnter", [dragEvent](jsi::Runtime &runtime) { +void HostPlatformViewEventEmitter::onDragEnter(const DragEvent& dragEvent) const { + dispatchEvent("dragEnter", [dragEvent](jsi::Runtime& runtime) { return dragEventPayload(runtime, dragEvent); }); } -void HostPlatformViewEventEmitter::onDragLeave(DragEvent const& dragEvent) const { - dispatchEvent("dragLeave", [dragEvent](jsi::Runtime &runtime) { +void HostPlatformViewEventEmitter::onDragLeave(const DragEvent& dragEvent) const { + dispatchEvent("dragLeave", [dragEvent](jsi::Runtime& runtime) { return dragEventPayload(runtime, dragEvent); }); } -void HostPlatformViewEventEmitter::onDrop(DragEvent const& dragEvent) const { - dispatchEvent("drop", [dragEvent](jsi::Runtime &runtime) { +void HostPlatformViewEventEmitter::onDrop(const DragEvent& dragEvent) const { + dispatchEvent("drop", [dragEvent](jsi::Runtime& runtime) { return dragEventPayload(runtime, dragEvent); }); } 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 index 0fd77c573f38d0..e3a4927cacc722 100644 --- 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 @@ -26,24 +26,23 @@ class HostPlatformViewEventEmitter : public BaseViewEventEmitter { #pragma mark - Keyboard Events - void onKeyDown(KeyEvent const& keyEvent) const; - void onKeyUp(KeyEvent const& keyEvent) const; + void onKeyDown(const KeyEvent& keyEvent) const; + void onKeyUp(const KeyEvent& keyEvent) const; #pragma mark - Mouse Events - void onMouseEnter(MouseEvent const& mouseEvent) const; - void onMouseLeave(MouseEvent const& mouseEvent) const; + void onMouseEnter(const MouseEvent& mouseEvent) const; + void onMouseLeave(const MouseEvent& mouseEvent) const; + void onDoubleClick(const MouseEvent& 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; - + void onDragEnter(const DragEvent& dragEvent) const; + void onDragLeave(const DragEvent& dragEvent) const; + void onDrop(const DragEvent& dragEvent) const; static jsi::Value dataTransferPayload( jsi::Runtime& runtime, - DataTransfer const& dataTransfer); - + const DataTransfer& dataTransfer); }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h index 26b7cb9bd9512e..2a3fc7c3b837ed 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h @@ -1,5 +1,5 @@ /* - * Copyright (c) Meta Platforms, Inc. and affiliates. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. @@ -9,8 +9,10 @@ #include #include - +#include #include +#include +#include namespace facebook::react { @@ -29,48 +31,66 @@ struct HostPlatformViewEvents { // Mouse Events MouseEnter = 4, MouseLeave = 5, + DoubleClick = 6, }; constexpr bool operator[](const Offset offset) const { - return bits[static_cast(offset)]; + return bits[static_cast(offset)]; } std::bitset<64>::reference operator[](const Offset offset) { - return bits[static_cast(offset)]; + return bits[static_cast(offset)]; } }; -inline static bool operator==(HostPlatformViewEvents const &lhs, HostPlatformViewEvents const &rhs) { +inline static bool operator==(const HostPlatformViewEvents& lhs, const HostPlatformViewEvents& rhs) { return lhs.bits == rhs.bits; } -inline static bool operator!=(HostPlatformViewEvents const &lhs, HostPlatformViewEvents const &rhs) { +inline static bool operator!=(const HostPlatformViewEvents& lhs, const HostPlatformViewEvents& rhs) { return lhs.bits != rhs.bits; } static inline HostPlatformViewEvents convertRawProp( - const PropsParserContext &context, - const RawProps &rawProps, - const HostPlatformViewEvents &sourceValue, - const HostPlatformViewEvents &defaultValue) { + const PropsParserContext& context, + const RawProps& rawProps, + const HostPlatformViewEvents& sourceValue, + const HostPlatformViewEvents& defaultValue) { HostPlatformViewEvents result{}; using Offset = HostPlatformViewEvents::Offset; - // Focus Events - result[Offset::Focus] = - convertRawProp(context, rawProps, "onFocus", sourceValue[Offset::Focus], defaultValue[Offset::Focus]); - result[Offset::Blur] = - convertRawProp(context, rawProps, "onBlur", sourceValue[Offset::Blur], defaultValue[Offset::Blur]); - // Keyboard 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::KeyDown] = convertRawProp( + context, + rawProps, + "onKeyDown", + sourceValue[Offset::KeyDown], + defaultValue[Offset::KeyDown]); + result[Offset::KeyUp] = convertRawProp( + context, + rawProps, + "onKeyUp", + sourceValue[Offset::KeyUp], + defaultValue[Offset::KeyUp]); + + 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; } 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 index 2ef2fdd7dfb313..3b261ec43cc3ab 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) Meta Platforms, Inc. and affiliates. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. @@ -26,7 +26,7 @@ HostPlatformViewProps::HostPlatformViewProps( ReactNativeFeatureFlags::enableCppPropsIteratorSetter() ? sourceProps.hostPlatformEvents : convertRawProp( - context, + context, rawProps, sourceProps.hostPlatformEvents, {})), @@ -106,9 +106,9 @@ void HostPlatformViewProps::setProp( // 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(Focus); VIEW_EVENT_CASE_MACOS(Blur); @@ -116,6 +116,7 @@ void HostPlatformViewProps::setProp( VIEW_EVENT_CASE_MACOS(KeyUp); VIEW_EVENT_CASE_MACOS(MouseEnter); VIEW_EVENT_CASE_MACOS(MouseLeave); + VIEW_EVENT_CASE_MACOS(DoubleClick); RAW_SET_PROP_SWITCH_CASE_BASIC(focusable); RAW_SET_PROP_SWITCH_CASE_BASIC(enableFocusRing); RAW_SET_PROP_SWITCH_CASE_BASIC(keyDownEvents); @@ -125,5 +126,4 @@ void HostPlatformViewProps::setProp( } } - } // 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 index 74d4f57ad74c4d..c9b5e0023d4ccd 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) Meta Platforms, Inc. and affiliates. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. @@ -13,13 +13,15 @@ namespace facebook::react::HostPlatformViewTraitsInitializer { inline bool formsStackingContext(const ViewProps& props) { - return false; + return false; } inline bool formsView(const ViewProps& props) { return props.focusable || + props.tooltip || props.hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] || - props.hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave]; + props.hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave] || + props.hostPlatformEvents[HostPlatformViewEvents::Offset::DoubleClick]; } } // namespace facebook::react::HostPlatformViewTraitsInitializer 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 index 334506c93adc6e..bf6df30f5934bd 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) Meta Platforms, Inc. and affiliates. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. @@ -7,7 +7,7 @@ #pragma once -#include +#include namespace facebook::react { @@ -19,7 +19,7 @@ struct MouseEvent { * Pointer horizontal location in target view. */ Float clientX{0}; - + /** * Pointer vertical location in target view. */ @@ -29,7 +29,7 @@ struct MouseEvent { * Pointer horizontal location in window. */ Float screenX{0}; - + /** * Pointer vertical location in window. */ diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm index 069af91004855f..5dd122a61f40e8 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm @@ -78,27 +78,30 @@ bool UIColorIsP3ColorSpace(const std::shared_ptr &uiColor) }]; return color; #else // [macOS - NSColor *color = [NSColor colorWithName:nil dynamicProvider:^NSColor * _Nonnull(NSAppearance * _Nonnull appearance) { - NSMutableArray *appearances = [NSMutableArray arrayWithArray:@[NSAppearanceNameAqua,NSAppearanceNameDarkAqua]]; - if (highContrastLightColor != nil) { - [appearances addObject:NSAppearanceNameAccessibilityHighContrastAqua]; - } - if (highContrastDarkColor != nil) { - [appearances addObject:NSAppearanceNameAccessibilityHighContrastDarkAqua]; - } - NSAppearanceName bestMatchingAppearance = [appearance bestMatchFromAppearancesWithNames:appearances]; - if (bestMatchingAppearance == NSAppearanceNameAqua) { - return lightColor; - } else if (bestMatchingAppearance == NSAppearanceNameDarkAqua) { - return darkColor; - } else if (bestMatchingAppearance == NSAppearanceNameAccessibilityHighContrastAqua) { - return highContrastLightColor; - } else if (bestMatchingAppearance == NSAppearanceNameAccessibilityHighContrastDarkAqua) { - return highContrastDarkColor; - } else { - return lightColor; - } - }]; + NSColor *color = [NSColor colorWithName:nil + dynamicProvider:^NSColor *_Nonnull(NSAppearance *_Nonnull appearance) { + NSMutableArray *appearances = + [NSMutableArray arrayWithArray:@[ NSAppearanceNameAqua, NSAppearanceNameDarkAqua ]]; + if (highContrastLightColor != nil) { + [appearances addObject:NSAppearanceNameAccessibilityHighContrastAqua]; + } + if (highContrastDarkColor != nil) { + [appearances addObject:NSAppearanceNameAccessibilityHighContrastDarkAqua]; + } + NSAppearanceName bestMatchingAppearance = + [appearance bestMatchFromAppearancesWithNames:appearances]; + if (bestMatchingAppearance == NSAppearanceNameAqua) { + return lightColor; + } else if (bestMatchingAppearance == NSAppearanceNameDarkAqua) { + return darkColor; + } else if (bestMatchingAppearance == NSAppearanceNameAccessibilityHighContrastAqua) { + return highContrastLightColor; + } else if (bestMatchingAppearance == NSAppearanceNameAccessibilityHighContrastDarkAqua) { + return highContrastDarkColor; + } else { + return lightColor; + } + }]; return color; #endif // macOS] } else { @@ -121,7 +124,15 @@ int32_t ColorFromUIColor(RCTUIColor *color) // [macOS] { CGFloat rgba[4]; [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; - return ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); +#else // [macOS + // [NSColor getRed:green:blue:alpha]` wil throw an exception if the colorspace is not SRGB, + [[color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]] getRed:&rgba[0] + green:&rgba[1] + blue:&rgba[2] + alpha:&rgba[3]]; +#endif // macOS] + return ((int32_t)round((float)rgba[3] * ratio) & 0xff) << 24 | ((int)round((float)rgba[0] * ratio) & 0xff) << 16 | + ((int)round((float)rgba[1] * ratio) & 0xff) << 8 | ((int)round((float)rgba[2] * ratio) & 0xff); } #if !TARGET_OS_OSX // [macOS] @@ -160,8 +171,10 @@ int32_t ColorFromUIColor(const std::shared_ptr &uiColor) } else { uiColor = [RCTUIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha]; // [macOS] } - - return uiColor; + return [RCTUIColor colorWithRed:components.red + green:components.green + blue:components.blue + alpha:components.alpha]; // [macOS] } int32_t hashFromUIColor(const std::shared_ptr &uiColor) diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm index 019fe25a6ab79e..c5b4c6347e0111 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm @@ -104,6 +104,7 @@ inline static UIFontTextStyle RCTUIFontTextStyleForDynamicTypeRamp(const Dynamic } #endif // [macOS] +#if !TARGET_OS_OSX // [macOS] inline static CGFloat RCTBaseSizeForDynamicTypeRamp(const DynamicTypeRamp &dynamicTypeRamp) { // Values taken from @@ -133,6 +134,7 @@ inline static CGFloat RCTBaseSizeForDynamicTypeRamp(const DynamicTypeRamp &dynam return 34.0; } } +#endif // [macOS] inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const TextAttributes &textAttributes) { diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm index c6b0a6cc410bbc..b6d0397376da58 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTInstance.mm @@ -551,10 +551,10 @@ - (void)_handleJSError:(const JsErrorHandler::ProcessedError &)error withRuntime name:errorData[@"name"] componentStack:errorData[@"componentStack"] exceptionId:error.id -#if TARGET_OS_OSX // [macOS Use boolValue for isFatal to ensure type safety - isFatal:[errorData[@"isFatal"] boolValue] // Explicitly convert to BOOL to avoid compiler errors on macOS -#else - isFatal:errorData[@"isFatal"] +#if !TARGET_OS_OSX // [macOS Use boolValue for isFatal to ensure type safety // [macOS] + isFatal:errorData[@"isFatal"] +#else // [macOS + isFatal:[errorData[@"isFatal"] boolValue] // Explicitly convert to BOOL to avoid compiler errors on macOS #endif // macOS] extraData:errorData[@"extraData"]]) { JS::NativeExceptionsManager::ExceptionData jsErrorData{errorData}; diff --git a/packages/rn-tester/Podfile.lock b/packages/rn-tester/Podfile.lock index 0c498053a0f364..d43b171ba9f4d7 100644 --- a/packages/rn-tester/Podfile.lock +++ b/packages/rn-tester/Podfile.lock @@ -2053,7 +2053,7 @@ SPEC CHECKSUMS: boost: cea1d4f90a3a59537f3deb03ff5656489d7133dd DoubleConversion: d31b1eb37f6d6f456530c4fd9124b857d6889cab fast_float: 5596a99716f77fe44b617183d4db34777538174d - FBLazyVector: 0a3f4ff238f4a5a12df1ce799726fbae48b3219a + FBLazyVector: d988ba6879c9c21838d786f41957892b9c3c9f1a fmt: 24e7591456deb60b4a77518f83d9a916ac84223f glog: 0b31c25149b9d350b2666c7d459229861a00ec07 hermes-engine: cc8feeacabc5b95002c368a555a173cddafb29c5 @@ -2063,72 +2063,72 @@ SPEC CHECKSUMS: OSSLibraryExample: d57a46a4520f0964e36b3ac349cb37404a469ae3 RCT-Folly: 957ce397c08be7a9a91bb9245e57e69fa5255ec5 RCTDeprecation: 3808e36294137f9ee5668f4df2e73dc079cd1dcf - RCTRequired: 5c10a42787b18fabbacda1669d333270f78dfed1 - RCTTypeSafety: 04629d6e8ee80c4ddb1bc2acb80b1ca88de284c9 - React: ee7f072e1e2db3fd4da25483b3cb6868964aa7ed - React-callinvoker: 94acfd04af532e289b9e992fad6f71b0a1fb752f - React-Core: 7b58428f7a2c1a685713e3e14387a8f98da88e03 - React-CoreModules: 5f97a14ece13839935df5ae9456db172f966c1f4 - React-cxxreact: d152f08b17a34ae1ec345029cdc24919828bda88 - React-debug: bd53bf62a49fe5858a569204c3df64dc0a3c0e6a - React-defaultsnativemodule: a59c04097aff0751a08745ef6aaa217ab1178be7 - React-domnativemodule: 403a11dd566425a55d3bf58a13127a2c12c2afc7 - React-Fabric: 0c8a6ce4745ccdc43aec6a664e496218d8bdfc33 - React-FabricComponents: 4af2a2865dc407f5c2e75faa2734ba389461e4b4 - React-FabricImage: 7ca716a0ed4dcf506fcdcd60986501c527cfcc6e - React-featureflags: 8a8f76aa906abe8a561c2e81569d159509a7b244 - React-featureflagsnativemodule: f9330ce11a0cb7e1b51e30cc261add31294d93c2 - React-graphics: aa9108bb694c9ddb04eb0cd8b9bd7c0c09de83a2 - React-hermes: a10ae5b0156d75c05f4c737336cb593b2b27a2df - React-idlecallbacksnativemodule: c9fe93ba4f421319ad4bcd0d7131e0d3c510cbfb - React-ImageManager: cfbe01dd1d75ed9aa33c09c66e3e3f20970f4981 - React-jserrorhandler: 099f5a111a9117e5cb7be4a31a151e2977ec20ef - React-jsi: f06232f271ddafb13d90bfecda92a84d29bb0d36 - React-jsiexecutor: 445b0458ddf8920c995d96af05bc8d66b7fc138a - React-jsinspector: af8da9536a4e2402d65c47e01a64118628f067b4 - React-jsinspectortracing: eef865f1f8db567faac91bb857a880f7eab7e2d0 - React-jsitooling: 397f96aed700fd416ff6fe16ed88f4fa45c64ba2 - React-jsitracing: b149f1f31dda23f0ba72151b0024964a76741fa7 - React-logger: 51cdb2d4cb36b78f9f154dda2fdb1e7dba204765 - React-Mapbuffer: bb17a086220da31233ca695abe63ca01dec40d82 - React-microtasksnativemodule: ac413e56d6d81b77b9a1aae7ff1aad8afb6050c5 - React-NativeModulesApple: db1a001d34e7f0e8b25dfefa2a2ce83b6a6442d9 - React-oscompat: 825c3b67ca607307fc7493101bee3ec6c18b2ca7 - React-perflogger: ad75d71af0ddce6ea06d6d47eae3151563a01b55 - React-performancetimeline: 53df2b097c87eab5bb14ed00cab95be9f82e2f64 - React-RCTActionSheet: a9fc70ec6783733305602ffb0cb9ab956221230e - React-RCTAnimation: cc7bff4261b79f6fbb2df431bf6c312690f6c40c - React-RCTAppDelegate: bd84d753358c4103b81d6fe33c1e64c844615516 - React-RCTBlob: 80c0a82e1b20f05416636841f0f13cbe28c6df6d - React-RCTFabric: dd0221952b4cfafbe1d548e59272a48f5d7b233a - React-RCTFBReactNativeSpec: c3b76c04af6b9842ce62db58c670a756a45bdee1 - React-RCTImage: f810320175a60490085043f6aa65f4e91c4fd42d - React-RCTLinking: 1a4ed8deb97f91b641bbe31c223a80d3bfa040b2 - React-RCTNetwork: bd30579e9d1b20708525428d6f1a44c1adfdcf5f - React-RCTPushNotification: 85664072a6ed949dbc244440a60be24f201eee2b - React-RCTRuntime: e7a1a5d3c48065d96e351f20b521c3e1e01cd55f - React-RCTSettings: 65a4df3015d7fd9c6efc455f0df9de8a48001b88 - React-RCTTest: 0ee2333eb05005b100ce25749e3b3dfca40908f7 - React-RCTText: d633a9c03fca2a1a228a0ede5e340169bb851618 - React-RCTVibration: 0deb664da65c4302963a8c2d6f23995059341fea - React-rendererconsistency: 00316a8639306ac717d62d2874b885088cde31d4 - React-renderercss: f11c56e70856232f2d34ab96df245a674a2ba769 - React-rendererdebug: 12d895f5fae069c25aa0c33a138ddd3f308fd8f2 - React-rncore: a0871316bff44288ba62c1374b6e9093717dbc89 - React-RuntimeApple: 29d74569f8f83a2e98b73a532d063250a5170347 - React-RuntimeCore: 54235f3ab77822160c01fa7ab543f3ea512e56e6 - React-runtimeexecutor: a3ea35a56f73642ff5c888869e3e5eecd842f8c9 - React-RuntimeHermes: 9f0c0fd0b045d2ee552ad96044be15e76a1b7485 - React-runtimescheduler: 5d60a6ef3abdba3b0f688eacd3a106a45f09c334 - React-timing: ba0ea02d49e288be015576fc217184a521f3508d - React-utils: 3101cddb3c50406b3949fe36e8fad34609dd561c - ReactAppDependencyProvider: 4d000089a4f20b7df5b5849581808301af186297 + RCTRequired: 823011e8cd7db29504b2d7b357b4291dd7851014 + RCTTypeSafety: 03082806df846baa2274d2266a0555687a868b87 + React: e11e7dc273411afc18e8b78cf71ab84ef481f84e + React-callinvoker: 81deaea7bc1e1069423088fa2905bf02af4b0f72 + React-Core: 4df7012baa0ffee3c518dccc277c89a285c3757c + React-CoreModules: b8dba4cf245dcf96bc0ba89d5bcb42d3f4757ad7 + React-cxxreact: 629585226dde4c1015a7b2cf320625b7bcc3cbe6 + React-debug: 90089891a2efa920a6faf703c7d19da8ec171add + React-defaultsnativemodule: 8b62b9ff66f8b949952ba0a7ba7c51c547a54c57 + React-domnativemodule: 0b7fd7517f816d49bb4ecd5ff7651ef2219924b6 + React-Fabric: ea10eb69fa55d925f5496ef5077ca67a4e4f31df + React-FabricComponents: 97b85aa8921c99fac63a2665c40ac79f64047a49 + React-FabricImage: 99f3b31f1875b4a714897491b3fd61ff085b15d1 + React-featureflags: 7a49c8fd005384e774e3440d607f2f222a2a8510 + React-featureflagsnativemodule: 21bdbbe71511b11f39372a40334dd58981edf463 + React-graphics: b41caccd61eb1cb9caf87dea997991fd876ce9ab + React-hermes: 2db97c336c271d877c876bb18ec69e412fbbf1b5 + React-idlecallbacksnativemodule: d9a12e7a746510710349c48607d501dee459dea5 + React-ImageManager: 567512900ecd9d16768da97c74788dda2b0e44ef + React-jserrorhandler: b7732f4867e65ea01de1aab1a35df3018a419134 + React-jsi: 6c1005ac2378c2111209ea003240d1516730c8e0 + React-jsiexecutor: 8babbb493c7ef09327a156c09b036b16ff57d721 + React-jsinspector: 0f7c4f1cda283f717d8b8cc3e1d83a11ecaaab7a + React-jsinspectortracing: 85c67fba297cf144e528ea2539dbeb899c3d2e34 + React-jsitooling: af48eba847fe7fc92bdf22d806022b0074122715 + React-jsitracing: 6391ce01407296d4305d104625c8137e6171d904 + React-logger: de83c1b3a6051ef5bb066a4ef77b0ace2f6a156d + React-Mapbuffer: 5b607c3591cc9a24c4fb5e9d1786648ef4f0faf9 + React-microtasksnativemodule: f12254d978377c0303428057f2d7f303f89ae712 + React-NativeModulesApple: 6b10d8c272062a3ecaa48965559f8c368ce1570f + React-oscompat: f6f1f20d3e77b0fe37a190a898b9203d900aca4b + React-perflogger: 8d6611aa167f36425483c43c1b3a504b2d5222c9 + React-performancetimeline: 94075ed1fc64375d6832f46a18495865186a03f6 + React-RCTActionSheet: ff6b87b77d0a155ea744f78087f7fe39b4e597d9 + React-RCTAnimation: 96cd5e7a78d00d083ee378f179b848c04159294b + React-RCTAppDelegate: 59831ea10f58ea3f3725e2bb9de028347512b470 + React-RCTBlob: c109e016169748b49b2af5c88f571c7fd806e1c1 + React-RCTFabric: 2220477b95ea9deb33c0c83bac71000b7cf7a2ad + React-RCTFBReactNativeSpec: bd82b78f9bc13802f21455cbf16b0071e74991d7 + React-RCTImage: a6857005f2fa3faa399eba022aff0e4187a23483 + React-RCTLinking: 400724645638f228380aad5c6dbb01dea2b1c751 + React-RCTNetwork: 08aad843fed91bd87a088ec785c7f0815fda3f3a + React-RCTPushNotification: d30484dcfc5e4975cf62b104dcce56e2b34487de + React-RCTRuntime: a2de21229fe6ba5d2ce2c7425129a7f4635f407e + React-RCTSettings: bbc7450993ba67d9dbd2d44a4d272799cbbb614c + React-RCTTest: dd23a0b18f90d84e070882823a1612ed022c1e45 + React-RCTText: 6e2502e04322f5c1754fc62f484c0fb4d0237b6f + React-RCTVibration: 811bed4f058acb6f6ae3a1cbd9869b32646e5d8f + React-rendererconsistency: ddeb2bd43b3fa76dd038bdf70f5880691ddaf63b + React-renderercss: fefee601d45a8046a47b4782dcbc44eddcf67342 + React-rendererdebug: 637e8ac964ab57ff728c79de66fd25219520d30c + React-rncore: 80ac395b52cdd985cbac805aad66272550d2206b + React-RuntimeApple: e58eaca668a08c80f9ec0fe353bd2945137945cb + React-RuntimeCore: b0b344923620c923c117ed3a915999bc7be329bd + React-runtimeexecutor: a7c89ec3df028485cd50d495c0d04c3ee2c8e875 + React-RuntimeHermes: 6ebc3addcbafd8ed7943debace652d275fee9472 + React-runtimescheduler: 1454b46c54379118ac7cb6debb990e5e59f11400 + React-timing: 7ebe02a69b5800137fe04be5b48aef6f3d73fea5 + React-utils: fa3f640aabf588942052ddb9018fac8614500fe3 + ReactAppDependencyProvider: 110eaab98ce23b8507470156bbd83138199d04d9 ReactCodegen: 0082198e27eef7ce13bf03680eb9d15bbde56601 - ReactCommon: ccca86fc30847bda1f64f6d8f84a596a4ce02f26 - ReactCommon-Samples: feac96a466176d8aa790a5ae4e6076d66f437ffc + ReactCommon: 072ebc1a05bb767e607dee7397f09154c96c16d3 + ReactCommon-Samples: 269528407b78b7fabcd196c2ff33cc7ba48292bb ScreenshotManager: c96f07e207c96f5d91080e408aff95df63875dae SocketRocket: a1845ec01e17d55e3da5df40600892972afb45e1 - Yoga: 1cff0cfbf14d209e21b2f3b023de56f24b43b6fd + Yoga: ed3e3abdd6b5e702120b791bc9dd0962de8cd5e7 PODFILE CHECKSUM: 07eddbe098f0e50aff590a91207f692788a9fe4c diff --git a/packages/rn-tester/js/examples/AccessibilityShowMenu/AccessibilityShowMenu.js b/packages/rn-tester/js/examples/AccessibilityShowMenu/AccessibilityShowMenu.js index 2a05e17498e035..3dbee52ddb4400 100644 --- a/packages/rn-tester/js/examples/AccessibilityShowMenu/AccessibilityShowMenu.js +++ b/packages/rn-tester/js/examples/AccessibilityShowMenu/AccessibilityShowMenu.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict-local * @format - * @flow */ 'use strict'; diff --git a/packages/rn-tester/js/examples/FocusEventsExample/FocusEventsExample.js b/packages/rn-tester/js/examples/FocusEventsExample/FocusEventsExample.js index 86f17a91ca55da..8ad91d626512eb 100644 --- a/packages/rn-tester/js/examples/FocusEventsExample/FocusEventsExample.js +++ b/packages/rn-tester/js/examples/FocusEventsExample/FocusEventsExample.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. diff --git a/packages/rn-tester/js/examples/FocusOnMount/FocusOnMount.js b/packages/rn-tester/js/examples/FocusOnMount/FocusOnMount.js index 1cc7f4cb466aa1..9ae610b1a16928 100644 --- a/packages/rn-tester/js/examples/FocusOnMount/FocusOnMount.js +++ b/packages/rn-tester/js/examples/FocusOnMount/FocusOnMount.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. @@ -7,7 +7,8 @@ * @format * @flow */ -// [macOS] Github#1412) + +// [macOS] 'use strict'; const React = require('react'); diff --git a/packages/rn-tester/js/examples/FocusRing/FocusRingExample.js b/packages/rn-tester/js/examples/FocusRing/FocusRingExample.js index a78ef18ae3f7fd..ae346407bcc327 100644 --- a/packages/rn-tester/js/examples/FocusRing/FocusRingExample.js +++ b/packages/rn-tester/js/examples/FocusRing/FocusRingExample.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict-local * @format - * @flow */ 'use strict'; diff --git a/packages/rn-tester/js/examples/GhostText/GhostText.js b/packages/rn-tester/js/examples/GhostText/GhostText.js index b688fa05391402..73d4aff6420b04 100644 --- a/packages/rn-tester/js/examples/GhostText/GhostText.js +++ b/packages/rn-tester/js/examples/GhostText/GhostText.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict-local * @format - * @flow */ 'use strict'; // [macOS] diff --git a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js index 1ae94b84e055c8..342e57d7e825dd 100644 --- a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js +++ b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js @@ -1,11 +1,11 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict-local * @format - * @flow */ 'use strict'; // [macOS] diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js index cd1ab2d1f758c5..5f9b1ab99b09b1 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js @@ -621,6 +621,11 @@ const textInputExamples: Array = [ { title: 'Multiline blur on submit', render: function (): React.Node { + const [value, setValue] = React.useState(''); + const onSubmitEditing = React.useCallback( + () => Alert.alert('Alert', value), + [value], + ); return ( = [ returnKeyType="next" blurOnSubmit={true} multiline={true} - onSubmitEditing={event => - Alert.alert('Alert', event.nativeEvent.text) - } + onChangeText={setValue} + onSubmitEditing={onSubmitEditing} /> ); @@ -1102,6 +1106,12 @@ if (Platform.OS === 'macos') { { title: 'Clear text on submit - Multiline Textfield', render: function (): React.Node { + const [value, setValue] = React.useState(''); + const onSubmitEditing = React.useCallback( + () => Alert.alert('Alert', value), + [value], + ); + return ( Default submit key (Enter): @@ -1109,6 +1119,8 @@ if (Platform.OS === 'macos') { multiline={true} clearTextOnSubmit={true} style={styles.multiline} + onChangeText={setValue} + onSubmitEditing={onSubmitEditing} /> Custom submit key (Enter): - same as above Custom submit key (CMD + Enter): Custom submit key (Shift + Enter): ); diff --git a/packages/rn-tester/js/examples/Tooltip/TooltipExample.js b/packages/rn-tester/js/examples/Tooltip/TooltipExample.js index 480a041e97b358..4eef686e885a5e 100644 --- a/packages/rn-tester/js/examples/Tooltip/TooltipExample.js +++ b/packages/rn-tester/js/examples/Tooltip/TooltipExample.js @@ -1,10 +1,10 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * Copyright (c) Microsoft Corporation. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow + * @flow strict-local * @format */