From 9be29160d46c9afce5d1fad338fc3207ca8f60c1 Mon Sep 17 00:00:00 2001 From: devan Date: Sun, 2 Mar 2025 20:46:28 -0800 Subject: [PATCH] Squash of all diffs --- RNPrefixHeader.h | 5 + .../ActionSheetIOS/ActionSheetIOS.js | 13 + .../react-native/Libraries/Alert/Alert.js | 52 +- .../Libraries/Alert/RCTAlertManager.ios.js | 2 +- .../Libraries/Alert/RCTAlertManager.macos.js | 19 +- .../Libraries/Animated/AnimatedEvent.js | 2 +- .../Animated/AnimatedImplementation.js | 4 +- .../Animated/NativeAnimatedAllowlist.js | 29 +- .../Animated/animations/Animation.js | 106 +- .../Animated/animations/DecayAnimation.js | 65 +- .../Animated/animations/SpringAnimation.js | 73 +- .../Animated/animations/TimingAnimation.js | 77 +- .../Animated/components/AnimatedFlatList.js | 2 +- .../components/AnimatedSectionList.js | 4 +- .../Animated/createAnimatedComponent.js | 93 +- .../Animated/nodes/AnimatedAddition.js | 10 +- .../Libraries/Animated/nodes/AnimatedColor.js | 7 +- .../Animated/nodes/AnimatedDiffClamp.js | 11 +- .../Animated/nodes/AnimatedDivision.js | 10 +- .../Animated/nodes/AnimatedInterpolation.js | 7 +- .../Animated/nodes/AnimatedModulo.js | 6 +- .../Animated/nodes/AnimatedMultiplication.js | 10 +- .../Libraries/Animated/nodes/AnimatedNode.js | 109 +- .../Animated/nodes/AnimatedObject.js | 26 +- .../Libraries/Animated/nodes/AnimatedProps.js | 157 +- .../Libraries/Animated/nodes/AnimatedStyle.js | 158 +- .../Animated/nodes/AnimatedSubtraction.js | 10 +- .../Animated/nodes/AnimatedTracking.js | 5 +- .../Animated/nodes/AnimatedTransform.js | 84 +- .../Libraries/Animated/nodes/AnimatedValue.js | 7 +- .../Animated/nodes/AnimatedValueXY.js | 4 +- .../Animated/nodes/AnimatedWithChildren.js | 4 +- .../Libraries/Animated/useAnimatedProps.js | 122 +- .../Libraries/AppDelegate/RCTAppDelegate.mm | 14 +- .../Libraries/AppDelegate/RCTAppSetupUtils.mm | 2 +- .../AppDelegate/RCTRootViewFactory.mm | 12 +- .../Libraries/AppState/AppState.js | 14 +- .../AccessibilityInfo/AccessibilityInfo.js | 88 +- .../legacySendAccessibilityEvent.android.js | 2 +- .../legacySendAccessibilityEvent.ios.js | 2 +- .../legacySendAccessibilityEvent.js.flow | 2 +- .../legacySendAccessibilityEvent.macos.js | 20 +- .../ActivityIndicator/ActivityIndicator.js | 8 +- .../Libraries/Components/Button.js | 21 +- .../DrawerAndroid/DrawerLayoutAndroid.js | 4 +- .../Libraries/Components/Keyboard/Keyboard.js | 18 +- .../Keyboard/KeyboardAvoidingView.js | 7 + .../Components/Pressable/Pressable.js | 34 +- .../ProgressBarAndroid.android.js | 20 +- .../Components/SafeAreaView/SafeAreaView.js | 8 +- ...roidHorizontalScrollViewNativeComponent.js | 1 - .../ScrollContentViewNativeComponent.js | 4 +- .../Components/ScrollView/ScrollView.js | 294 ++- .../ScrollView/ScrollViewCommands.js | 2 +- .../ScrollView/ScrollViewContext.js | 2 + .../ScrollView/ScrollViewNativeComponent.js | 18 +- .../ScrollView/ScrollViewStickyHeader.js | 17 +- .../Components/StatusBar/StatusBar.js | 14 +- .../Libraries/Components/Switch/Switch.js | 14 +- .../TextInput/InputAccessoryView.js | 2 +- .../RCTMultilineTextInputNativeComponent.js | 8 +- .../RCTSingelineTextInputNativeComponent.js | 10 +- .../TextInput/RCTTextInputViewConfig.js | 7 + .../Components/TextInput/TextInput.flow.js | 55 +- .../Components/TextInput/TextInput.js | 311 +-- .../Components/TextInput/TextInputState.js | 30 +- .../Touchable/BoundingDimensions.js | 14 +- .../Components/Touchable/Position.js | 9 +- .../Components/Touchable/Touchable.js | 36 +- .../Components/Touchable/TouchableBounce.js | 18 +- .../Touchable/TouchableHighlight.js | 32 +- .../Touchable/TouchableNativeFeedback.js | 34 +- .../Components/Touchable/TouchableOpacity.js | 25 +- .../Touchable/TouchableWithoutFeedback.js | 25 +- .../Libraries/Components/View/DraggedType.js | 6 +- .../View/ReactNativeStyleAttributes.js | 7 +- .../View/ReactNativeViewAttributes.js | 2 +- .../Libraries/Components/View/View.js | 8 +- .../Components/View/ViewAccessibility.js | 8 +- .../Components/View/ViewNativeComponent.js | 104 +- .../Components/View/ViewPropTypes.js | 120 +- .../Libraries/Core/ExceptionsManager.js | 79 +- .../Libraries/Core/setUpBatchedBridge.js | 26 +- .../Libraries/Core/setUpReactDevTools.js | 115 +- .../Libraries/Core/setUpSegmentFetcher.js | 1 + .../Libraries/Core/setUpTimers.js | 39 +- .../Libraries/Debugging/DebuggingOverlay.js | 9 +- .../Libraries/EventEmitter/RCTEventEmitter.js | 8 +- .../Libraries/Image/AssetSourceResolver.js | 13 +- .../Libraries/Image/Image.android.js | 6 +- .../react-native/Libraries/Image/Image.ios.js | 4 +- .../Libraries/Image/Image.macos.js | 251 ++- .../Libraries/Image/ImageBackground.js | 7 +- .../Libraries/Image/ImageProps.js | 13 +- .../Libraries/Image/ImageResizeMode.js | 5 +- .../Libraries/Image/ImageSource.js | 2 - .../Libraries/Image/ImageTypes.flow.js | 20 +- .../Libraries/Image/ImageUtils.js | 9 +- .../Image/ImageViewNativeComponent.js | 8 +- .../Libraries/Image/RCTImageCache.mm | 5 + .../Libraries/Image/RCTImageLoader.mm | 12 +- .../Libraries/Image/RCTImageURLLoader.h | 8 + .../Libraries/Image/RCTImageView.mm | 27 +- .../Libraries/Image/RCTResizeMode.h | 11 +- .../Libraries/Image/nativeImageSource.js | 6 +- .../Libraries/Inspector/NetworkOverlay.js | 6 +- .../Inspector/ReactDevToolsOverlay.js | 22 +- .../getInspectorDataForViewAtPoint.js | 8 +- .../Interaction/InteractionManager.js | 7 +- .../Libraries/Interaction/TouchHistoryMath.js | 41 +- .../Libraries/JSInspector/NetworkAgent.js | 2 +- .../LayoutAnimation/LayoutAnimation.js | 4 +- .../react-native/Libraries/Linking/Linking.js | 2 +- .../Libraries/Lists/FillRateHelper.js | 6 +- .../react-native/Libraries/Lists/FlatList.js | 46 +- .../Libraries/Lists/SectionList.js | 4 +- .../Libraries/Lists/SectionListModern.js | 18 +- .../Libraries/Lists/ViewabilityHelper.js | 6 +- .../Libraries/Lists/VirtualizeUtils.js | 4 +- .../Libraries/Lists/VirtualizedList.js | 6 +- .../Libraries/Lists/VirtualizedListContext.js | 4 +- .../Libraries/Lists/VirtualizedSectionList.js | 6 +- .../Libraries/LogBox/Data/LogBoxData.js | 6 +- .../react-native/Libraries/LogBox/LogBox.js | 23 +- .../LogBox/LogBoxInspectorContainer.js | 2 +- .../LogBox/LogBoxNotificationContainer.js | 4 +- .../Libraries/LogBox/UI/AnsiHighlight.js | 43 +- .../LogBox/UI/LogBoxInspectorCodeFrame.js | 11 +- .../LogBox/UI/LogBoxInspectorHeader.js | 2 +- .../LogBox/UI/LogBoxInspectorReactFrames.js | 4 +- .../LogBox/UI/LogBoxInspectorStackFrame.js | 4 +- .../LogBox/UI/LogBoxInspectorStackFrames.js | 2 +- .../Libraries/LogBox/UI/LogBoxMessage.js | 4 +- .../react-native/Libraries/Modal/Modal.js | 37 +- .../RCTNativeAnimatedNodesManager.mm | 2 +- .../NativeComponent/BaseViewConfig.android.js | 71 +- .../NativeComponent/BaseViewConfig.ios.js | 3 +- .../NativeComponent/BaseViewConfig.macos.js | 232 ++- .../NativeComponentRegistry.js | 6 +- .../StaticViewConfigValidator.js | 1 - .../Libraries/Network/FormData.js | 14 +- .../Network/RCTNetworking.android.js | 40 +- .../Libraries/Network/RCTNetworking.ios.js | 49 +- .../Libraries/Network/RCTNetworking.js.flow | 47 +- .../Libraries/Network/RCTNetworking.macos.js | 109 +- .../Libraries/Network/XHRInterceptor.js | 77 +- .../Libraries/Network/XMLHttpRequest.js | 27 +- .../NewAppScreen/components/HermesBadge.js | 2 +- .../PermissionsAndroid/PermissionsAndroid.js | 8 +- .../Libraries/Pressability/HoverState.js | 2 + .../Libraries/Pressability/Pressability.js | 71 +- .../Libraries/Pressability/usePressability.js | 5 +- .../PushNotificationIOS.js | 2 +- .../Libraries/ReactNative/AppContainer.js | 2 +- .../Libraries/ReactNative/AppRegistry.js | 6 - .../Libraries/ReactNative/DisplayMode.js | 2 +- .../Libraries/ReactNative/PaperUIManager.js | 11 +- .../ReactFabricHostComponent.js | 5 +- .../ReactNative/RendererImplementation.js | 35 +- .../getCachedComponentWithDebugName.js | 4 +- .../ReactNative/renderApplication.js | 17 +- .../ReactNative/requireNativeComponent.js | 7 +- .../ReactNativePrivateInterface.js | 2 +- .../Libraries/Settings/Settings.ios.js | 2 +- .../Libraries/Settings/Settings.macos.js | 74 +- .../PlatformColorValueTypes.macos.js | 4 +- .../PlatformColorValueTypesMacOS.js | 2 +- .../PlatformColorValueTypesMacOS.macos.js | 18 +- .../Libraries/StyleSheet/StyleSheet.js | 8 +- .../Libraries/StyleSheet/StyleSheetTypes.js | 24 +- .../StyleSheet/processBackgroundImage.js | 197 +- .../Libraries/StyleSheet/processTransform.js | 43 +- .../Text/BaseText/RCTBaseTextViewManager.mm | 1 + .../Libraries/Text/RCTTextAttributes.h | 1 + .../Libraries/Text/RCTTextAttributes.mm | 4 + packages/react-native/Libraries/Text/Text.js | 507 ++--- .../Libraries/Text/Text/RCTTextView.h | 4 + .../Libraries/Text/Text/RCTTextView.mm | 79 +- .../Libraries/Text/Text/RCTTextViewManager.mm | 2 + .../RCTMultilineTextInputViewManager.mm | 13 + .../Text/TextInput/Multiline/RCTUITextView.h | 3 +- .../Text/TextInput/Multiline/RCTUITextView.mm | 41 +- .../TextInput/Multiline/RCTWrappedTextView.h | 24 + .../TextInput/Multiline/RCTWrappedTextView.mm | 204 ++ .../RCTBackedTextInputDelegateAdapter.h | 4 + .../RCTBackedTextInputDelegateAdapter.mm | 88 +- .../RCTBackedTextInputViewProtocol.h | 2 + .../Text/TextInput/RCTBaseTextInputView.mm | 41 +- .../TextInput/RCTBaseTextInputViewManager.mm | 4 + .../TextInput/Singleline/RCTUITextField.h | 3 +- .../TextInput/Singleline/RCTUITextField.mm | 35 +- .../Singleline/macOS/RCTUISecureTextField.h | 3 - .../Singleline/macOS/RCTUISecureTextField.m | 4 - .../Libraries/Text/TextNativeComponent.js | 9 +- .../react-native/Libraries/Text/TextProps.js | 26 +- .../TurboModule/TurboModuleRegistry.js | 10 +- .../Libraries/Types/CoreEventTypes.js | 134 +- .../Libraries/Utilities/Appearance.js | 12 +- .../Utilities/BackHandler.android.js | 24 +- .../Libraries/Utilities/BackHandler.ios.js | 15 +- .../Libraries/Utilities/BackHandler.js.flow | 4 - .../Libraries/Utilities/BackHandler.macos.js | 39 +- .../Libraries/Utilities/DevSettings.js | 2 +- .../Libraries/Utilities/HMRClient.js | 14 +- .../Libraries/Utilities/Platform.flow.js | 4 +- .../Libraries/Utilities/Platform.macos.js | 10 +- .../Utilities/ReactNativeTestTools.js | 2 +- .../Utilities/codegenNativeComponent.js | 2 +- .../Libraries/Utilities/useMergeRefs.js | 33 +- .../Libraries/WebSocket/WebSocket.js | 2 +- .../Libraries/WebSocket/WebSocketEvent.js | 5 +- .../WebSocket/WebSocketInterceptor.js | 46 +- .../promiseRejectionTrackingOptions.js | 2 +- packages/react-native/React/Base/RCTAssert.h | 2 +- packages/react-native/React/Base/RCTBridge.mm | 1 - packages/react-native/React/Base/RCTDefines.h | 12 +- .../react-native/React/Base/RCTRootView.m | 7 + .../react-native/React/Base/RCTTouchHandler.h | 7 + .../react-native/React/Base/RCTTouchHandler.m | 251 ++- packages/react-native/React/Base/RCTUIKit.h | 18 +- packages/react-native/React/Base/RCTUtils.h | 6 + packages/react-native/React/Base/RCTUtils.m | 28 +- .../Base/Surface/RCTSurfaceRootShadowView.h | 2 +- .../RCTSurfaceHostingProxyRootView.h | 1 + .../RCTSurfaceHostingProxyRootView.mm | 3 + .../RCTSurfaceHostingView.mm | 11 + .../React/Base/macOS/RCTPlatform.m | 6 +- .../React/Base/macOS/RCTPlatformDisplayLink.m | 2 - .../react-native/React/Base/macOS/RCTUIKit.m | 178 +- .../CoreModules/RCTActionSheetManager.mm | 16 +- .../React/CoreModules/RCTDevMenu.h | 12 + .../React/CoreModules/RCTDevMenu.mm | 57 +- .../React/CoreModules/RCTDeviceInfo.mm | 12 +- .../React/CoreModules/RCTEventDispatcher.mm | 1 + .../React/CoreModules/RCTKeyboardObserver.mm | 3 +- .../React/CxxBridge/JSCExecutorFactory.mm | 20 +- .../React/CxxModule/RCTCxxMethod.mm | 12 +- .../DevSupport/RCTInspectorDevServerHelper.mm | 16 +- .../React/Fabric/AppleEventBeat.cpp | 31 + .../React/Fabric/AppleEventBeat.h | 40 + .../Image/RCTImageComponentView.mm | 8 - .../ScrollView/RCTEnhancedScrollView.h | 6 + .../ScrollView/RCTEnhancedScrollView.mm | 81 +- .../ScrollView/RCTScrollViewComponentView.h | 4 + .../ScrollView/RCTScrollViewComponentView.mm | 97 +- .../Text/RCTParagraphComponentView.h | 4 + .../Text/RCTParagraphComponentView.mm | 206 +- .../TextInput/RCTTextInputComponentView.mm | 87 +- .../TextInput/RCTTextInputUtils.h | 4 +- .../TextInput/RCTTextInputUtils.mm | 13 +- .../View/RCTViewComponentView.h | 5 + .../View/RCTViewComponentView.mm | 614 +++++- .../Mounting/RCTComponentViewProtocol.h | 42 + .../Fabric/Mounting/RCTMountingManager.h | 2 +- .../Fabric/Mounting/RCTMountingManager.mm | 10 +- .../react-native/React/Fabric/RCTScheduler.h | 4 +- .../react-native/React/Fabric/RCTScheduler.mm | 9 +- .../React/Fabric/RCTSurfacePresenter.mm | 24 +- .../React/Fabric/RCTSurfaceTouchHandler.h | 13 + .../React/Fabric/RCTSurfaceTouchHandler.mm | 287 ++- .../React/Fabric/Utils/RCTLinearGradient.h | 19 + .../React/Fabric/Utils/RCTLinearGradient.mm | 138 ++ .../RCTCxxInspectorPackagerConnection.mm | 3 + .../RCTCxxInspectorWebSocketAdapter.mm | 8 + .../React/Inspector/RCTInspector.mm | 2 +- .../Modules/MacOS/RCTAccessibilityManager.m | 3 +- .../react-native/React/Modules/RCTUIManager.m | 6 +- packages/react-native/React/Views/RCTCursor.h | 2 +- packages/react-native/React/Views/RCTFont.h | 2 + packages/react-native/React/Views/RCTFont.mm | 18 +- .../react-native/React/Views/RCTShadowView.h | 6 +- .../react-native/React/Views/RCTShadowView.m | 26 +- .../React/Views/RCTSwitchManager.m | 4 + packages/react-native/React/Views/RCTView.h | 3 + packages/react-native/React/Views/RCTView.m | 42 +- .../react-native/React/Views/RCTViewManager.m | 5 +- .../MacOS/RCTScrollContentLocalData.h | 5 - .../MacOS/RCTScrollContentLocalData.m | 2 - .../Views/ScrollView/RCTScrollContentView.m | 14 +- .../React/Views/ScrollView/RCTScrollView.m | 58 +- .../RCTNativeSampleTurboModuleSpec.h | 4 + .../ios/ReactCommon/RCTSampleLegacyModule.mm | 4 +- .../ios/ReactCommon/RCTSampleTurboCxxModule.h | 11 +- .../ReactCommon/RCTSampleTurboCxxModule.mm | 25 +- .../ios/ReactCommon/RCTSampleTurboModule.mm | 2 +- .../textinput/TextInputEventEmitter.cpp | 193 ++ .../textinput/TextInputEventEmitter.h | 57 + .../components/textinput/TextInputState.cpp | 71 + .../iostextinput => }/TextInputState.h | 34 +- .../components/textinput/baseConversions.h | 46 + .../TextInputState.cpp => basePrimitives.h} | 13 +- .../MacOSTextInputEventEmitter.cpp | 51 + .../iostextinput/MacOSTextInputEventEmitter.h | 40 + .../iostextinput/TextInputProps.cpp | 26 - .../components/iostextinput/TextInputProps.h | 6 - .../iostextinput/TextInputShadowNode.cpp | 163 -- .../iostextinput/TextInputShadowNode.h | 70 +- .../components/iostextinput/primitives.h | 32 +- .../iostextinput/propsConversions.h | 51 + .../components/view/HostPlatformTouch.h | 70 +- .../components/view/HostPlatformTouch.h | 70 + .../view/HostPlatformViewEventEmitter.cpp | 163 ++ .../view/HostPlatformViewEventEmitter.h | 46 + .../components/view/HostPlatformViewEvents.h | 98 + .../components/view/HostPlatformViewProps.cpp | 124 ++ .../components/view/HostPlatformViewProps.h | 40 + .../view/HostPlatformViewTraitsInitializer.h | 28 + .../react/renderer/components/view/KeyEvent.h | 135 ++ .../renderer/components/view/MouseEvent.h | 73 + .../renderer/graphics/HostPlatformColor.mm | 55 +- .../renderer/graphics/RCTPlatformColorUtils.h | 3 +- .../graphics/RCTPlatformColorUtils.mm | 4 +- .../renderer/imagemanager/ImageManager.mm | 9 + .../imagemanager/ImageRequestParams.h | 30 + .../renderer/imagemanager/RCTImageManager.mm | 44 +- .../RCTImagePrimitivesConversions.h | 52 +- .../imagemanager/RCTSyncImageManager.mm | 7 +- .../RCTAttributedTextUtils.mm | 7 +- .../textlayoutmanager/RCTFontUtils.mm | 24 +- .../textlayoutmanager/RCTTextLayoutManager.mm | 78 +- .../RCTTextPrimitivesConversions.h | 7 +- .../textlayoutmanager/TextLayoutManager.mm | 32 +- .../ios/ReactCommon/RCTHermesInstance.mm | 5 +- .../ios/ReactCommon/RCTHost+Internal.h | 3 + .../platform/ios/ReactCommon/RCTHost.h | 3 + .../platform/ios/ReactCommon/RCTHost.mm | 82 +- .../platform/ios/ReactCommon/RCTInstance.mm | 114 +- packages/react-native/index.js | 4 +- .../private/animated/NativeAnimatedHelper.js | 34 +- .../ReactDevToolsSettingsManager.ios.js | 30 + .../src/private/setup/setUpDOM.js | 20 +- .../private/setup/setUpMutationObserver.js | 5 + ...izontalScrollContentViewNativeComponent.js | 1 + .../RCTModalHostViewNativeComponent.js | 8 + .../specs/modules/NativeAccessibilityInfo.js | 9 + .../modules/NativeAccessibilityManager.js | 8 +- .../specs/modules/NativeActionSheetManager.js | 2 + .../specs/modules/NativeAlertManager.js | 4 +- .../private/specs/modules/NativeAppearance.js | 14 +- .../specs/modules/NativeExceptionsManager.js | 18 +- .../private/webapis/dom/geometry/DOMRect.js | 4 +- .../webapis/dom/geometry/DOMRectReadOnly.js | 4 +- .../webapis/dom/nodes/ReactNativeElement.js | 59 +- .../private/webapis/dom/nodes/ReadOnlyNode.js | 4 +- .../IntersectionObserver.js | 113 +- .../IntersectionObserverEntry.js | 26 + .../IntersectionObserverManager.js | 1 + .../specs/NativeIntersectionObserver.js | 2 + .../webapis/performance/EventTiming.js | 21 +- .../webapis/performance/Performance.js | 143 +- .../webapis/performance/PerformanceEntry.js | 7 +- .../performance/PerformanceObserver.js | 229 +-- .../performance/RawPerformanceEntry.js | 2 +- .../private/webapis/performance/UserTiming.js | 18 +- .../performance/specs/NativePerformance.js | 64 +- .../IntegrationTests/LayoutEventsTest.js | 24 +- packages/rn-tester/js/RNTesterApp.ios.js | 2 +- packages/rn-tester/js/RNTesterApp.macos.js | 47 +- packages/rn-tester/js/RNTesterAppShared.js | 37 +- .../js/components/ListExampleShared.js | 15 +- .../js/components/RNTesterModuleContainer.js | 37 +- .../js/components/RNTesterModuleList.js | 2 +- .../rn-tester/js/components/RNTesterNavbar.js | 40 +- .../js/components/RNTesterSettingSwitchRow.js | 5 +- .../rn-tester/js/components/RNTesterTheme.js | 6 + .../js/examples/ASAN/ASANCrashExample.js | 4 +- .../AccessibilityAndroidExample.js | 111 +- .../Accessibility/AccessibilityExample.js | 1796 ++++++++--------- .../AccessibilityShowMenu.js | 6 +- .../ActionSheetIOS/ActionSheetIOSExample.js | 50 + .../ActivityIndicatorExample.js | 3 +- .../js/examples/Alert/AlertExample.js | 7 +- .../js/examples/AppState/AppStateExample.js | 29 +- .../examples/Appearance/AppearanceExample.js | 5 +- .../examples/Dimensions/DimensionsExample.js | 14 +- .../RNTesterPlatformTestEventRecorder.js | 7 +- ...PointerEventAttributesHoverablePointers.js | 11 +- .../PointerEventAttributesNoHoverPointers.js | 11 +- .../PointerEventClickTouch.js | 5 - .../PointerEventPointerOverOut.js | 31 +- .../Experimental/W3CPointerEventsExample.js | 2 +- .../js/examples/Filter/FilterExample.js | 3 +- .../examples/FlatList/BaseFlatListExample.js | 21 +- .../js/examples/FlatList/FlatList-basic.js | 38 +- .../examples/FlatList/FlatListExampleIndex.js | 10 +- .../FocusEventsExample/FocusEventsExample.js | 2 +- .../js/examples/FocusOnMount/FocusOnMount.js | 3 +- .../js/examples/FocusRing/FocusRingExample.js | 4 +- .../js/examples/GhostText/GhostText.js | 4 +- .../js/examples/Image/ImageExample.js | 211 +- .../IntersectionObserverIndex.js | 2 + .../InvalidProps/InvalidPropsExample.js | 21 +- .../JSResponderHandlerExample.js | 16 +- .../js/examples/Keyboard/KeyboardExample.js | 17 +- .../KeyboardAvoidingViewExample.js | 23 +- .../KeyboardEventsExample.js | 4 +- .../examples/Layout/LayoutAnimationExample.js | 54 +- .../js/examples/Layout/LayoutEventsExample.js | 46 +- .../js/examples/Layout/LayoutExample.js | 37 +- .../LinearGradient/LinearGradientExample.js | 71 +- .../js/examples/Linking/LinkingExample.js | 18 +- .../MixBlendMode/MixBlendModeExample.js | 22 +- .../js/examples/Modal/ModalOnShow.js | 23 +- .../js/examples/Modal/ModalPresentation.js | 98 +- .../MutationObserver/MutationObserverIndex.js | 4 +- .../NativeAnimationsExample.js | 27 +- .../OSSLibraryExample/OSSLibraryExample.js | 5 +- .../OrientationChangeExample.js | 8 +- .../PanResponder/PanResponderExample.js | 155 +- .../Performance/PerformanceApiExample.js | 93 +- .../Performance/components/ItemList.js | 11 +- .../PermissionsAndroid/PermissionsExample.js | 20 +- .../examples/PixelRatio/PixelRatioExample.js | 52 +- .../PlatformColor/PlatformColorExample.js | 38 +- .../js/examples/Pressable/PressableExample.js | 4 +- .../RefreshControl/RefreshControlExample.js | 12 +- .../examples/ScrollView/ScrollViewExample.js | 70 +- .../SectionList/SectionList-contentInset.js | 5 +- ...Changed-horizontal-noWaitForInteraction.js | 1 - ...rizontal-offScreen-noWaitForInteraction.js | 1 - ...msChanged-horizontal-waitForInteraction.js | 1 - ...ewableItemsChanged-noWaitForInteraction.js | 1 - ...sChanged-offScreen-noWaitForInteraction.js | 1 - ...ViewableItemsChanged-waitForInteraction.js | 1 - .../SectionList-onViewableItemsChanged.js | 1 - .../SectionList/SectionList-scrollable.js | 25 +- .../SectionList/SectionListBaseExample.js | 14 +- .../examples/SectionList/SectionListIndex.js | 2 +- .../js/examples/Share/ShareExample.js | 25 +- .../examples/Snapshot/SnapshotViewIOS.ios.js | 8 +- .../js/examples/StatusBar/StatusBarExample.js | 128 +- .../SwipeableCardExample.js | 2 +- .../js/examples/Switch/SwitchExample.js | 19 +- .../js/examples/Text/TextExample.android.js | 121 +- .../js/examples/Text/TextExample.ios.js | 70 +- .../js/examples/TextInput/ExampleTextInput.js | 14 +- .../TextInput/TextInputExample.ios.js | 174 +- .../TextInput/TextInputSharedExamples.js | 74 +- .../js/examples/Timer/TimerExample.js | 9 +- .../ToastAndroid/ToastAndroidExample.js | 21 +- .../js/examples/Tooltip/TooltipExample.js | 4 +- .../js/examples/Touchable/TouchableExample.js | 157 +- .../NativeCxxModuleExampleExample.js | 24 +- .../TurboModule/SampleLegacyModuleExample.js | 21 +- .../TurboModule/SampleTurboModuleExample.js | 31 +- .../js/examples/Vibration/VibrationExample.js | 23 +- .../rn-tester/js/examples/View/ViewExample.js | 199 +- .../js/examples/WebSocket/WebSocketExample.js | 36 +- .../js/examples/XHR/XHRExampleBinaryUpload.js | 14 +- .../js/examples/XHR/XHRExampleDownload.js | 28 +- .../js/examples/XHR/XHRExampleFetch.js | 52 +- .../js/examples/XHR/XHRExampleHeaders.js | 7 +- .../js/examples/XHR/XHRExampleOnTimeOut.js | 4 +- packages/rn-tester/js/types/RNTesterTypes.js | 19 +- .../js/utils/RNTesterList.android.js | 24 +- .../rn-tester/js/utils/RNTesterList.ios.js | 29 +- .../rn-tester/js/utils/RNTesterList.js.flow | 4 +- .../rn-tester/js/utils/RNTesterList.macos.js | 392 +++- .../js/utils/RNTesterNavigationReducer.js | 17 +- .../rn-tester/js/utils/testerStateUtils.js | 1 + .../Interaction/Batchinator.js | 76 - .../Lists/ListMetricsAggregator.js | 35 +- .../Lists/ViewabilityHelper.js | 4 +- .../Lists/VirtualizeUtils.js | 16 +- .../Lists/VirtualizedList.js | 85 +- .../Lists/VirtualizedListCellRenderer.js | 34 +- .../Lists/VirtualizedListProps.js | 28 +- .../Lists/VirtualizedSectionList.js | 22 +- 468 files changed, 11965 insertions(+), 6010 deletions(-) create mode 100644 RNPrefixHeader.h create mode 100644 packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.h create mode 100644 packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.mm create mode 100644 packages/react-native/React/Fabric/AppleEventBeat.cpp create mode 100644 packages/react-native/React/Fabric/AppleEventBeat.h create mode 100644 packages/react-native/React/Fabric/Utils/RCTLinearGradient.h create mode 100644 packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm create mode 100644 packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputState.cpp rename packages/react-native/ReactCommon/react/renderer/components/textinput/{platform/ios/react/renderer/components/iostextinput => }/TextInputState.h (63%) create mode 100644 packages/react-native/ReactCommon/react/renderer/components/textinput/baseConversions.h rename packages/react-native/ReactCommon/react/renderer/components/textinput/{platform/ios/react/renderer/components/iostextinput/TextInputState.cpp => basePrimitives.h} (55%) create mode 100644 packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/MacOSTextInputEventEmitter.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/MacOSTextInputEventEmitter.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformTouch.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewTraitsInitializer.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/KeyEvent.h create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h create mode 100644 packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/ImageRequestParams.h create mode 100644 packages/react-native/src/private/debugging/ReactDevToolsSettingsManager.ios.js delete mode 100644 packages/virtualized-lists/Interaction/Batchinator.js diff --git a/RNPrefixHeader.h b/RNPrefixHeader.h new file mode 100644 index 00000000000000..ec91ca9bdcd141 --- /dev/null +++ b/RNPrefixHeader.h @@ -0,0 +1,5 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#ifdef __OBJC__ + #import +#endif diff --git a/packages/react-native/Libraries/ActionSheetIOS/ActionSheetIOS.js b/packages/react-native/Libraries/ActionSheetIOS/ActionSheetIOS.js index e970cdc06a68a2..e758572882c64e 100644 --- a/packages/react-native/Libraries/ActionSheetIOS/ActionSheetIOS.js +++ b/packages/react-native/Libraries/ActionSheetIOS/ActionSheetIOS.js @@ -49,6 +49,7 @@ const ActionSheetIOS = { +anchor?: ?number, +tintColor?: ColorValue | ProcessedColorValue, +cancelButtonTintColor?: ColorValue | ProcessedColorValue, + +disabledButtonTintColor?: ColorValue | ProcessedColorValue, +userInterfaceStyle?: string, +disabledButtonIndices?: Array, |}, @@ -64,6 +65,7 @@ const ActionSheetIOS = { const { tintColor, cancelButtonTintColor, + disabledButtonTintColor, destructiveButtonIndex, ...remainingOptions } = options; @@ -77,6 +79,10 @@ const ActionSheetIOS = { const processedTintColor = processColor(tintColor); const processedCancelButtonTintColor = processColor(cancelButtonTintColor); + const processedDisabledButtonTintColor = processColor( + disabledButtonTintColor, + ); + invariant( processedTintColor == null || typeof processedTintColor === 'number', 'Unexpected color given for ActionSheetIOS.showActionSheetWithOptions tintColor', @@ -86,6 +92,11 @@ const ActionSheetIOS = { typeof processedCancelButtonTintColor === 'number', 'Unexpected color given for ActionSheetIOS.showActionSheetWithOptions cancelButtonTintColor', ); + invariant( + processedDisabledButtonTintColor == null || + typeof processedDisabledButtonTintColor === 'number', + 'Unexpected color given for ActionSheetIOS.showActionSheetWithOptions disabledButtonTintColor', + ); RCTActionSheetManager.showActionSheetWithOptions( { ...remainingOptions, @@ -93,6 +104,8 @@ const ActionSheetIOS = { tintColor: processedTintColor, // $FlowFixMe[incompatible-call] cancelButtonTintColor: processedCancelButtonTintColor, + // $FlowFixMe[incompatible-call] + disabledButtonTintColor: processedDisabledButtonTintColor, destructiveButtonIndices, }, callback, diff --git a/packages/react-native/Libraries/Alert/Alert.js b/packages/react-native/Libraries/Alert/Alert.js index 22756c171a3196..e742704a28b821 100644 --- a/packages/react-native/Libraries/Alert/Alert.js +++ b/packages/react-native/Libraries/Alert/Alert.js @@ -9,53 +9,17 @@ */ import type {DialogOptions} from '../NativeModules/specs/NativeDialogManagerAndroid'; +import type {AlertButtons, AlertOptions, AlertType} from './AlertTypes.flow'; import Platform from '../Utilities/Platform'; import RCTAlertManager from './RCTAlertManager'; -export type AlertType = - | 'default' - | 'plain-text' - | 'secure-text' - | 'login-password'; -export type AlertButtonStyle = 'default' | 'cancel' | 'destructive'; -export type Buttons = Array<{ - text?: string, - onPress?: ?Function, - isPreferred?: boolean, - style?: AlertButtonStyle, - ... -}>; -// [macOS -export type DefaultInputsArray = Array<{ - default?: string, - placeholder?: string, - style?: AlertButtonStyle, -}>; -// macOS] - -type Options = { - cancelable?: ?boolean, - userInterfaceStyle?: 'unspecified' | 'light' | 'dark', - onDismiss?: ?() => void, - // [macOS - modal?: ?boolean, - critical?: ?boolean, - // macOS] - ... -}; - -/** - * Launches an alert dialog with the specified title and message. - * - * See https://reactnative.dev/docs/alert - */ class Alert { static alert( title: ?string, message?: ?string, - buttons?: Buttons, - options?: Options, + buttons?: AlertButtons, + options?: AlertOptions, ): void { if (Platform.OS === 'ios') { Alert.prompt( @@ -99,7 +63,7 @@ class Alert { // At most three buttons (neutral, negative, positive). Ignore rest. // The text 'OK' should be probably localized. iOS Alert does that in native. const defaultPositiveText = 'OK'; - const validButtons: Buttons = buttons + const validButtons: AlertButtons = buttons ? buttons.slice(0, 3) : [{text: defaultPositiveText}]; const buttonPositive = validButtons.pop(); @@ -142,11 +106,11 @@ class Alert { static prompt( title: ?string, message?: ?string, - callbackOrButtons?: ?(((text: string) => void) | Buttons), + callbackOrButtons?: ?(((text: string) => void) | AlertButtons), type?: ?AlertType = 'plain-text', defaultValue?: string, keyboardType?: string, - options?: Options, + options?: AlertOptions, ): void { if (Platform.OS === 'ios') { let callbacks: Array = []; @@ -252,7 +216,7 @@ class Alert { static promptMacOS( title: ?string, message?: ?string, - callbackOrButtons?: ?((text: string) => void) | Buttons, + callbackOrButtons?: ?((text: string) => void) | AlertButtons, type?: ?AlertType = 'plain-text', defaultInputs?: DefaultInputsArray, modal?: ?boolean, @@ -292,4 +256,4 @@ class Alert { // macOS] } -module.exports = Alert; +export default Alert; diff --git a/packages/react-native/Libraries/Alert/RCTAlertManager.ios.js b/packages/react-native/Libraries/Alert/RCTAlertManager.ios.js index d4e9eab97ff66c..eb40889589bc55 100644 --- a/packages/react-native/Libraries/Alert/RCTAlertManager.ios.js +++ b/packages/react-native/Libraries/Alert/RCTAlertManager.ios.js @@ -12,7 +12,7 @@ import type {Args} from './NativeAlertManager'; import NativeAlertManager from './NativeAlertManager'; -module.exports = { +export default { alertWithArgs( args: Args, callback: (id: number, value: string) => void, diff --git a/packages/react-native/Libraries/Alert/RCTAlertManager.macos.js b/packages/react-native/Libraries/Alert/RCTAlertManager.macos.js index 81c3416dcf36b1..d4e9eab97ff66c 100644 --- a/packages/react-native/Libraries/Alert/RCTAlertManager.macos.js +++ b/packages/react-native/Libraries/Alert/RCTAlertManager.macos.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015-present, Facebook, Inc. + * 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. @@ -8,9 +8,18 @@ * @flow strict-local */ -// [macOS] +import type {Args} from './NativeAlertManager'; -/* $FlowFixMe allow macOS to share iOS file */ -const alertWithArgs = require('./RCTAlertManager.ios'); +import NativeAlertManager from './NativeAlertManager'; -module.exports = alertWithArgs; +module.exports = { + alertWithArgs( + args: Args, + callback: (id: number, value: string) => void, + ): void { + if (NativeAlertManager == null) { + return; + } + NativeAlertManager.alertWithArgs(args, callback); + }, +}; diff --git a/packages/react-native/Libraries/Animated/AnimatedEvent.js b/packages/react-native/Libraries/Animated/AnimatedEvent.js index 39b25807b9be9d..52e732e4070e19 100644 --- a/packages/react-native/Libraries/Animated/AnimatedEvent.js +++ b/packages/react-native/Libraries/Animated/AnimatedEvent.js @@ -12,8 +12,8 @@ import type {PlatformConfig} from './AnimatedPlatformConfig'; -import {findNodeHandle} from '../ReactNative/RendererProxy'; import NativeAnimatedHelper from '../../src/private/animated/NativeAnimatedHelper'; +import {findNodeHandle} from '../ReactNative/RendererProxy'; import AnimatedValue from './nodes/AnimatedValue'; import AnimatedValueXY from './nodes/AnimatedValueXY'; import invariant from 'invariant'; diff --git a/packages/react-native/Libraries/Animated/AnimatedImplementation.js b/packages/react-native/Libraries/Animated/AnimatedImplementation.js index 0677cc0fd4b280..ae641bc7fabc2e 100644 --- a/packages/react-native/Libraries/Animated/AnimatedImplementation.js +++ b/packages/react-native/Libraries/Animated/AnimatedImplementation.js @@ -373,7 +373,7 @@ const parallel = function ( const stopTogether = !(config && config.stopTogether === false); const result = { - start: function (callback?: ?EndCallback) { + start: function (callback?: ?EndCallback, isLooping?: boolean) { if (doneCount === animations.length) { callback && callback({finished: true}); return; @@ -397,7 +397,7 @@ const parallel = function ( if (!animation) { cb({finished: true}); } else { - animation.start(cb); + animation.start(cb, isLooping); } }); }, diff --git a/packages/react-native/Libraries/Animated/NativeAnimatedAllowlist.js b/packages/react-native/Libraries/Animated/NativeAnimatedAllowlist.js index d86712540ea961..ac41c77621e14d 100644 --- a/packages/react-native/Libraries/Animated/NativeAnimatedAllowlist.js +++ b/packages/react-native/Libraries/Animated/NativeAnimatedAllowlist.js @@ -8,6 +8,8 @@ * @format */ +import type {AnimatedPropsAllowlist} from './nodes/AnimatedProps'; + import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags'; /** @@ -16,7 +18,7 @@ import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNa * In general native animated implementation should support any numeric or color property that * doesn't need to be updated through the shadow view hierarchy (all non-layout properties). */ -const SUPPORTED_COLOR_STYLES: {[string]: boolean} = { +const SUPPORTED_COLOR_STYLES: {[string]: true} = { backgroundColor: true, borderBottomColor: true, borderColor: true, @@ -29,7 +31,7 @@ const SUPPORTED_COLOR_STYLES: {[string]: boolean} = { tintColor: true, }; -const SUPPORTED_STYLES: {[string]: boolean} = { +const SUPPORTED_STYLES: {[string]: true} = { ...SUPPORTED_COLOR_STYLES, borderBottomEndRadius: true, borderBottomLeftRadius: true, @@ -58,7 +60,7 @@ const SUPPORTED_STYLES: {[string]: boolean} = { translateY: true, }; -const SUPPORTED_TRANSFORMS: {[string]: boolean} = { +const SUPPORTED_TRANSFORMS: {[string]: true} = { translateX: true, translateY: true, scale: true, @@ -71,10 +73,12 @@ const SUPPORTED_TRANSFORMS: {[string]: boolean} = { perspective: true, skewX: true, skewY: true, - matrix: ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform(), + ...(ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform() + ? {matrix: true} + : {}), }; -const SUPPORTED_INTERPOLATION_PARAMS: {[string]: boolean} = { +const SUPPORTED_INTERPOLATION_PARAMS: {[string]: true} = { inputRange: true, outputRange: true, extrapolate: true, @@ -82,6 +86,13 @@ const SUPPORTED_INTERPOLATION_PARAMS: {[string]: boolean} = { extrapolateLeft: true, }; +/** + * Default allowlist for component props that support native animated values. + */ +export default { + style: SUPPORTED_STYLES, +} as AnimatedPropsAllowlist; + export function allowInterpolationParam(param: string): void { SUPPORTED_INTERPOLATION_PARAMS[param] = true; } @@ -95,17 +106,17 @@ export function allowTransformProp(prop: string): void { } export function isSupportedColorStyleProp(prop: string): boolean { - return SUPPORTED_COLOR_STYLES[prop] === true; + return SUPPORTED_COLOR_STYLES.hasOwnProperty(prop); } export function isSupportedInterpolationParam(param: string): boolean { - return SUPPORTED_INTERPOLATION_PARAMS[param] === true; + return SUPPORTED_INTERPOLATION_PARAMS.hasOwnProperty(param); } export function isSupportedStyleProp(prop: string): boolean { - return SUPPORTED_STYLES[prop] === true; + return SUPPORTED_STYLES.hasOwnProperty(prop); } export function isSupportedTransformProp(prop: string): boolean { - return SUPPORTED_TRANSFORMS[prop] === true; + return SUPPORTED_TRANSFORMS.hasOwnProperty(prop); } diff --git a/packages/react-native/Libraries/Animated/animations/Animation.js b/packages/react-native/Libraries/Animated/animations/Animation.js index 8152fa30bccb3b..3df909742401bf 100644 --- a/packages/react-native/Libraries/Animated/animations/Animation.js +++ b/packages/react-native/Libraries/Animated/animations/Animation.js @@ -4,31 +4,31 @@ * 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 */ -'use strict'; - import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type AnimatedNode from '../nodes/AnimatedNode'; import type AnimatedValue from '../nodes/AnimatedValue'; -import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; +import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; import AnimatedProps from '../nodes/AnimatedProps'; export type EndResult = {finished: boolean, value?: number, ...}; export type EndCallback = (result: EndResult) => void; -export type AnimationConfig = { +export type AnimationConfig = $ReadOnly<{ isInteraction?: boolean, useNativeDriver: boolean, platformConfig?: PlatformConfig, onComplete?: ?EndCallback, iterations?: number, isLooping?: boolean, -}; + debugID?: ?string, + ... +}>; let startNativeAnimationNextId = 1; @@ -36,13 +36,27 @@ let startNativeAnimationNextId = 1; // Once an animation has been stopped or finished its course, it will // not be reused. export default class Animation { + #nativeID: ?number; + #onEnd: ?EndCallback; + #useNativeDriver: boolean; + __active: boolean; __isInteraction: boolean; - __onEnd: ?EndCallback; - __iterations: number; __isLooping: ?boolean; + __iterations: number; + __debugID: ?string; - _nativeId: number; + constructor(config: AnimationConfig) { + this.#useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config); + + this.__active = false; + this.__isInteraction = config.isInteraction ?? !this.#useNativeDriver; + this.__isLooping = config.isLooping; + this.__iterations = config.iterations ?? 1; + if (__DEV__) { + this.__debugID = config.debugID; + } + } start( fromValue: number, @@ -50,27 +64,44 @@ export default class Animation { onEnd: ?EndCallback, previousAnimation: ?Animation, animatedValue: AnimatedValue, - ): void {} + ): void { + if (!this.#useNativeDriver && animatedValue.__isNative === true) { + throw new Error( + 'Attempting to run JS driven animation on animated node ' + + 'that has been moved to "native" earlier by starting an ' + + 'animation with `useNativeDriver: true`', + ); + } + + this.#onEnd = onEnd; + this.__active = true; + } stop(): void { - if (this._nativeId) { - NativeAnimatedHelper.API.stopAnimation(this._nativeId); + if (this.#nativeID != null) { + const nativeID = this.#nativeID; + const identifier = `${nativeID}:stopAnimation`; + try { + // This is only required when singleOpBatching is used, as otherwise + // we flush calls immediately when there's no pending queue. + NativeAnimatedHelper.API.setWaitingForIdentifier(identifier); + NativeAnimatedHelper.API.stopAnimation(nativeID); + } finally { + NativeAnimatedHelper.API.unsetWaitingForIdentifier(identifier); + } } + this.__active = false; } - __getNativeAnimationConfig(): any { + __getNativeAnimationConfig(): $ReadOnly<{ + platformConfig: ?PlatformConfig, + ... + }> { // Subclasses that have corresponding animation implementation done in native // should override this method throw new Error('This animation type cannot be offloaded to native'); } - // Helper function for subclasses to make sure onEnd is only called once. - __debouncedOnEnd(result: EndResult): void { - const onEnd = this.__onEnd; - this.__onEnd = null; - onEnd && onEnd(result); - } - __findAnimatedPropsNodes(node: AnimatedNode): Array { const result = []; @@ -86,7 +117,11 @@ export default class Animation { return result; } - __startNativeAnimation(animatedValue: AnimatedValue): void { + __startAnimationIfNative(animatedValue: AnimatedValue): boolean { + if (!this.#useNativeDriver) { + return false; + } + const startNativeAnimationWaitId = `${startNativeAnimationNextId}:startAnimation`; startNativeAnimationNextId += 1; NativeAnimatedHelper.API.setWaitingForIdentifier( @@ -95,13 +130,13 @@ export default class Animation { try { const config = this.__getNativeAnimationConfig(); animatedValue.__makeNative(config.platformConfig); - this._nativeId = NativeAnimatedHelper.generateNewAnimationId(); + this.#nativeID = NativeAnimatedHelper.generateNewAnimationId(); NativeAnimatedHelper.API.startAnimatingNode( - this._nativeId, + this.#nativeID, animatedValue.__getNativeTag(), config, result => { - this.__debouncedOnEnd(result); + this.__notifyAnimationEnd(result); // When using natively driven animations, once the animation completes, // we need to ensure that the JS side nodes are synced with the updated @@ -112,7 +147,7 @@ export default class Animation { if ( ReactNativeFeatureFlags.shouldSkipStateUpdatesForLoopingAnimations() && - this.__isLooping + this.__isLooping === true ) { return; } @@ -125,6 +160,8 @@ export default class Animation { } }, ); + + return true; } catch (e) { throw e; } finally { @@ -133,4 +170,23 @@ export default class Animation { ); } } + + /** + * Notify the completion callback that the animation has ended. The completion + * callback will never be called more than once. + */ + __notifyAnimationEnd(result: EndResult): void { + const callback = this.#onEnd; + if (callback != null) { + this.#onEnd = null; + callback(result); + } + } + + __getDebugID(): ?string { + if (__DEV__) { + return this.__debugID; + } + return undefined; + } } diff --git a/packages/react-native/Libraries/Animated/animations/DecayAnimation.js b/packages/react-native/Libraries/Animated/animations/DecayAnimation.js index 915d913b6d8205..30c532ee4c1930 100644 --- a/packages/react-native/Libraries/Animated/animations/DecayAnimation.js +++ b/packages/react-native/Libraries/Animated/animations/DecayAnimation.js @@ -4,36 +4,35 @@ * 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 */ -'use strict'; - import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type AnimatedValue from '../nodes/AnimatedValue'; import type {AnimationConfig, EndCallback} from './Animation'; -import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import Animation from './Animation'; -export type DecayAnimationConfig = { +export type DecayAnimationConfig = $ReadOnly<{ ...AnimationConfig, velocity: | number - | { + | $ReadOnly<{ x: number, y: number, ... - }, + }>, deceleration?: number, -}; + ... +}>; -export type DecayAnimationConfigSingle = { +export type DecayAnimationConfigSingle = $ReadOnly<{ ...AnimationConfig, velocity: number, deceleration?: number, -}; + ... +}>; export default class DecayAnimation extends Animation { _startTime: number; @@ -42,33 +41,32 @@ export default class DecayAnimation extends Animation { _deceleration: number; _velocity: number; _onUpdate: (value: number) => void; - _animationFrame: any; - _useNativeDriver: boolean; + _animationFrame: ?AnimationFrameID; _platformConfig: ?PlatformConfig; constructor(config: DecayAnimationConfigSingle) { - super(); + super(config); + this._deceleration = config.deceleration ?? 0.998; this._velocity = config.velocity; - this._useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config); this._platformConfig = config.platformConfig; - this.__isInteraction = config.isInteraction ?? !this._useNativeDriver; - this.__iterations = config.iterations ?? 1; } - __getNativeAnimationConfig(): {| + __getNativeAnimationConfig(): $ReadOnly<{ deceleration: number, iterations: number, platformConfig: ?PlatformConfig, - type: $TEMPORARY$string<'decay'>, + type: 'decay', velocity: number, - |} { + ... + }> { return { type: 'decay', deceleration: this._deceleration, velocity: this._velocity, iterations: this.__iterations, platformConfig: this._platformConfig, + debugID: this.__getDebugID(), }; } @@ -79,26 +77,16 @@ export default class DecayAnimation extends Animation { previousAnimation: ?Animation, animatedValue: AnimatedValue, ): void { - this.__active = true; + super.start(fromValue, onUpdate, onEnd, previousAnimation, animatedValue); + this._lastValue = fromValue; this._fromValue = fromValue; this._onUpdate = onUpdate; - this.__onEnd = onEnd; this._startTime = Date.now(); - if (!this._useNativeDriver && animatedValue.__isNative === true) { - throw new Error( - 'Attempting to run JS driven animation on animated node ' + - 'that has been moved to "native" earlier by starting an ' + - 'animation with `useNativeDriver: true`', - ); - } - - if (this._useNativeDriver) { - this.__startNativeAnimation(animatedValue); - } else { - // $FlowFixMe[method-unbinding] added when improving typing for this parameters - this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this)); + const useNativeDriver = this.__startAnimationIfNative(animatedValue); + if (!useNativeDriver) { + this._animationFrame = requestAnimationFrame(() => this.onUpdate()); } } @@ -113,7 +101,7 @@ export default class DecayAnimation extends Animation { this._onUpdate(value); if (Math.abs(this._lastValue - value) < 0.1) { - this.__debouncedOnEnd({finished: true}); + this.__notifyAnimationEnd({finished: true}); return; } @@ -126,8 +114,9 @@ export default class DecayAnimation extends Animation { stop(): void { super.stop(); - this.__active = false; - global.cancelAnimationFrame(this._animationFrame); - this.__debouncedOnEnd({finished: false}); + if (this._animationFrame != null) { + global.cancelAnimationFrame(this._animationFrame); + } + this.__notifyAnimationEnd({finished: false}); } } diff --git a/packages/react-native/Libraries/Animated/animations/SpringAnimation.js b/packages/react-native/Libraries/Animated/animations/SpringAnimation.js index d5641ad95a28d1..1a28de90748f4f 100644 --- a/packages/react-native/Libraries/Animated/animations/SpringAnimation.js +++ b/packages/react-native/Libraries/Animated/animations/SpringAnimation.js @@ -4,25 +4,22 @@ * 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 */ -'use strict'; - import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type AnimatedInterpolation from '../nodes/AnimatedInterpolation'; import type AnimatedValue from '../nodes/AnimatedValue'; import type AnimatedValueXY from '../nodes/AnimatedValueXY'; import type {AnimationConfig, EndCallback} from './Animation'; -import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import AnimatedColor from '../nodes/AnimatedColor'; import * as SpringConfig from '../SpringConfig'; import Animation from './Animation'; import invariant from 'invariant'; -export type SpringAnimationConfig = { +export type SpringAnimationConfig = $ReadOnly<{ ...AnimationConfig, toValue: | number @@ -47,11 +44,11 @@ export type SpringAnimationConfig = { restSpeedThreshold?: number, velocity?: | number - | { + | $ReadOnly<{ x: number, y: number, ... - }, + }>, bounciness?: number, speed?: number, tension?: number, @@ -60,9 +57,10 @@ export type SpringAnimationConfig = { damping?: number, mass?: number, delay?: number, -}; + ... +}>; -export type SpringAnimationConfigSingle = { +export type SpringAnimationConfigSingle = $ReadOnly<{ ...AnimationConfig, toValue: number, overshootClamping?: boolean, @@ -77,7 +75,14 @@ export type SpringAnimationConfigSingle = { damping?: number, mass?: number, delay?: number, -}; + ... +}>; + +opaque type SpringAnimationInternalState = $ReadOnly<{ + lastPosition: number, + lastVelocity: number, + lastTime: number, +}>; export default class SpringAnimation extends Animation { _overshootClamping: boolean; @@ -93,17 +98,16 @@ export default class SpringAnimation extends Animation { _mass: number; _initialVelocity: number; _delay: number; - _timeout: any; + _timeout: ?TimeoutID; _startTime: number; _lastTime: number; _frameTime: number; _onUpdate: (value: number) => void; - _animationFrame: any; - _useNativeDriver: boolean; + _animationFrame: ?AnimationFrameID; _platformConfig: ?PlatformConfig; constructor(config: SpringAnimationConfigSingle) { - super(); + super(config); this._overshootClamping = config.overshootClamping ?? false; this._restDisplacementThreshold = config.restDisplacementThreshold ?? 0.001; @@ -112,10 +116,7 @@ export default class SpringAnimation extends Animation { this._lastVelocity = config.velocity ?? 0; this._toValue = config.toValue; this._delay = config.delay ?? 0; - this._useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config); this._platformConfig = config.platformConfig; - this.__isInteraction = config.isInteraction ?? !this._useNativeDriver; - this.__iterations = config.iterations ?? 1; if ( config.stiffness !== undefined || @@ -167,7 +168,7 @@ export default class SpringAnimation extends Animation { invariant(this._mass > 0, 'Mass value must be greater than 0'); } - __getNativeAnimationConfig(): {| + __getNativeAnimationConfig(): $ReadOnly<{ damping: number, initialVelocity: number, iterations: number, @@ -177,9 +178,10 @@ export default class SpringAnimation extends Animation { restDisplacementThreshold: number, restSpeedThreshold: number, stiffness: number, - toValue: any, - type: $TEMPORARY$string<'spring'>, - |} { + toValue: number, + type: 'spring', + ... + }> { return { type: 'spring', overshootClamping: this._overshootClamping, @@ -192,6 +194,7 @@ export default class SpringAnimation extends Animation { toValue: this._toValue, iterations: this.__iterations, platformConfig: this._platformConfig, + debugID: this.__getDebugID(), }; } @@ -202,12 +205,12 @@ export default class SpringAnimation extends Animation { previousAnimation: ?Animation, animatedValue: AnimatedValue, ): void { - this.__active = true; + super.start(fromValue, onUpdate, onEnd, previousAnimation, animatedValue); + this._startPosition = fromValue; this._lastPosition = this._startPosition; this._onUpdate = onUpdate; - this.__onEnd = onEnd; this._lastTime = Date.now(); this._frameTime = 0.0; @@ -221,17 +224,8 @@ export default class SpringAnimation extends Animation { } const start = () => { - if (!this._useNativeDriver && animatedValue.__isNative === true) { - throw new Error( - 'Attempting to run JS driven animation on animated node ' + - 'that has been moved to "native" earlier by starting an ' + - 'animation with `useNativeDriver: true`', - ); - } - - if (this._useNativeDriver) { - this.__startNativeAnimation(animatedValue); - } else { + const useNativeDriver = this.__startAnimationIfNative(animatedValue); + if (!useNativeDriver) { this.onUpdate(); } }; @@ -244,7 +238,7 @@ export default class SpringAnimation extends Animation { } } - getInternalState(): Object { + getInternalState(): SpringAnimationInternalState { return { lastPosition: this._lastPosition, lastVelocity: this._lastVelocity, @@ -361,7 +355,7 @@ export default class SpringAnimation extends Animation { this._onUpdate(this._toValue); } - this.__debouncedOnEnd({finished: true}); + this.__notifyAnimationEnd({finished: true}); return; } // $FlowFixMe[method-unbinding] added when improving typing for this parameters @@ -370,9 +364,10 @@ export default class SpringAnimation extends Animation { stop(): void { super.stop(); - this.__active = false; clearTimeout(this._timeout); - global.cancelAnimationFrame(this._animationFrame); - this.__debouncedOnEnd({finished: false}); + if (this._animationFrame != null) { + global.cancelAnimationFrame(this._animationFrame); + } + this.__notifyAnimationEnd({finished: false}); } } diff --git a/packages/react-native/Libraries/Animated/animations/TimingAnimation.js b/packages/react-native/Libraries/Animated/animations/TimingAnimation.js index c4436f25ffc132..3114d55105b22e 100644 --- a/packages/react-native/Libraries/Animated/animations/TimingAnimation.js +++ b/packages/react-native/Libraries/Animated/animations/TimingAnimation.js @@ -4,12 +4,10 @@ * 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 */ -'use strict'; - import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type {RgbaValue} from '../nodes/AnimatedColor'; import type AnimatedInterpolation from '../nodes/AnimatedInterpolation'; @@ -17,7 +15,6 @@ import type AnimatedValue from '../nodes/AnimatedValue'; import type AnimatedValueXY from '../nodes/AnimatedValueXY'; import type {AnimationConfig, EndCallback} from './Animation'; -import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import AnimatedColor from '../nodes/AnimatedColor'; import Animation from './Animation'; @@ -26,11 +23,11 @@ export type TimingAnimationConfig = $ReadOnly<{ toValue: | number | AnimatedValue - | { + | $ReadOnly<{ x: number, y: number, ... - } + }> | AnimatedValueXY | RgbaValue | AnimatedColor @@ -38,6 +35,7 @@ export type TimingAnimationConfig = $ReadOnly<{ easing?: (value: number) => number, duration?: number, delay?: number, + ... }>; export type TimingAnimationConfigSingle = $ReadOnly<{ @@ -46,6 +44,7 @@ export type TimingAnimationConfigSingle = $ReadOnly<{ easing?: (value: number) => number, duration?: number, delay?: number, + ... }>; let _easeInOut; @@ -65,25 +64,28 @@ export default class TimingAnimation extends Animation { _delay: number; _easing: (value: number) => number; _onUpdate: (value: number) => void; - _animationFrame: any; - _timeout: any; - _useNativeDriver: boolean; + _animationFrame: ?AnimationFrameID; + _timeout: ?TimeoutID; _platformConfig: ?PlatformConfig; constructor(config: TimingAnimationConfigSingle) { - super(); + super(config); + this._toValue = config.toValue; this._easing = config.easing ?? easeInOut(); this._duration = config.duration ?? 500; this._delay = config.delay ?? 0; - this.__iterations = config.iterations ?? 1; - this._useNativeDriver = NativeAnimatedHelper.shouldUseNativeDriver(config); this._platformConfig = config.platformConfig; - this.__isInteraction = config.isInteraction ?? !this._useNativeDriver; - this.__isLooping = config.isLooping; } - __getNativeAnimationConfig(): any { + __getNativeAnimationConfig(): $ReadOnly<{ + type: 'frames', + frames: $ReadOnlyArray, + toValue: number, + iterations: number, + platformConfig: ?PlatformConfig, + ... + }> { const frameDuration = 1000.0 / 60.0; const frames = []; const numFrames = Math.round(this._duration / frameDuration); @@ -97,6 +99,7 @@ export default class TimingAnimation extends Animation { toValue: this._toValue, iterations: this.__iterations, platformConfig: this._platformConfig, + debugID: this.__getDebugID(), }; } @@ -107,35 +110,24 @@ export default class TimingAnimation extends Animation { previousAnimation: ?Animation, animatedValue: AnimatedValue, ): void { - this.__active = true; + super.start(fromValue, onUpdate, onEnd, previousAnimation, animatedValue); + this._fromValue = fromValue; this._onUpdate = onUpdate; - this.__onEnd = onEnd; const start = () => { - if (!this._useNativeDriver && animatedValue.__isNative === true) { - throw new Error( - 'Attempting to run JS driven animation on animated node ' + - 'that has been moved to "native" earlier by starting an ' + - 'animation with `useNativeDriver: true`', - ); - } + this._startTime = Date.now(); - // Animations that sometimes have 0 duration and sometimes do not - // still need to use the native driver when duration is 0 so as to - // not cause intermixed JS and native animations. - if (this._duration === 0 && !this._useNativeDriver) { - this._onUpdate(this._toValue); - this.__debouncedOnEnd({finished: true}); - } else { - this._startTime = Date.now(); - if (this._useNativeDriver) { - this.__startNativeAnimation(animatedValue); + const useNativeDriver = this.__startAnimationIfNative(animatedValue); + if (!useNativeDriver) { + // Animations that sometimes have 0 duration and sometimes do not + // still need to use the native driver when duration is 0 so as to + // not cause intermixed JS and native animations. + if (this._duration === 0) { + this._onUpdate(this._toValue); + this.__notifyAnimationEnd({finished: true}); } else { - this._animationFrame = requestAnimationFrame( - // $FlowFixMe[method-unbinding] added when improving typing for this parameters - this.onUpdate.bind(this), - ); + this._animationFrame = requestAnimationFrame(() => this.onUpdate()); } } }; @@ -156,7 +148,7 @@ export default class TimingAnimation extends Animation { this._fromValue + this._easing(1) * (this._toValue - this._fromValue), ); } - this.__debouncedOnEnd({finished: true}); + this.__notifyAnimationEnd({finished: true}); return; } @@ -173,9 +165,10 @@ export default class TimingAnimation extends Animation { stop(): void { super.stop(); - this.__active = false; clearTimeout(this._timeout); - global.cancelAnimationFrame(this._animationFrame); - this.__debouncedOnEnd({finished: false}); + if (this._animationFrame != null) { + global.cancelAnimationFrame(this._animationFrame); + } + this.__notifyAnimationEnd({finished: false}); } } diff --git a/packages/react-native/Libraries/Animated/components/AnimatedFlatList.js b/packages/react-native/Libraries/Animated/components/AnimatedFlatList.js index adfb8639585656..aabdf6a00e8c47 100644 --- a/packages/react-native/Libraries/Animated/components/AnimatedFlatList.js +++ b/packages/react-native/Libraries/Animated/components/AnimatedFlatList.js @@ -16,5 +16,5 @@ import * as React from 'react'; export default (createAnimatedComponent(FlatList): AnimatedComponentType< React.ElementConfig, - React.ElementRef, + FlatList, >); diff --git a/packages/react-native/Libraries/Animated/components/AnimatedSectionList.js b/packages/react-native/Libraries/Animated/components/AnimatedSectionList.js index 5b2f80c2c53eab..c7b47147931967 100644 --- a/packages/react-native/Libraries/Animated/components/AnimatedSectionList.js +++ b/packages/react-native/Libraries/Animated/components/AnimatedSectionList.js @@ -8,6 +8,7 @@ * @format */ +import type {SectionBase} from '../../Lists/SectionList'; import type {AnimatedComponentType} from '../createAnimatedComponent'; import SectionList from '../../Lists/SectionList'; @@ -16,5 +17,6 @@ import * as React from 'react'; export default (createAnimatedComponent(SectionList): AnimatedComponentType< React.ElementConfig, - React.ElementRef, + // $FlowExpectedError[unclear-type] + SectionList>, >); diff --git a/packages/react-native/Libraries/Animated/createAnimatedComponent.js b/packages/react-native/Libraries/Animated/createAnimatedComponent.js index 8c9fbea9c57825..cef22cd810fd45 100644 --- a/packages/react-native/Libraries/Animated/createAnimatedComponent.js +++ b/packages/react-native/Libraries/Animated/createAnimatedComponent.js @@ -8,6 +8,8 @@ * @format */ +import type {AnimatedPropsAllowlist} from './nodes/AnimatedProps'; + import composeStyles from '../../src/private/styles/composeStyles'; import View from '../Components/View/View'; import useMergeRefs from '../Utilities/useMergeRefs'; @@ -25,46 +27,71 @@ export type AnimatedProps = { }>)]: any, }; -export type AnimatedComponentType< +// We could use a mapped type here to introduce acceptable Animated variants +// of properties, instead of doing so in the core StyleSheetTypes +// Inexact Props are not supported, they'll be made exact here. +export type StrictAnimatedProps = $ReadOnly<{ + ...$Exact, + passthroughAnimatedPropExplicitValues?: ?Props, +}>; + +export type AnimatedComponentType = component( + ref: React.RefSetter, + ...AnimatedProps +); + +export type StrictAnimatedComponentType< Props: {...}, +Instance = mixed, -> = React.AbstractComponent, Instance>; +> = component(ref: React.RefSetter, ...StrictAnimatedProps); export default function createAnimatedComponent( - Component: React.AbstractComponent, + Component: component(ref: React.RefSetter, ...TProps), ): AnimatedComponentType { - const AnimatedComponent = React.forwardRef, TInstance>( - (props, forwardedRef) => { - const [reducedProps, callbackRef] = useAnimatedProps( - // $FlowFixMe[incompatible-call] - props, - ); - const ref = useMergeRefs(callbackRef, forwardedRef); + return unstable_createAnimatedComponentWithAllowlist(Component, null); +} + +export function unstable_createAnimatedComponentWithAllowlist< + TProps: {...}, + TInstance, +>( + Component: component(ref: React.RefSetter, ...TProps), + allowlist: ?AnimatedPropsAllowlist, +): StrictAnimatedComponentType { + const AnimatedComponent = React.forwardRef< + StrictAnimatedProps, + TInstance, + >((props, forwardedRef) => { + const [reducedProps, callbackRef] = useAnimatedProps( + // $FlowFixMe[incompatible-call] + props, + allowlist, + ); + const ref = useMergeRefs(callbackRef, forwardedRef); - // Some components require explicit passthrough values for animation - // to work properly. For example, if an animated component is - // transformed and Pressable, onPress will not work after transform - // without these passthrough values. - // $FlowFixMe[prop-missing] - const {passthroughAnimatedPropExplicitValues, style} = reducedProps; - const passthroughStyle = passthroughAnimatedPropExplicitValues?.style; - const mergedStyle = useMemo( - () => composeStyles(style, passthroughStyle), - [passthroughStyle, style], - ); + // Some components require explicit passthrough values for animation + // to work properly. For example, if an animated component is + // transformed and Pressable, onPress will not work after transform + // without these passthrough values. + // $FlowFixMe[prop-missing] + const {passthroughAnimatedPropExplicitValues, style} = reducedProps; + const passthroughStyle = passthroughAnimatedPropExplicitValues?.style; + const mergedStyle = useMemo( + () => composeStyles(style, passthroughStyle), + [passthroughStyle, style], + ); - // NOTE: It is important that `passthroughAnimatedPropExplicitValues` is - // spread after `reducedProps` but before `style`. - return ( - - ); - }, - ); + // NOTE: It is important that `passthroughAnimatedPropExplicitValues` is + // spread after `reducedProps` but before `style`. + return ( + + ); + }); AnimatedComponent.displayName = `Animated(${ Component.displayName || 'Anonymous' diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js b/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js index 7a48965f5dd545..5dc40064c8d74e 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedAddition.js @@ -13,6 +13,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type {InterpolationConfigType} from './AnimatedInterpolation'; import type AnimatedNode from './AnimatedNode'; +import type {AnimatedNodeConfig} from './AnimatedNode'; import AnimatedInterpolation from './AnimatedInterpolation'; import AnimatedValue from './AnimatedValue'; @@ -22,8 +23,12 @@ export default class AnimatedAddition extends AnimatedWithChildren { _a: AnimatedNode; _b: AnimatedNode; - constructor(a: AnimatedNode | number, b: AnimatedNode | number) { - super(); + constructor( + a: AnimatedNode | number, + b: AnimatedNode | number, + config?: ?AnimatedNodeConfig, + ) { + super(config); this._a = typeof a === 'number' ? new AnimatedValue(a) : a; this._b = typeof b === 'number' ? new AnimatedValue(b) : b; } @@ -59,6 +64,7 @@ export default class AnimatedAddition extends AnimatedWithChildren { return { type: 'addition', input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedColor.js b/packages/react-native/Libraries/Animated/nodes/AnimatedColor.js index 4485df1f3fd664..c1481cc21fddbd 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedColor.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedColor.js @@ -14,14 +14,16 @@ import type {ProcessedColorValue} from '../../StyleSheet/processColor'; import type {ColorValue} from '../../StyleSheet/StyleSheet'; import type {NativeColorValue} from '../../StyleSheet/StyleSheetTypes'; import type {PlatformConfig} from '../AnimatedPlatformConfig'; +import type {AnimatedNodeConfig} from './AnimatedNode'; +import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import normalizeColor from '../../StyleSheet/normalizeColor'; import {processColorObject} from '../../StyleSheet/PlatformColorValueTypes'; -import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import AnimatedValue, {flushValue} from './AnimatedValue'; import AnimatedWithChildren from './AnimatedWithChildren'; export type AnimatedColorConfig = $ReadOnly<{ + ...AnimatedNodeConfig, useNativeDriver: boolean, }>; @@ -118,7 +120,7 @@ export default class AnimatedColor extends AnimatedWithChildren { _suspendCallbacks: number = 0; constructor(valueIn?: InputValue, config?: ?AnimatedColorConfig) { - super(); + super(config); let value: RgbaValue | RgbaAnimatedValue | ColorValue = valueIn ?? defaultColor; @@ -315,6 +317,7 @@ export default class AnimatedColor extends AnimatedWithChildren { b: this.b.__getNativeTag(), a: this.a.__getNativeTag(), nativeColor: this.nativeColor, + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js b/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js index d2ac11ae764a11..242bd840442da2 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedDiffClamp.js @@ -13,6 +13,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type {InterpolationConfigType} from './AnimatedInterpolation'; import type AnimatedNode from './AnimatedNode'; +import type {AnimatedNodeConfig} from './AnimatedNode'; import AnimatedInterpolation from './AnimatedInterpolation'; import AnimatedWithChildren from './AnimatedWithChildren'; @@ -24,8 +25,13 @@ export default class AnimatedDiffClamp extends AnimatedWithChildren { _value: number; _lastValue: number; - constructor(a: AnimatedNode, min: number, max: number) { - super(); + constructor( + a: AnimatedNode, + min: number, + max: number, + config?: ?AnimatedNodeConfig, + ) { + super(config); this._a = a; this._min = min; @@ -67,6 +73,7 @@ export default class AnimatedDiffClamp extends AnimatedWithChildren { input: this._a.__getNativeTag(), min: this._min, max: this._max, + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js b/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js index 158a0f3d7805f2..4abbb362f4ea0c 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedDivision.js @@ -12,6 +12,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type {InterpolationConfigType} from './AnimatedInterpolation'; +import type {AnimatedNodeConfig} from './AnimatedNode'; import AnimatedInterpolation from './AnimatedInterpolation'; import AnimatedNode from './AnimatedNode'; @@ -23,8 +24,12 @@ export default class AnimatedDivision extends AnimatedWithChildren { _b: AnimatedNode; _warnedAboutDivideByZero: boolean = false; - constructor(a: AnimatedNode | number, b: AnimatedNode | number) { - super(); + constructor( + a: AnimatedNode | number, + b: AnimatedNode | number, + config?: ?AnimatedNodeConfig, + ) { + super(config); if (b === 0 || (b instanceof AnimatedNode && b.__getValue() === 0)) { console.error('Detected potential division by zero in AnimatedDivision'); } @@ -75,6 +80,7 @@ export default class AnimatedDivision extends AnimatedWithChildren { return { type: 'division', input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js b/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js index 7d191938ab0abc..e425d1cbf48c99 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedInterpolation.js @@ -14,18 +14,20 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type AnimatedNode from './AnimatedNode'; +import type {AnimatedNodeConfig} from './AnimatedNode'; +import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import {validateInterpolation} from '../../../src/private/animated/NativeAnimatedValidation'; import normalizeColor from '../../StyleSheet/normalizeColor'; import processColor from '../../StyleSheet/processColor'; import Easing from '../Easing'; -import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import AnimatedWithChildren from './AnimatedWithChildren'; import invariant from 'invariant'; type ExtrapolateType = 'extend' | 'identity' | 'clamp'; export type InterpolationConfigType = $ReadOnly<{ + ...AnimatedNodeConfig, inputRange: $ReadOnlyArray, outputRange: $ReadOnlyArray, easing?: (input: number) => number, @@ -327,7 +329,7 @@ export default class AnimatedInterpolation< _interpolation: ?(input: number) => OutputT; constructor(parent: AnimatedNode, config: InterpolationConfigType) { - super(); + super(config); this._parent = parent; this._config = config; @@ -411,6 +413,7 @@ export default class AnimatedInterpolation< extrapolateRight: this._config.extrapolateRight || this._config.extrapolate || 'extend', type: 'interpolation', + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js b/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js index d5334478afd406..3dd59b88ee6365 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedModulo.js @@ -13,6 +13,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type {InterpolationConfigType} from './AnimatedInterpolation'; import type AnimatedNode from './AnimatedNode'; +import type {AnimatedNodeConfig} from './AnimatedNode'; import AnimatedInterpolation from './AnimatedInterpolation'; import AnimatedWithChildren from './AnimatedWithChildren'; @@ -21,8 +22,8 @@ export default class AnimatedModulo extends AnimatedWithChildren { _a: AnimatedNode; _modulus: number; - constructor(a: AnimatedNode, modulus: number) { - super(); + constructor(a: AnimatedNode, modulus: number, config?: ?AnimatedNodeConfig) { + super(config); this._a = a; this._modulus = modulus; } @@ -58,6 +59,7 @@ export default class AnimatedModulo extends AnimatedWithChildren { type: 'modulus', input: this._a.__getNativeTag(), modulus: this._modulus, + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js b/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js index b3cfaf3aed3ff5..3e2f66efa110de 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedMultiplication.js @@ -13,6 +13,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type {InterpolationConfigType} from './AnimatedInterpolation'; import type AnimatedNode from './AnimatedNode'; +import type {AnimatedNodeConfig} from './AnimatedNode'; import AnimatedInterpolation from './AnimatedInterpolation'; import AnimatedValue from './AnimatedValue'; @@ -22,8 +23,12 @@ export default class AnimatedMultiplication extends AnimatedWithChildren { _a: AnimatedNode; _b: AnimatedNode; - constructor(a: AnimatedNode | number, b: AnimatedNode | number) { - super(); + constructor( + a: AnimatedNode | number, + b: AnimatedNode | number, + config?: ?AnimatedNodeConfig, + ) { + super(config); this._a = typeof a === 'number' ? new AnimatedValue(a) : a; this._b = typeof b === 'number' ? new AnimatedValue(b) : b; } @@ -58,6 +63,7 @@ export default class AnimatedMultiplication extends AnimatedWithChildren { return { type: 'multiplication', input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js b/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js index 6f4ba16ce20378..e09a2c7edea239 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedNode.js @@ -8,8 +8,6 @@ * @format */ -'use strict'; - import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; import type {PlatformConfig} from '../AnimatedPlatformConfig'; @@ -21,6 +19,10 @@ const {startListeningToAnimatedNodeValue, stopListeningToAnimatedNodeValue} = type ValueListenerCallback = (state: {value: number, ...}) => mixed; +export type AnimatedNodeConfig = $ReadOnly<{ + debugID?: string, +}>; + let _uniqueId = 1; let _assertNativeAnimatedModule: ?() => void = () => { NativeAnimatedHelper.assertNativeAnimatedModule(); @@ -29,12 +31,23 @@ let _assertNativeAnimatedModule: ?() => void = () => { _assertNativeAnimatedModule = null; }; -// Note(vjeux): this would be better as an interface but flow doesn't -// support them yet export default class AnimatedNode { #listeners: Map = new Map(); + #updateSubscription: ?EventSubscription = null; + _platformConfig: ?PlatformConfig = undefined; - __nativeAnimatedValueListener: ?EventSubscription = null; + + constructor( + config?: ?$ReadOnly<{ + ...AnimatedNodeConfig, + ... + }>, + ) { + if (__DEV__) { + this.__debugID = config?.debugID; + } + } + __attach(): void {} __detach(): void { this.removeAllListeners(); @@ -56,16 +69,17 @@ export default class AnimatedNode { /* Methods and props used by native Animated impl */ __isNative: boolean = false; __nativeTag: ?number = undefined; - __shouldUpdateListenersForNewNativeTag: boolean = false; __makeNative(platformConfig: ?PlatformConfig): void { - if (!this.__isNative) { - throw new Error('This node cannot be made a "native" animated node'); - } + // Subclasses are expected to set `__isNative` to true before this. + invariant( + this.__isNative, + 'This node cannot be made a "native" animated node', + ); this._platformConfig = platformConfig; if (this.#listeners.size > 0) { - this._startListeningToNativeValueUpdates(); + this.#ensureUpdateSubscriptionExists(); } } @@ -80,7 +94,7 @@ export default class AnimatedNode { const id = String(_uniqueId++); this.#listeners.set(id, callback); if (this.__isNative) { - this._startListeningToNativeValueUpdates(); + this.#ensureUpdateSubscriptionExists(); } return id; } @@ -94,7 +108,7 @@ export default class AnimatedNode { removeListener(id: string): void { this.#listeners.delete(id); if (this.__isNative && this.#listeners.size === 0) { - this._stopListeningForNativeValueUpdates(); + this.#updateSubscription?.remove(); } } @@ -106,7 +120,7 @@ export default class AnimatedNode { removeAllListeners(): void { this.#listeners.clear(); if (this.__isNative) { - this._stopListeningForNativeValueUpdates(); + this.#updateSubscription?.remove(); } } @@ -114,33 +128,36 @@ export default class AnimatedNode { return this.#listeners.size > 0; } - _startListeningToNativeValueUpdates() { - if ( - this.__nativeAnimatedValueListener && - !this.__shouldUpdateListenersForNewNativeTag - ) { + #ensureUpdateSubscriptionExists(): void { + if (this.#updateSubscription != null) { return; } - - if (this.__shouldUpdateListenersForNewNativeTag) { - this.__shouldUpdateListenersForNewNativeTag = false; - this._stopListeningForNativeValueUpdates(); - } - - startListeningToAnimatedNodeValue(this.__getNativeTag()); - this.__nativeAnimatedValueListener = + const nativeTag = this.__getNativeTag(); + startListeningToAnimatedNodeValue(nativeTag); + const subscription: EventSubscription = NativeAnimatedHelper.nativeEventEmitter.addListener( 'onAnimatedValueUpdate', data => { - if (data.tag !== this.__getNativeTag()) { - return; + if (data.tag === nativeTag) { + this.__onAnimatedValueUpdateReceived(data.value); } - this.__onAnimatedValueUpdateReceived(data.value); }, ); + + this.#updateSubscription = { + remove: () => { + // Only this function assigns to `this.#updateSubscription`. + if (this.#updateSubscription == null) { + return; + } + this.#updateSubscription = null; + subscription.remove(); + stopListeningToAnimatedNodeValue(nativeTag); + }, + }; } - __onAnimatedValueUpdateReceived(value: number) { + __onAnimatedValueUpdateReceived(value: number): void { this.__callListeners(value); } @@ -151,16 +168,6 @@ export default class AnimatedNode { }); } - _stopListeningForNativeValueUpdates() { - if (!this.__nativeAnimatedValueListener) { - return; - } - - this.__nativeAnimatedValueListener.remove(); - this.__nativeAnimatedValueListener = null; - stopListeningToAnimatedNodeValue(this.__getNativeTag()); - } - __getNativeTag(): number { let nativeTag = this.__nativeTag; if (nativeTag == null) { @@ -181,7 +188,6 @@ export default class AnimatedNode { config.platformConfig = this._platformConfig; } NativeAnimatedHelper.API.createAnimatedNode(nativeTag, config); - this.__shouldUpdateListenersForNewNativeTag = true; } return nativeTag; } @@ -192,10 +198,6 @@ export default class AnimatedNode { ); } - toJSON(): any { - return this.__getValue(); - } - __getPlatformConfig(): ?PlatformConfig { return this._platformConfig; } @@ -203,4 +205,21 @@ export default class AnimatedNode { __setPlatformConfig(platformConfig: ?PlatformConfig) { this._platformConfig = platformConfig; } + + /** + * NOTE: This is intended to prevent `JSON.stringify` from throwing "cyclic + * structure" errors in React DevTools. Avoid depending on this! + */ + toJSON(): mixed { + return this.__getValue(); + } + + __debugID: ?string = undefined; + + __getDebugID(): ?string { + if (__DEV__) { + return this.__debugID; + } + return undefined; + } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js b/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js index 2f3ff765de60ff..8077ed9d22fbe0 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedObject.js @@ -12,6 +12,7 @@ 'use strict'; import type {PlatformConfig} from '../AnimatedPlatformConfig'; +import type {AnimatedNodeConfig} from './AnimatedNode'; import AnimatedNode from './AnimatedNode'; import AnimatedWithChildren from './AnimatedWithChildren'; @@ -19,9 +20,11 @@ import * as React from 'react'; const MAX_DEPTH = 5; -/* $FlowIssue[incompatible-type-guard] - Flow does not know that the prototype - and ReactElement checks preserve the type refinement of `value`. */ -function isPlainObject(value: mixed): value is $ReadOnly<{[string]: mixed}> { +export function isPlainObject( + value: mixed, + /* $FlowIssue[incompatible-type-guard] - Flow does not know that the prototype + and ReactElement checks preserve the type refinement of `value`. */ +): value is $ReadOnly<{[string]: mixed}> { return ( value !== null && typeof value === 'object' && @@ -97,8 +100,12 @@ export default class AnimatedObject extends AnimatedWithChildren { /** * Should only be called by `AnimatedObject.from`. */ - constructor(nodes: $ReadOnlyArray, value: mixed) { - super(); + constructor( + nodes: $ReadOnlyArray, + value: mixed, + config?: ?AnimatedNodeConfig, + ) { + super(config); this.#nodes = nodes; this._value = value; } @@ -109,6 +116,14 @@ export default class AnimatedObject extends AnimatedWithChildren { }); } + __getValueWithStaticObject(staticObject: mixed): any { + const nodes = this.#nodes; + let index = 0; + // NOTE: We can depend on `this._value` and `staticObject` sharing a + // structure because of `useAnimatedPropsMemo`. + return mapAnimatedNodes(staticObject, () => nodes[index++].__getValue()); + } + __getAnimatedValue(): any { return mapAnimatedNodes(this._value, node => { return node.__getAnimatedValue(); @@ -147,6 +162,7 @@ export default class AnimatedObject extends AnimatedWithChildren { value: mapAnimatedNodes(this._value, node => { return {nodeTag: node.__getNativeTag()}; }), + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js index e0b642fd64feff..80e29a5704c0ac 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedProps.js @@ -8,42 +8,45 @@ * @format */ -'use strict'; - import type {PlatformConfig} from '../AnimatedPlatformConfig'; +import type {AnimatedNodeConfig} from './AnimatedNode'; +import type {AnimatedStyleAllowlist} from './AnimatedStyle'; +import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import {findNodeHandle} from '../../ReactNative/RendererProxy'; import {AnimatedEvent} from '../AnimatedEvent'; -import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; import AnimatedNode from './AnimatedNode'; import AnimatedObject from './AnimatedObject'; import AnimatedStyle from './AnimatedStyle'; import invariant from 'invariant'; +export type AnimatedPropsAllowlist = $ReadOnly<{ + style?: ?AnimatedStyleAllowlist, + [string]: true, +}>; + function createAnimatedProps( - inputProps: Object, -): [$ReadOnlyArray, $ReadOnlyArray, Object] { + inputProps: {[string]: mixed}, + allowlist: ?AnimatedPropsAllowlist, +): [$ReadOnlyArray, $ReadOnlyArray, {[string]: mixed}] { const nodeKeys: Array = []; const nodes: Array = []; - const props: Object = {}; + const props: {[string]: mixed} = {}; const keys = Object.keys(inputProps); for (let ii = 0, length = keys.length; ii < length; ii++) { const key = keys[ii]; const value = inputProps[key]; - if (key === 'style') { - const node = new AnimatedStyle(value); - nodeKeys.push(key); - nodes.push(node); - props[key] = node; - } else if (value instanceof AnimatedNode) { - const node = value; - nodeKeys.push(key); - nodes.push(node); - props[key] = node; - } else { - const node = AnimatedObject.from(value); + if (allowlist == null || hasOwn(allowlist, key)) { + let node; + if (key === 'style') { + node = AnimatedStyle.from(value, allowlist?.style); + } else if (value instanceof AnimatedNode) { + node = value; + } else { + node = AnimatedObject.from(value); + } if (node == null) { props[key] = value; } else { @@ -51,6 +54,20 @@ function createAnimatedProps( nodes.push(node); props[key] = node; } + } else { + if (__DEV__) { + // WARNING: This is a potentially expensive check that we should only + // do in development. Without this check in development, it might be + // difficult to identify which props need to be allowlisted. + if (AnimatedObject.from(inputProps[key]) != null) { + console.error( + `AnimatedProps: ${key} is not allowlisted for animation, but it ` + + 'contains AnimatedNode values; props allowing animation: ', + allowlist, + ); + } + } + props[key] = value; } } @@ -58,29 +75,33 @@ function createAnimatedProps( } export default class AnimatedProps extends AnimatedNode { + #animatedView: any = null; + #callback: () => void; #nodeKeys: $ReadOnlyArray; #nodes: $ReadOnlyArray; - - _animatedView: any = null; - _props: Object; - _callback: () => void; - - constructor(inputProps: Object, callback: () => void) { - super(); - const [nodeKeys, nodes, props] = createAnimatedProps(inputProps); + #props: {[string]: mixed}; + + constructor( + inputProps: {[string]: mixed}, + callback: () => void, + allowlist?: ?AnimatedPropsAllowlist, + config?: ?AnimatedNodeConfig, + ) { + super(config); + const [nodeKeys, nodes, props] = createAnimatedProps(inputProps, allowlist); this.#nodeKeys = nodeKeys; this.#nodes = nodes; - this._props = props; - this._callback = callback; + this.#props = props; + this.#callback = callback; } __getValue(): Object { - const props: {[string]: any | ((...args: any) => void)} = {}; + const props: {[string]: mixed} = {}; - const keys = Object.keys(this._props); + const keys = Object.keys(this.#props); for (let ii = 0, length = keys.length; ii < length; ii++) { const key = keys[ii]; - const value = this._props[key]; + const value = this.#props[key]; if (value instanceof AnimatedNode) { props[key] = value.__getValue(); @@ -94,8 +115,33 @@ export default class AnimatedProps extends AnimatedNode { return props; } + /** + * Creates a new `props` object that contains the same props as the supplied + * `staticProps` object, except with animated nodes for any props that were + * created by this `AnimatedProps` instance. + */ + __getValueWithStaticProps(staticProps: Object): Object { + const props: {[string]: mixed} = {...staticProps}; + + const keys = Object.keys(staticProps); + for (let ii = 0, length = keys.length; ii < length; ii++) { + const key = keys[ii]; + const maybeNode = this.#props[key]; + + if (key === 'style' && maybeNode instanceof AnimatedStyle) { + props[key] = maybeNode.__getValueWithStaticStyle(staticProps.style); + } else if (maybeNode instanceof AnimatedNode) { + props[key] = maybeNode.__getValue(); + } else if (maybeNode instanceof AnimatedEvent) { + props[key] = maybeNode.__getHandler(); + } + } + + return props; + } + __getAnimatedValue(): Object { - const props: {[string]: any} = {}; + const props: {[string]: mixed} = {}; const nodeKeys = this.#nodeKeys; const nodes = this.#nodes; @@ -117,10 +163,10 @@ export default class AnimatedProps extends AnimatedNode { } __detach(): void { - if (this.__isNative && this._animatedView) { + if (this.__isNative && this.#animatedView) { this.__disconnectAnimatedView(); } - this._animatedView = null; + this.#animatedView = null; const nodes = this.#nodes; for (let ii = 0, length = nodes.length; ii < length; ii++) { @@ -132,7 +178,7 @@ export default class AnimatedProps extends AnimatedNode { } update(): void { - this._callback(); + this.#callback(); } __makeNative(platformConfig: ?PlatformConfig): void { @@ -150,17 +196,17 @@ export default class AnimatedProps extends AnimatedNode { // where it will be needed to traverse the graph of attached values. super.__setPlatformConfig(platformConfig); - if (this._animatedView) { + if (this.#animatedView) { this.__connectAnimatedView(); } } } setNativeView(animatedView: any): void { - if (this._animatedView === animatedView) { + if (this.#animatedView === animatedView) { return; } - this._animatedView = animatedView; + this.#animatedView = animatedView; if (this.__isNative) { this.__connectAnimatedView(); } @@ -168,11 +214,14 @@ export default class AnimatedProps extends AnimatedNode { __connectAnimatedView(): void { invariant(this.__isNative, 'Expected node to be marked as "native"'); - const nativeViewTag: ?number = findNodeHandle(this._animatedView); - invariant( - nativeViewTag != null, - 'Unable to locate attached view in the native tree', - ); + let nativeViewTag: ?number = findNodeHandle(this.#animatedView); + if (nativeViewTag == null) { + if (process.env.NODE_ENV === 'test') { + nativeViewTag = -1; + } else { + throw new Error('Unable to locate attached view in the native tree'); + } + } NativeAnimatedHelper.API.connectAnimatedNodeToView( this.__getNativeTag(), nativeViewTag, @@ -181,11 +230,14 @@ export default class AnimatedProps extends AnimatedNode { __disconnectAnimatedView(): void { invariant(this.__isNative, 'Expected node to be marked as "native"'); - const nativeViewTag: ?number = findNodeHandle(this._animatedView); - invariant( - nativeViewTag != null, - 'Unable to locate attached view in the native tree', - ); + let nativeViewTag: ?number = findNodeHandle(this.#animatedView); + if (nativeViewTag == null) { + if (process.env.NODE_ENV === 'test') { + nativeViewTag = -1; + } else { + throw new Error('Unable to locate attached view in the native tree'); + } + } NativeAnimatedHelper.API.disconnectAnimatedNodeFromView( this.__getNativeTag(), nativeViewTag, @@ -218,6 +270,15 @@ export default class AnimatedProps extends AnimatedNode { return { type: 'props', props: propsConfig, + debugID: this.__getDebugID(), }; } } + +// Supported versions of JSC do not implement the newer Object.hasOwn. Remove +// this shim when they do. +// $FlowIgnore[method-unbinding] +const _hasOwnProp = Object.prototype.hasOwnProperty; +const hasOwn: (obj: $ReadOnly<{...}>, prop: string) => boolean = + // $FlowIgnore[method-unbinding] + Object.hasOwn ?? ((obj, prop) => _hasOwnProp.call(obj, prop)); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js b/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js index bf08605e5f4706..439d1a46fe5eaf 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedStyle.js @@ -8,9 +8,8 @@ * @format */ -'use strict'; - import type {PlatformConfig} from '../AnimatedPlatformConfig'; +import type {AnimatedNodeConfig} from './AnimatedNode'; import {validateStyles} from '../../../src/private/animated/NativeAnimatedValidation'; import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; @@ -21,24 +20,34 @@ import AnimatedObject from './AnimatedObject'; import AnimatedTransform from './AnimatedTransform'; import AnimatedWithChildren from './AnimatedWithChildren'; +export type AnimatedStyleAllowlist = $ReadOnly<{[string]: true}>; + function createAnimatedStyle( inputStyle: {[string]: mixed}, + allowlist: ?AnimatedStyleAllowlist, keepUnanimatedValues: boolean, -): [$ReadOnlyArray, $ReadOnlyArray, Object] { +): [$ReadOnlyArray, $ReadOnlyArray, {[string]: mixed}] { const nodeKeys: Array = []; const nodes: Array = []; - const style: {[string]: any} = {}; + const style: {[string]: mixed} = {}; const keys = Object.keys(inputStyle); for (let ii = 0, length = keys.length; ii < length; ii++) { const key = keys[ii]; const value = inputStyle[key]; - if (value != null && key === 'transform') { - const node = ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform() - ? AnimatedObject.from(value) - : // $FlowFixMe[incompatible-call] - `value` is mixed. - new AnimatedTransform(value); + if (allowlist == null || hasOwn(allowlist, key)) { + let node; + if (value != null && key === 'transform') { + node = ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform() + ? AnimatedObject.from(value) + : // $FlowFixMe[incompatible-call] - `value` is mixed. + AnimatedTransform.from(value); + } else if (value instanceof AnimatedNode) { + node = value; + } else { + node = AnimatedObject.from(value); + } if (node == null) { if (keepUnanimatedValues) { style[key] = value; @@ -48,21 +57,21 @@ function createAnimatedStyle( nodes.push(node); style[key] = node; } - } else if (value instanceof AnimatedNode) { - const node = value; - nodeKeys.push(key); - nodes.push(node); - style[key] = value; } else { - const node = AnimatedObject.from(value); - if (node == null) { - if (keepUnanimatedValues) { - style[key] = value; + if (__DEV__) { + // WARNING: This is a potentially expensive check that we should only + // do in development. Without this check in development, it might be + // difficult to identify which styles need to be allowlisted. + if (AnimatedObject.from(inputStyle[key]) != null) { + console.error( + `AnimatedStyle: ${key} is not allowlisted for animation, but it ` + + 'contains AnimatedNode values; styles allowing animation: ', + allowlist, + ); } - } else { - nodeKeys.push(key); - nodes.push(node); - style[key] = node; + } + if (keepUnanimatedValues) { + style[key] = value; } } } @@ -71,34 +80,55 @@ function createAnimatedStyle( } export default class AnimatedStyle extends AnimatedWithChildren { + #inputStyle: any; #nodeKeys: $ReadOnlyArray; #nodes: $ReadOnlyArray; - - _inputStyle: any; - _style: {[string]: any}; - - constructor(inputStyle: any) { - super(); - this._inputStyle = inputStyle; + #style: {[string]: mixed}; + + /** + * Creates an `AnimatedStyle` if `value` contains `AnimatedNode` instances. + * Otherwise, returns `null`. + */ + static from( + inputStyle: any, + allowlist: ?AnimatedStyleAllowlist, + ): ?AnimatedStyle { + const flatStyle = flattenStyle(inputStyle); + if (flatStyle == null) { + return null; + } const [nodeKeys, nodes, style] = createAnimatedStyle( - // NOTE: This null check should not be necessary, but the types are not - // strong nor enforced as of this writing. This check should be hoisted - // to instantiation sites. - flattenStyle(inputStyle) ?? {}, + flatStyle, + allowlist, Platform.OS !== 'web', ); + if (nodes.length === 0) { + return null; + } + return new AnimatedStyle(nodeKeys, nodes, style, inputStyle); + } + + constructor( + nodeKeys: $ReadOnlyArray, + nodes: $ReadOnlyArray, + style: {[string]: mixed}, + inputStyle: any, + config?: ?AnimatedNodeConfig, + ) { + super(config); this.#nodeKeys = nodeKeys; this.#nodes = nodes; - this._style = style; + this.#style = style; + this.#inputStyle = inputStyle; } __getValue(): Object | Array { - const style: {[string]: any} = {}; + const style: {[string]: mixed} = {}; - const keys = Object.keys(this._style); + const keys = Object.keys(this.#style); for (let ii = 0, length = keys.length; ii < length; ii++) { const key = keys[ii]; - const value = this._style[key]; + const value = this.#style[key]; if (value instanceof AnimatedNode) { style[key] = value.__getValue(); @@ -107,11 +137,52 @@ export default class AnimatedStyle extends AnimatedWithChildren { } } - return Platform.OS === 'web' ? [this._inputStyle, style] : style; + /* $FlowFixMe[incompatible-type] Error found due to incomplete typing of + * Platform.flow.js */ + return Platform.OS === 'web' ? [this.#inputStyle, style] : style; + } + + /** + * Creates a new `style` object that contains the same style properties as + * the supplied `staticStyle` object, except with animated nodes for any + * style properties that were created by this `AnimatedStyle` instance. + */ + __getValueWithStaticStyle(staticStyle: Object): Object | Array { + const flatStaticStyle = flattenStyle(staticStyle); + const style: {[string]: mixed} = + flatStaticStyle == null + ? {} + : flatStaticStyle === staticStyle + ? // Copy the input style, since we'll mutate it below. + {...flatStaticStyle} + : // Reuse `flatStaticStyle` if it is a newly created object. + flatStaticStyle; + + const keys = Object.keys(style); + for (let ii = 0, length = keys.length; ii < length; ii++) { + const key = keys[ii]; + const maybeNode = this.#style[key]; + + if (key === 'transform' && maybeNode instanceof AnimatedTransform) { + style[key] = maybeNode.__getValueWithStaticTransforms( + // NOTE: This check should not be necessary, but the types are not + // enforced as of this writing. + Array.isArray(style[key]) ? style[key] : [], + ); + } else if (maybeNode instanceof AnimatedObject) { + style[key] = maybeNode.__getValueWithStaticObject(style[key]); + } else if (maybeNode instanceof AnimatedNode) { + style[key] = maybeNode.__getValue(); + } + } + + /* $FlowFixMe[incompatible-type] Error found due to incomplete typing of + * Platform.flow.js */ + return Platform.OS === 'web' ? [this.#inputStyle, style] : style; } __getAnimatedValue(): Object { - const style: {[string]: any} = {}; + const style: {[string]: mixed} = {}; const nodeKeys = this.#nodeKeys; const nodes = this.#nodes; @@ -169,6 +240,15 @@ export default class AnimatedStyle extends AnimatedWithChildren { return { type: 'style', style: styleConfig, + debugID: this.__getDebugID(), }; } } + +// Supported versions of JSC do not implement the newer Object.hasOwn. Remove +// this shim when they do. +// $FlowIgnore[method-unbinding] +const _hasOwnProp = Object.prototype.hasOwnProperty; +const hasOwn: (obj: $ReadOnly<{...}>, prop: string) => boolean = + // $FlowIgnore[method-unbinding] + Object.hasOwn ?? ((obj, prop) => _hasOwnProp.call(obj, prop)); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js b/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js index d9a423b4cbbc34..7e0b9cad884f77 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedSubtraction.js @@ -13,6 +13,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type {InterpolationConfigType} from './AnimatedInterpolation'; import type AnimatedNode from './AnimatedNode'; +import type {AnimatedNodeConfig} from './AnimatedNode'; import AnimatedInterpolation from './AnimatedInterpolation'; import AnimatedValue from './AnimatedValue'; @@ -22,8 +23,12 @@ export default class AnimatedSubtraction extends AnimatedWithChildren { _a: AnimatedNode; _b: AnimatedNode; - constructor(a: AnimatedNode | number, b: AnimatedNode | number) { - super(); + constructor( + a: AnimatedNode | number, + b: AnimatedNode | number, + config?: ?AnimatedNodeConfig, + ) { + super(config); this._a = typeof a === 'number' ? new AnimatedValue(a) : a; this._b = typeof b === 'number' ? new AnimatedValue(b) : b; } @@ -59,6 +64,7 @@ export default class AnimatedSubtraction extends AnimatedWithChildren { return { type: 'subtraction', input: [this._a.__getNativeTag(), this._b.__getNativeTag()], + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedTracking.js b/packages/react-native/Libraries/Animated/nodes/AnimatedTracking.js index 057ad0f8468595..f4541817aedf49 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedTracking.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedTracking.js @@ -12,6 +12,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig'; import type {EndCallback} from '../animations/Animation'; +import type {AnimatedNodeConfig} from './AnimatedNode'; import type AnimatedValue from './AnimatedValue'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; @@ -31,8 +32,9 @@ export default class AnimatedTracking extends AnimatedNode { animationClass: any, animationConfig: Object, callback?: ?EndCallback, + config?: ?AnimatedNodeConfig, ) { - super(); + super(config); this._value = value; this._parent = parent; this._animationClass = animationClass; @@ -95,6 +97,7 @@ export default class AnimatedTracking extends AnimatedNode { animationConfig, toValue: this._parent.__getNativeTag(), value: this._value.__getNativeTag(), + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js b/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js index c0284d28c01bc2..c77a546090f440 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedTransform.js @@ -11,9 +11,10 @@ 'use strict'; import type {PlatformConfig} from '../AnimatedPlatformConfig'; +import type {AnimatedNodeConfig} from './AnimatedNode'; -import {validateTransform} from '../../../src/private/animated/NativeAnimatedValidation'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; +import {validateTransform} from '../../../src/private/animated/NativeAnimatedValidation'; import AnimatedNode from './AnimatedNode'; import AnimatedWithChildren from './AnimatedWithChildren'; @@ -26,37 +27,59 @@ type Transform = { | {[string]: number | string | T}, }; +function flatAnimatedNodes( + transforms: $ReadOnlyArray>, +): Array { + const nodes = []; + for (let ii = 0, length = transforms.length; ii < length; ii++) { + const transform = transforms[ii]; + // There should be exactly one property in `transform`. + for (const key in transform) { + const value = transform[key]; + if (value instanceof AnimatedNode) { + nodes.push(value); + } + } + } + return nodes; +} + export default class AnimatedTransform extends AnimatedWithChildren { // NOTE: For potentially historical reasons, some operations only operate on // the first level of AnimatedNode instances. This optimizes that bevavior. - #shallowNodes: $ReadOnlyArray; + #nodes: $ReadOnlyArray; _transforms: $ReadOnlyArray>; - constructor(transforms: $ReadOnlyArray>) { - super(); - this._transforms = transforms; - - const shallowNodes = []; - // NOTE: This check should not be necessary, but the types are not enforced - // as of this writing. This check should be hoisted to instantiation sites. - if (Array.isArray(transforms)) { - for (let ii = 0, length = transforms.length; ii < length; ii++) { - const transform = transforms[ii]; - // There should be exactly one property in `transform`. - for (const key in transform) { - const value = transform[key]; - if (value instanceof AnimatedNode) { - shallowNodes.push(value); - } - } - } + /** + * Creates an `AnimatedTransform` if `transforms` contains `AnimatedNode` + * instances. Otherwise, returns `null`. + */ + static from(transforms: $ReadOnlyArray>): ?AnimatedTransform { + const nodes = flatAnimatedNodes( + // NOTE: This check should not be necessary, but the types are not + // enforced as of this writing. This check should be hoisted to + // instantiation sites. + Array.isArray(transforms) ? transforms : [], + ); + if (nodes.length === 0) { + return null; } - this.#shallowNodes = shallowNodes; + return new AnimatedTransform(nodes, transforms); + } + + constructor( + nodes: $ReadOnlyArray, + transforms: $ReadOnlyArray>, + config?: ?AnimatedNodeConfig, + ) { + super(config); + this.#nodes = nodes; + this._transforms = transforms; } __makeNative(platformConfig: ?PlatformConfig) { - const nodes = this.#shallowNodes; + const nodes = this.#nodes; for (let ii = 0, length = nodes.length; ii < length; ii++) { const node = nodes[ii]; node.__makeNative(platformConfig); @@ -70,6 +93,18 @@ export default class AnimatedTransform extends AnimatedWithChildren { ); } + __getValueWithStaticTransforms( + staticTransforms: $ReadOnlyArray, + ): $ReadOnlyArray { + const values = []; + mapTransforms(this._transforms, node => { + values.push(node.__getValue()); + }); + // NOTE: We can depend on `this._transforms` and `staticTransforms` sharing + // a structure because of `useAnimatedPropsMemo`. + return mapTransforms(staticTransforms, () => values.shift()); + } + __getAnimatedValue(): $ReadOnlyArray> { return mapTransforms(this._transforms, animatedNode => animatedNode.__getAnimatedValue(), @@ -77,7 +112,7 @@ export default class AnimatedTransform extends AnimatedWithChildren { } __attach(): void { - const nodes = this.#shallowNodes; + const nodes = this.#nodes; for (let ii = 0, length = nodes.length; ii < length; ii++) { const node = nodes[ii]; node.__addChild(this); @@ -85,7 +120,7 @@ export default class AnimatedTransform extends AnimatedWithChildren { } __detach(): void { - const nodes = this.#shallowNodes; + const nodes = this.#nodes; for (let ii = 0, length = nodes.length; ii < length; ii++) { const node = nodes[ii]; node.__removeChild(this); @@ -127,6 +162,7 @@ export default class AnimatedTransform extends AnimatedWithChildren { return { type: 'transform', transforms: transformsConfig, + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js b/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js index 15fcd4ec3817d4..fe72a33d243d1d 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedValue.js @@ -13,14 +13,16 @@ import type Animation, {EndCallback} from '../animations/Animation'; import type {InterpolationConfigType} from './AnimatedInterpolation'; import type AnimatedNode from './AnimatedNode'; +import type {AnimatedNodeConfig} from './AnimatedNode'; import type AnimatedTracking from './AnimatedTracking'; -import InteractionManager from '../../Interaction/InteractionManager'; import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; +import InteractionManager from '../../Interaction/InteractionManager'; import AnimatedInterpolation from './AnimatedInterpolation'; import AnimatedWithChildren from './AnimatedWithChildren'; export type AnimatedValueConfig = $ReadOnly<{ + ...AnimatedNodeConfig, useNativeDriver: boolean, }>; @@ -90,7 +92,7 @@ export default class AnimatedValue extends AnimatedWithChildren { _tracking: ?AnimatedTracking; constructor(value: number, config?: ?AnimatedValueConfig) { - super(); + super(config); if (typeof value !== 'number') { throw new Error('AnimatedValue: Attempting to set value to undefined'); } @@ -298,6 +300,7 @@ export default class AnimatedValue extends AnimatedWithChildren { type: 'value', value: this._value, offset: this._offset, + debugID: this.__getDebugID(), }; } } diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedValueXY.js b/packages/react-native/Libraries/Animated/nodes/AnimatedValueXY.js index fa4a82320e3069..f44d9e57fa615c 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedValueXY.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedValueXY.js @@ -11,12 +11,14 @@ 'use strict'; import type {PlatformConfig} from '../AnimatedPlatformConfig'; +import type {AnimatedNodeConfig} from './AnimatedNode'; import AnimatedValue from './AnimatedValue'; import AnimatedWithChildren from './AnimatedWithChildren'; import invariant from 'invariant'; export type AnimatedValueXYConfig = $ReadOnly<{ + ...AnimatedNodeConfig, useNativeDriver: boolean, }>; type ValueXYListenerCallback = (value: {x: number, y: number, ...}) => mixed; @@ -49,7 +51,7 @@ export default class AnimatedValueXY extends AnimatedWithChildren { }, config?: ?AnimatedValueXYConfig, ) { - super(); + super(config); const value: any = valueIn || {x: 0, y: 0}; // @flowfixme: shouldn't need `: any` if (typeof value.x === 'number' && typeof value.y === 'number') { this.x = new AnimatedValue(value.x); diff --git a/packages/react-native/Libraries/Animated/nodes/AnimatedWithChildren.js b/packages/react-native/Libraries/Animated/nodes/AnimatedWithChildren.js index 21de142464d9f6..9d4469e33c44c9 100644 --- a/packages/react-native/Libraries/Animated/nodes/AnimatedWithChildren.js +++ b/packages/react-native/Libraries/Animated/nodes/AnimatedWithChildren.js @@ -28,12 +28,10 @@ export default class AnimatedWithChildren extends AnimatedNode { const children = this._children; let length = children.length; if (length > 0) { - const nativeTag = this.__getNativeTag(); - for (let ii = 0; ii < length; ii++) { const child = children[ii]; child.__makeNative(platformConfig); - connectAnimatedNodes(nativeTag, child.__getNativeTag()); + connectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag()); } } } diff --git a/packages/react-native/Libraries/Animated/useAnimatedProps.js b/packages/react-native/Libraries/Animated/useAnimatedProps.js index df3df01a3c9a83..2d853379c783a8 100644 --- a/packages/react-native/Libraries/Animated/useAnimatedProps.js +++ b/packages/react-native/Libraries/Animated/useAnimatedProps.js @@ -8,23 +8,22 @@ * @format */ -'use strict'; - import type {EventSubscription} from '../EventEmitter/NativeEventEmitter'; +import type {AnimatedPropsAllowlist} from './nodes/AnimatedProps'; +import NativeAnimatedHelper from '../../src/private/animated/NativeAnimatedHelper'; +import {useAnimatedPropsMemo} from '../../src/private/animated/useAnimatedPropsMemo'; import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags'; import {isPublicInstance as isFabricPublicInstance} from '../ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstanceUtils'; import useRefEffect from '../Utilities/useRefEffect'; import {AnimatedEvent} from './AnimatedEvent'; -import NativeAnimatedHelper from '../../src/private/animated/NativeAnimatedHelper'; import AnimatedNode from './nodes/AnimatedNode'; import AnimatedProps from './nodes/AnimatedProps'; import AnimatedValue from './nodes/AnimatedValue'; import { useCallback, useEffect, - useLayoutEffect, - useMemo, + useInsertionEffect, useReducer, useRef, } from 'react'; @@ -36,6 +35,8 @@ type ReducedProps = { }; type CallbackRef = T => mixed; +type UpdateCallback = () => void; + type AnimatedValueListeners = Array<{ propValue: AnimatedValue, listenerId: string, @@ -43,28 +44,19 @@ type AnimatedValueListeners = Array<{ export default function useAnimatedProps( props: TProps, + allowlist?: ?AnimatedPropsAllowlist, ): [ReducedProps, CallbackRef] { const [, scheduleUpdate] = useReducer(count => count + 1, 0); - const onUpdateRef = useRef void>(null); + const onUpdateRef = useRef(null); const timerRef = useRef(null); - // TODO: Only invalidate `node` if animated props or `style` change. In the - // previous implementation, we permitted `style` to override props with the - // same name property name as styles, so we can probably continue doing that. - // The ordering of other props *should* not matter. - const node = useMemo( - () => new AnimatedProps(props, () => onUpdateRef.current?.()), - [props], + const node = useAnimatedPropsMemo( + () => new AnimatedProps(props, () => onUpdateRef.current?.(), allowlist), + [allowlist, props], ); + const useNativePropsInFabric = ReactNativeFeatureFlags.shouldUseSetNativePropsInFabric(); - const useSetNativePropsInNativeAnimationsInFabric = - ReactNativeFeatureFlags.shouldUseSetNativePropsInNativeAnimationsInFabric(); - - const useAnimatedPropsLifecycle = - ReactNativeFeatureFlags.usePassiveEffectsForAnimations() - ? useAnimatedPropsLifecycle_passiveEffects - : useAnimatedPropsLifecycle_layoutEffects; useAnimatedPropsLifecycle(node); @@ -104,12 +96,7 @@ export default function useAnimatedProps( if (isFabricNode) { // Call `scheduleUpdate` to synchronise Fiber and Shadow tree. // Must not be called in Paper. - if (useSetNativePropsInNativeAnimationsInFabric) { - // $FlowFixMe[incompatible-use] - instance.setNativeProps(node.__getAnimatedValue()); - } else { - scheduleUpdate(); - } + scheduleUpdate(); } return; } @@ -186,23 +173,21 @@ export default function useAnimatedProps( } }; }, - [ - node, - useNativePropsInFabric, - useSetNativePropsInNativeAnimationsInFabric, - props, - ], + [node, useNativePropsInFabric, props], ); const callbackRef = useRefEffect(refEffect); - return [reduceAnimatedProps(node), callbackRef]; + return [reduceAnimatedProps(node, props), callbackRef]; } -function reduceAnimatedProps(node: AnimatedNode): ReducedProps { +function reduceAnimatedProps( + node: AnimatedProps, + props: TProps, +): ReducedProps { // Force `collapsable` to be false so that the native view is not flattened. // Flattened views cannot be accurately referenced by the native driver. return { - ...node.__getValue(), + ...node.__getValueWithStaticProps(props), collapsable: false, }; } @@ -243,68 +228,7 @@ function addAnimatedValuesListenersToProps( * nodes. So in order to optimize this, we avoid detaching until the next attach * unless we are unmounting. */ -function useAnimatedPropsLifecycle_layoutEffects(node: AnimatedProps): void { - const prevNodeRef = useRef(null); - const isUnmountingRef = useRef(false); - - useEffect(() => { - // It is ok for multiple components to call `flushQueue` because it noops - // if the queue is empty. When multiple animated components are mounted at - // the same time. Only first component flushes the queue and the others will noop. - NativeAnimatedHelper.API.flushQueue(); - let drivenAnimationEndedListener: ?EventSubscription = null; - if (node.__isNative) { - drivenAnimationEndedListener = - NativeAnimatedHelper.nativeEventEmitter.addListener( - 'onUserDrivenAnimationEnded', - data => { - node.update(); - }, - ); - } - - return () => { - drivenAnimationEndedListener?.remove(); - }; - }); - - useLayoutEffect(() => { - isUnmountingRef.current = false; - return () => { - isUnmountingRef.current = true; - }; - }, []); - - useLayoutEffect(() => { - node.__attach(); - if (prevNodeRef.current != null) { - const prevNode = prevNodeRef.current; - // TODO: Stop restoring default values (unless `reset` is called). - prevNode.__restoreDefaultValues(); - prevNode.__detach(); - prevNodeRef.current = null; - } - return () => { - if (isUnmountingRef.current) { - // NOTE: Do not restore default values on unmount, see D18197735. - node.__detach(); - } else { - prevNodeRef.current = node; - } - }; - }, [node]); -} - -/** - * Manages the lifecycle of the supplied `AnimatedProps` by invoking `__attach` - * and `__detach`. However, this is more complicated because `AnimatedProps` - * uses reference counting to determine when to recursively detach its children - * nodes. So in order to optimize this, we avoid detaching until the next attach - * unless we are unmounting. - * - * NOTE: unlike `useAnimatedPropsLifecycle_layoutEffects`, this version uses passive effects to setup animation graph. - */ -function useAnimatedPropsLifecycle_passiveEffects(node: AnimatedProps): void { +function useAnimatedPropsLifecycle(node: AnimatedProps): void { const prevNodeRef = useRef(null); const isUnmountingRef = useRef(false); @@ -315,14 +239,14 @@ function useAnimatedPropsLifecycle_passiveEffects(node: AnimatedProps): void { NativeAnimatedHelper.API.flushQueue(); }); - useEffect(() => { + useInsertionEffect(() => { isUnmountingRef.current = false; return () => { isUnmountingRef.current = true; }; }, []); - useEffect(() => { + useInsertionEffect(() => { node.__attach(); let drivenAnimationEndedListener: ?EventSubscription = null; diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm index 3f9f0d16d17f8c..177f668de6f8e7 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm @@ -33,6 +33,8 @@ #endif #import +using namespace facebook::react; + @interface RCTAppDelegate () @end @@ -331,17 +333,21 @@ - (RCTRootViewFactory *)createRCTRootViewFactory #pragma mark - Feature Flags -class RCTAppDelegateBridgelessFeatureFlags : public facebook::react::ReactNativeFeatureFlagsDefaults { +class RCTAppDelegateBridgelessFeatureFlags : public ReactNativeFeatureFlagsDefaults { public: - bool useModernRuntimeScheduler() override + bool enableBridgelessArchitecture() override + { + return true; + } + bool enableFabricRenderer() override { return true; } - bool enableMicrotasks() override + bool useTurboModules() override { return true; } - bool batchRenderingUpdatesInEventLoop() override + bool useNativeViewConfigsInBridgelessMode() override { return true; } diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm b/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm index 0765e136eb6c66..372a0a618bf87c 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm @@ -39,7 +39,7 @@ void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled) { RCTEnableTurboModule(turboModuleEnabled); -#if DEBUG +#ifdef DEBUG #if !TARGET_OS_OSX // [macOS] // Disable idle timer in dev builds to avoid putting application in background and complicating // Metro reconnection logic. Users only need this when running the application using our CLI tooling. diff --git a/packages/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm b/packages/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm index 12003fb979b8a6..2b996b8f7e6c54 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm @@ -35,7 +35,6 @@ #import #import #import -#import #import #import #import @@ -188,7 +187,7 @@ - (RCTPlatformView *)createRootViewWithBridge:(RCTBridge *)bridge #if !TARGET_OS_OSX // [macOS] rootView.backgroundColor = [UIColor systemBackgroundColor]; #endif // [macOS] - + return rootView; } @@ -279,7 +278,6 @@ - (RCTHost *)createReactHost:(NSDictionary *)launchOptions [reactHost setBundleURLProvider:^NSURL *() { return [weakSelf bundleURL]; }]; - [reactHost setContextContainerHandler:self]; [reactHost start]; return reactHost; } @@ -287,18 +285,12 @@ - (RCTHost *)createReactHost:(NSDictionary *)launchOptions - (std::shared_ptr)createJSRuntimeFactory { #if USE_HERMES - return std::make_shared( - _reactNativeConfig, nullptr, /* allocInOldGenBeforeTTI */ false); + return std::make_shared(nullptr, /* allocInOldGenBeforeTTI */ false); #else return std::make_shared(); #endif } -- (void)didCreateContextContainer:(std::shared_ptr)contextContainer -{ - contextContainer->insert("ReactNativeConfig", _reactNativeConfig); -} - - (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge { if (_configuration.extraModulesForBridge != nil) { diff --git a/packages/react-native/Libraries/AppState/AppState.js b/packages/react-native/Libraries/AppState/AppState.js index 6ec9a262bedfea..ed01047d05fa0b 100644 --- a/packages/react-native/Libraries/AppState/AppState.js +++ b/packages/react-native/Libraries/AppState/AppState.js @@ -14,17 +14,19 @@ import Platform from '../Utilities/Platform'; import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import NativeAppState from './NativeAppState'; -export type AppStateValues = 'inactive' | 'background' | 'active'; +export type AppStateStatus = 'inactive' | 'background' | 'active'; type AppStateEventDefinitions = { - change: [AppStateValues], + change: [AppStateStatus], memoryWarning: [], blur: [], focus: [], }; +export type AppStateEvent = $Keys; + type NativeAppStateEventDefinitions = { - appStateDidChange: [{app_state: AppStateValues}], + appStateDidChange: [{app_state: AppStateStatus}], appStateFocusChange: [boolean], memoryWarning: [], }; @@ -91,7 +93,7 @@ class AppState { * * See https://reactnative.dev/docs/appstate#addeventlistener */ - addEventListener>( + addEventListener( type: K, handler: (...$ElementType) => void, ): EventSubscription { @@ -102,7 +104,7 @@ class AppState { switch (type) { case 'change': // $FlowIssue[invalid-tuple-arity] Flow cannot refine handler based on the event type - const changeHandler: AppStateValues => void = handler; + const changeHandler: AppStateStatus => void = handler; return emitter.addListener('appStateDidChange', appStateData => { changeHandler(appStateData.app_state); }); @@ -127,4 +129,4 @@ class AppState { } } -module.exports = (new AppState(): AppState); +export default (new AppState(): AppState); diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js index 64c2a67a5af0a5..0ef5ad3eaf776b 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js @@ -8,9 +8,8 @@ * @format */ -import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +import type {HostInstance} from '../../Renderer/shims/ReactNativeTypes'; import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; -import type {ElementRef} from 'react'; import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; import {sendAccessibilityEvent} from '../../ReactNative/RendererProxy'; @@ -22,6 +21,7 @@ import NativeAccessibilityManagerApple from './NativeAccessibilityManager'; // [ // Events that are only supported on Android. type AccessibilityEventDefinitionsAndroid = { accessibilityServiceChanged: [boolean], + highTextContrastChanged: [boolean], }; // Events that are only supported on iOS. @@ -31,6 +31,7 @@ type AccessibilityEventDefinitionsIOS = { grayscaleChanged: [boolean], invertColorsChanged: [boolean], reduceTransparencyChanged: [boolean], + darkerSystemColorsChanged: [boolean], }; // [macOS @@ -59,8 +60,11 @@ const EventNames: Map< ? new Map([ ['change', 'touchExplorationDidChange'], ['reduceMotionChanged', 'reduceMotionDidChange'], + ['highTextContrastChanged', 'highTextContrastDidChange'], ['screenReaderChanged', 'touchExplorationDidChange'], ['accessibilityServiceChanged', 'accessibilityServiceDidChange'], + ['invertColorsChanged', 'invertColorDidChange'], + ['grayscaleChanged', 'grayscaleModeDidChange'], ]) : new Map([ ['announcementFinished', 'announcementFinished'], @@ -72,6 +76,7 @@ const EventNames: Map< ['reduceMotionChanged', 'reduceMotionChanged'], ['reduceTransparencyChanged', 'reduceTransparencyChanged'], ['screenReaderChanged', 'screenReaderChanged'], + ['darkerSystemColorsChanged', 'darkerSystemColorsChanged'], ]); /** @@ -121,7 +126,15 @@ const AccessibilityInfo = { */ isGrayscaleEnabled(): Promise { // [macOS rework logic to return Promise.resolve(false) on macOS - if (Platform.OS === 'ios') { + if (Platform.OS === 'android') { + return new Promise((resolve, reject) => { + if (NativeAccessibilityInfoAndroid?.isGrayscaleEnabled != null) { + NativeAccessibilityInfoAndroid.isGrayscaleEnabled(resolve); + } else { + reject(null); + } + }); + } else if (Platform.OS === 'ios') { return new Promise((resolve, reject) => { if (NativeAccessibilityManagerApple != null) { NativeAccessibilityManagerApple.getCurrentGrayscaleState( @@ -170,7 +183,15 @@ const AccessibilityInfo = { */ isInvertColorsEnabled(): Promise { // [macOS rework logic to return Promise.resolve(false) on macOS - if (Platform.OS === 'ios') { + if (Platform.OS === 'android') { + return new Promise((resolve, reject) => { + if (NativeAccessibilityInfoAndroid?.isInvertColorsEnabled != null) { + NativeAccessibilityInfoAndroid.isInvertColorsEnabled(resolve); + } else { + reject(null); + } + }); + } else if (Platform.OS === 'ios') { return new Promise((resolve, reject) => { if (NativeAccessibilityManagerApple != null) { NativeAccessibilityManagerApple.getCurrentInvertColorsState( @@ -216,6 +237,54 @@ const AccessibilityInfo = { }); }, + /** + * Query whether high text contrast is currently enabled. Android only. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when high text contrast is enabled and `false` otherwise. + */ + isHighTextContrastEnabled(): Promise { + return new Promise((resolve, reject) => { + if (Platform.OS === 'android') { + if (NativeAccessibilityInfoAndroid?.isHighTextContrastEnabled != null) { + NativeAccessibilityInfoAndroid.isHighTextContrastEnabled(resolve); + } else { + reject(null); + } + } else { + return Promise.resolve(false); + } + }); + }, + + /** + * Query whether dark system colors is currently enabled. iOS only. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when dark system colors is enabled and `false` otherwise. + */ + isDarkerSystemColorsEnabled(): Promise { + // [macOS rework logic to return Promise.resolve(false) on macOS + if (Platform.OS === 'ios') { + return new Promise((resolve, reject) => { + if ( + NativeAccessibilityManagerApple?.getCurrentDarkerSystemColorsState != + null + ) { + NativeAccessibilityManagerApple.getCurrentDarkerSystemColorsState( + resolve, + reject, + ); + } else { + reject(null); + } + }); + } else { + return Promise.resolve(false); + } + // macOS] + }, + /** * Query whether reduce motion and prefer cross-fade transitions settings are currently enabled. * @@ -359,6 +428,15 @@ const AccessibilityInfo = { * - `announcement`: The string announced by the screen reader. * - `success`: A boolean indicating whether the announcement was * successfully made. + * - `darkerSystemColorsChanged`: iOS-only event. Fires when the state of the dark system colors + * toggle changes. The argument to the event handler is a boolean. The boolean is `true` when + * dark system colors is enabled and `false` otherwise. + * + * These events are only supported on Android: + * + * - `highTextContrastChanged`: Android-only event. Fires when the state of the high text contrast + * toggle changes. The argument to the event handler is a boolean. The boolean is `true` when + * high text contrast is enabled and `false` otherwise. * * See https://reactnative.dev/docs/accessibilityinfo#addeventlistener */ @@ -387,7 +465,7 @@ const AccessibilityInfo = { * Send a named accessibility event to a HostComponent. */ sendAccessibilityEvent( - handle: ElementRef>, + handle: HostInstance, eventType: AccessibilityEventTypes, ) { // iOS only supports 'focus' event types diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js b/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js index fdbabb475c7f93..cc75d54fafdb24 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js @@ -33,4 +33,4 @@ function legacySendAccessibilityEvent( } } -module.exports = legacySendAccessibilityEvent; +export default legacySendAccessibilityEvent; diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js b/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js index 17091e354ff569..3fdf5a6a926837 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js @@ -23,4 +23,4 @@ function legacySendAccessibilityEvent( } } -module.exports = legacySendAccessibilityEvent; +export default legacySendAccessibilityEvent; diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.js.flow b/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.js.flow index 14e63dddeac7a7..9459d8cd45855a 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.js.flow +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.js.flow @@ -17,4 +17,4 @@ declare function legacySendAccessibilityEvent( eventType: string, ): void; -module.exports = legacySendAccessibilityEvent; +export default legacySendAccessibilityEvent; diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.macos.js b/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.macos.js index cdeb829bbb2113..3fdf5a6a926837 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.macos.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.macos.js @@ -1,5 +1,5 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * 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. @@ -8,9 +8,19 @@ * @flow strict-local */ -// [macOS] +import NativeAccessibilityManager from './NativeAccessibilityManager'; -/* $FlowFixMe allow macOS to share iOS file */ -const legacySendAccessibilityEvent = require('./legacySendAccessibilityEvent.ios'); +/** + * This is a function exposed to the React Renderer that can be used by the + * pre-Fabric renderer to emit accessibility events to pre-Fabric nodes. + */ +function legacySendAccessibilityEvent( + reactTag: number, + eventType: string, +): void { + if (eventType === 'focus' && NativeAccessibilityManager) { + NativeAccessibilityManager.setAccessibilityFocus(reactTag); + } +} -module.exports = legacySendAccessibilityEvent; +export default legacySendAccessibilityEvent; diff --git a/packages/react-native/Libraries/Components/ActivityIndicator/ActivityIndicator.js b/packages/react-native/Libraries/Components/ActivityIndicator/ActivityIndicator.js index 7f724524add5ce..2855cf47ffd6ae 100644 --- a/packages/react-native/Libraries/Components/ActivityIndicator/ActivityIndicator.js +++ b/packages/react-native/Libraries/Components/ActivityIndicator/ActivityIndicator.js @@ -153,10 +153,10 @@ const ActivityIndicator = ( ``` */ -const ActivityIndicatorWithRef: React.AbstractComponent< - Props, - HostComponent, -> = React.forwardRef(ActivityIndicator); +const ActivityIndicatorWithRef: component( + ref: React.RefSetter>, + ...props: Props +) = React.forwardRef(ActivityIndicator); ActivityIndicatorWithRef.displayName = 'ActivityIndicator'; const styles = StyleSheet.create({ diff --git a/packages/react-native/Libraries/Components/Button.js b/packages/react-native/Libraries/Components/Button.js index 1fd928195fe73a..42b8a498869b26 100644 --- a/packages/react-native/Libraries/Components/Button.js +++ b/packages/react-native/Libraries/Components/Button.js @@ -29,7 +29,7 @@ import View from './View/View'; import invariant from 'invariant'; import * as React from 'react'; -type ButtonProps = $ReadOnly<{| +type ButtonProps = $ReadOnly<{ /** Text to display inside the button. On Android the given title will be converted to the uppercased form. @@ -38,9 +38,9 @@ type ButtonProps = $ReadOnly<{| /** Handler to be called when the user taps the button. The first function - argument is an event in form of [PressEvent](pressevent). + argument is an event in form of [GestureResponderEvent](pressevent). */ - onPress: (event?: PressEvent) => mixed, + onPress: (event?: GestureResponderEvent) => mixed, /** If `true`, doesn't play system sound on touch. @@ -197,7 +197,7 @@ type ButtonProps = $ReadOnly<{| importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'), accessibilityHint?: ?string, accessibilityLanguage?: ?Stringish, -|}>; +}>; /** A basic button component that should render nicely on any platform. Supports a @@ -313,10 +313,12 @@ type ButtonProps = $ReadOnly<{| const Touchable: typeof TouchableNativeFeedback | typeof TouchableOpacity = Platform.OS === 'android' ? TouchableNativeFeedback : TouchableOpacity; -const Button: React.AbstractComponent< - ButtonProps, - React.ElementRef, -> = React.forwardRef((props: ButtonProps, ref) => { +type ButtonRef = React.ElementRef; + +const Button: component( + ref: React.RefSetter, + ...props: ButtonProps +) = React.forwardRef((props: ButtonProps, ref: React.RefSetter) => { const { accessibilityLabel, accessibilityRole, // [macOS] @@ -422,6 +424,9 @@ const Button: React.AbstractComponent< tooltip={tooltip} // macOS] touchSoundDisabled={touchSoundDisabled} + // $FlowFixMe[incompatible-exact] + // $FlowFixMe[prop-missing] + // $FlowFixMe[incompatible-type-arg] ref={ref}> diff --git a/packages/react-native/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.js b/packages/react-native/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.js index 3e519526096176..66f0e06f75fbfb 100644 --- a/packages/react-native/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.js +++ b/packages/react-native/Libraries/Components/DrawerAndroid/DrawerLayoutAndroid.js @@ -5,8 +5,10 @@ * LICENSE file in the root directory of this source tree. * * @format + * @flow strict-local */ 'use strict'; -module.exports = require('../UnimplementedViews/UnimplementedView'); +module.exports = + require('../UnimplementedViews/UnimplementedView') as $FlowFixMe; diff --git a/packages/react-native/Libraries/Components/Keyboard/Keyboard.js b/packages/react-native/Libraries/Components/Keyboard/Keyboard.js index 5a175ec33228e5..2f7e16968dbeb2 100644 --- a/packages/react-native/Libraries/Components/Keyboard/Keyboard.js +++ b/packages/react-native/Libraries/Components/Keyboard/Keyboard.js @@ -25,32 +25,32 @@ export type KeyboardEventEasing = | 'linear' | 'keyboard'; -export type KeyboardMetrics = $ReadOnly<{| +export type KeyboardMetrics = $ReadOnly<{ screenX: number, screenY: number, width: number, height: number, -|}>; +}>; export type KeyboardEvent = AndroidKeyboardEvent | IOSKeyboardEvent; -type BaseKeyboardEvent = {| +type BaseKeyboardEvent = { duration: number, easing: KeyboardEventEasing, endCoordinates: KeyboardMetrics, -|}; +}; -export type AndroidKeyboardEvent = $ReadOnly<{| +export type AndroidKeyboardEvent = $ReadOnly<{ ...BaseKeyboardEvent, duration: 0, easing: 'keyboard', -|}>; +}>; -export type IOSKeyboardEvent = $ReadOnly<{| +export type IOSKeyboardEvent = $ReadOnly<{ ...BaseKeyboardEvent, startCoordinates: KeyboardMetrics, isEventFromThisApp: boolean, -|}>; +}>; type KeyboardEventDefinitions = { keyboardWillShow: [KeyboardEvent], @@ -204,4 +204,4 @@ class Keyboard { } } -module.exports = (new Keyboard(): Keyboard); +export default (new Keyboard(): Keyboard); diff --git a/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js index e26d6771c47209..c0f84d886e5dec 100644 --- a/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js +++ b/packages/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js @@ -113,6 +113,8 @@ class KeyboardAvoidingView extends React.Component { }; _onLayout = async (event: ViewLayoutEvent) => { + event.persist(); + const oldFrame = this._frame; this._frame = event.nativeEvent.layout; if (!this._initialFrameHeight) { @@ -175,6 +177,11 @@ class KeyboardAvoidingView extends React.Component { } componentDidMount(): void { + if (!Keyboard.isVisible()) { + this._keyboardEvent = null; + this._setBottom(0); + } + if (Platform.OS === 'ios') { this._subscriptions = [ Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange), diff --git a/packages/react-native/Libraries/Components/Pressable/Pressable.js b/packages/react-native/Libraries/Components/Pressable/Pressable.js index d1c9433ebc83b7..7f52221984005c 100644 --- a/packages/react-native/Libraries/Components/Pressable/Pressable.js +++ b/packages/react-native/Libraries/Components/Pressable/Pressable.js @@ -14,9 +14,9 @@ import type { FocusEvent, HandledKeyEvent, KeyEvent, - LayoutEvent, + LayoutChangeEvent, MouseEvent, - PressEvent, + GestureResponderEvent, // macOS] } from '../../Types/CoreEventTypes'; import type {DraggedTypesType} from '../View/DraggedType'; // [macOS] @@ -41,11 +41,11 @@ import {useMemo, useRef, useState} from 'react'; type ViewStyleProp = $ElementType, 'style'>; -export type StateCallbackType = $ReadOnly<{| +export type StateCallbackType = $ReadOnly<{ pressed: boolean, -|}>; +}>; -type Props = $ReadOnly<{| +type Props = $ReadOnly<{ /** * Accessibility. */ @@ -133,7 +133,7 @@ type Props = $ReadOnly<{| /** * Called when this view's layout changes. */ - onLayout?: ?(event: LayoutEvent) => mixed, + onLayout?: ?(event: LayoutChangeEvent) => mixed, /** * Called when the hover is activated to provide visual feedback. @@ -148,22 +148,22 @@ type Props = $ReadOnly<{| /** * Called when a long-tap gesture is detected. */ - onLongPress?: ?(event: PressEvent) => mixed, + onLongPress?: ?(event: GestureResponderEvent) => mixed, /** * Called when a single tap gesture is detected. */ - onPress?: ?(event: PressEvent) => mixed, + onPress?: ?(event: GestureResponderEvent) => mixed, /** * Called when a touch is engaged before `onPress`. */ - onPressIn?: ?(event: PressEvent) => mixed, + onPressIn?: ?(event: GestureResponderEvent) => mixed, /** * Called when a touch is released before `onPress`. */ - onPressOut?: ?(event: PressEvent) => mixed, + onPressOut?: ?(event: GestureResponderEvent) => mixed, // [macOS /** @@ -308,7 +308,7 @@ type Props = $ReadOnly<{| * https://github.com/facebook/react-native/issues/34424 */ 'aria-label'?: ?string, -|}>; +}>; type Instance = React.ElementRef; @@ -429,7 +429,7 @@ function Pressable( onHoverOut, onLongPress, onPress, - onPressIn(event: PressEvent): void { + onPressIn(event: GestureResponderEvent): void { if (android_rippleConfig != null) { android_rippleConfig.onPressIn(event); } @@ -439,7 +439,7 @@ function Pressable( } }, onPressMove: android_rippleConfig?.onPressMove, - onPressOut(event: PressEvent): void { + onPressOut(event: GestureResponderEvent): void { if (android_rippleConfig != null) { android_rippleConfig.onPressOut(event); } @@ -505,7 +505,7 @@ function usePressState(forcePressed: boolean): [boolean, (boolean) => void] { const MemoedPressable = React.memo(React.forwardRef(Pressable)); MemoedPressable.displayName = 'Pressable'; -export default (MemoedPressable: React.AbstractComponent< - Props, - React.ElementRef, ->); +export default (MemoedPressable: component( + ref: React.RefSetter>, + ...props: Props +)); diff --git a/packages/react-native/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js b/packages/react-native/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js index a34a432a55f26c..ca5c4fb6a05341 100644 --- a/packages/react-native/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js +++ b/packages/react-native/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.android.js @@ -78,15 +78,23 @@ export type ProgressBarAndroidProps = $ReadOnly<{| * }, * ``` */ -const ProgressBarAndroid = ( +const ProgressBarAndroidWithForwardedRef: component( + ref: React.RefSetter< + React.ElementRef, + >, + ...props: ProgressBarAndroidProps +) = React.forwardRef(function ProgressBarAndroid( { + // $FlowFixMe[incompatible-type] styleAttr = 'Normal', indeterminate = true, animating = true, ...restProps }: ProgressBarAndroidProps, - forwardedRef: ?React.Ref, -) => { + forwardedRef: ?React.RefSetter< + React.ElementRef, + >, +) { return ( ); -}; - -const ProgressBarAndroidToExport = React.forwardRef(ProgressBarAndroid); +}); module.exports = /* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an * error found when Flow v0.89 was deployed. To see the error, delete this * comment and run Flow. */ - (ProgressBarAndroidToExport: typeof ProgressBarAndroidNativeComponent); + (ProgressBarAndroidWithForwardedRef: typeof ProgressBarAndroidNativeComponent); diff --git a/packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js b/packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js index a154c827511f34..e3da1d7798c42d 100644 --- a/packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js +++ b/packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js @@ -23,10 +23,10 @@ import * as React from 'react'; * limitation of the screen, such as rounded corners or camera notches (aka * sensor housing area on iPhone X). */ -const exported: React.AbstractComponent< - ViewProps, - React.ElementRef, -> = Platform.select({ +const exported: component( + ref: React.RefSetter>, + ...props: ViewProps +) = Platform.select({ ios: require('./RCTSafeAreaViewNativeComponent').default, default: View, }); diff --git a/packages/react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js b/packages/react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js index bdaef9b90941d1..c99ea196ae3bfc 100644 --- a/packages/react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js +++ b/packages/react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js @@ -31,7 +31,6 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { pagingEnabled: true, persistentScrollbar: true, horizontal: true, - enableSyncOnScroll: true, scrollEnabled: true, scrollEventThrottle: true, scrollPerfTag: true, diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollContentViewNativeComponent.js b/packages/react-native/Libraries/Components/ScrollView/ScrollContentViewNativeComponent.js index 6ad8519391e861..88a64f4ada04c7 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollContentViewNativeComponent.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollContentViewNativeComponent.js @@ -20,7 +20,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { uiViewClassName: 'RCTScrollContentView', bubblingEventTypes: {}, directEventTypes: {}, - validAttributes: {}, + validAttributes: { + inverted: true, // [macOS] + }, }; const ScrollContentViewNativeComponent: HostComponent = diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js index 5d0777ac802b47..787abd63518f72 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js @@ -8,18 +8,14 @@ * @flow strict-local */ -import type { - TScrollViewNativeComponentInstance, - TScrollViewNativeImperativeHandle, -} from '../../../src/private/components/useSyncOnScroll'; -import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +import type {HostInstance} from '../../Renderer/shims/ReactNativeTypes'; import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType'; import type {PointProp} from '../../StyleSheet/PointPropType'; import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; import type {ColorValue} from '../../StyleSheet/StyleSheet'; import type { - LayoutEvent, - PressEvent, + LayoutChangeEvent, + GestureResponderEvent, ScrollEvent, } from '../../Types/CoreEventTypes'; import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; @@ -46,7 +42,6 @@ import StyleSheet from '../../StyleSheet/StyleSheet'; import Dimensions from '../../Utilities/Dimensions'; import dismissKeyboard from '../../Utilities/dismissKeyboard'; import Platform from '../../Utilities/Platform'; -import EventEmitter from '../../vendor/emitter/EventEmitter'; import Keyboard from '../Keyboard/Keyboard'; import TextInputState from '../TextInput/TextInputState'; import processDecelerationRate from './processDecelerationRate'; @@ -132,7 +127,7 @@ import * as React from 'react'; */ // Public methods for ScrollView -export type ScrollViewImperativeMethods = $ReadOnly<{| +export type ScrollViewImperativeMethods = $ReadOnly<{ getScrollResponder: $PropertyType, getScrollableNode: $PropertyType, getInnerViewNode: $PropertyType, @@ -146,19 +141,19 @@ export type ScrollViewImperativeMethods = $ReadOnly<{| ScrollView, 'scrollResponderScrollNativeHandleToKeyboard', >, -|}>; +}>; export type DecelerationRateType = 'fast' | 'normal' | number; export type ScrollResponderType = ScrollViewImperativeMethods; -type PublicScrollViewInstance = $ReadOnly<{| - ...$Exact, +type PublicScrollViewInstance = $ReadOnly<{ + ...HostInstance, ...ScrollViewImperativeMethods, -|}>; +}>; type InnerViewInstance = React.ElementRef; -type IOSProps = $ReadOnly<{| +type IOSProps = $ReadOnly<{ /** * Controls whether iOS should automatically adjust the content inset * for scroll views that are placed behind a navigation bar or @@ -312,9 +307,9 @@ type IOSProps = $ReadOnly<{| | 'never' | 'always' ), -|}>; +}>; -type AndroidProps = $ReadOnly<{| +type AndroidProps = $ReadOnly<{ /** * Enables nested scrolling for Android API level 21+. * Nested scrolling is supported by default on iOS @@ -369,14 +364,14 @@ type AndroidProps = $ReadOnly<{| * @platform android */ fadingEdgeLength?: ?number, -|}>; +}>; -type StickyHeaderComponentType = React.AbstractComponent< - ScrollViewStickyHeaderProps, - $ReadOnly void}>, ->; +type StickyHeaderComponentType = component( + ref?: React.RefSetter<$ReadOnly void}>>, + ...ScrollViewStickyHeaderProps +); -export type Props = $ReadOnly<{| +export type Props = $ReadOnly<{ ...ViewProps, ...IOSProps, ...AndroidProps, @@ -509,10 +504,10 @@ export type Props = $ReadOnly<{| * whether content is "visible" or not. * */ - maintainVisibleContentPosition?: ?$ReadOnly<{| + maintainVisibleContentPosition?: ?$ReadOnly<{ minIndexForVisible: number, autoscrollToTopThreshold?: ?number, - |}>, + }>, /** * Called when the momentum scroll starts (scroll which occurs as the ScrollView glides to a stop). */ @@ -666,18 +661,18 @@ export type Props = $ReadOnly<{| * measure, measureLayout, etc. */ scrollViewRef?: React.RefSetter, -|}>; +}>; -type State = {| +type State = { contentKey: number, // [macOS] layoutHeight: ?number, -|}; +}; const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16; -export type ScrollViewComponentStatics = $ReadOnly<{| +export type ScrollViewComponentStatics = $ReadOnly<{ Context: typeof ScrollViewContext, -|}>; +}>; /** * Component that wraps platform ScrollView while providing @@ -754,10 +749,6 @@ class ScrollView extends React.Component { _subscriptionKeyboardDidShow: ?EventSubscription = null; _subscriptionKeyboardDidHide: ?EventSubscription = null; - #onScrollEmitter: ?EventEmitter<{ - scroll: [{x: number, y: number}], - }> = null; - state: State = { contentKey: 1, // [macOS] layoutHeight: null, @@ -829,8 +820,6 @@ class ScrollView extends React.Component { if (this._scrollAnimatedValueAttachment) { this._scrollAnimatedValueAttachment.detach(); } - - this.#onScrollEmitter?.removeAllListeners(); } /** @@ -856,9 +845,8 @@ class ScrollView extends React.Component { return this._innerView.nativeInstance; }; - getNativeScrollRef: () => TScrollViewNativeComponentInstance | null = () => { - const {nativeInstance} = this._scrollView; - return nativeInstance == null ? null : nativeInstance.componentRef.current; + getNativeScrollRef: () => HostInstance | null = () => { + return this._scrollView.nativeInstance; }; /** @@ -949,20 +937,6 @@ class ScrollView extends React.Component { Commands.flashScrollIndicators(component); }; - _subscribeToOnScroll: ( - callback: ({x: number, y: number}) => void, - ) => EventSubscription = callback => { - let onScrollEmitter = this.#onScrollEmitter; - if (onScrollEmitter == null) { - onScrollEmitter = new EventEmitter(); - this.#onScrollEmitter = onScrollEmitter; - // This is the first subscription, so make sure the native component is - // also configured to output synchronous scroll events. - this._scrollView.nativeInstance?.unstable_setEnableSyncOnScroll(true); - } - return onScrollEmitter.addListener('scroll', callback); - }; - /** * This method should be used as the callback to onFocus in a TextInputs' * parent view. Note that any module using this mixin needs to return @@ -973,12 +947,12 @@ class ScrollView extends React.Component { * @param {bool} preventNegativeScrolling Whether to allow pulling the content * down to make it meet the keyboard's top. Default is false. */ - scrollResponderScrollNativeHandleToKeyboard: ( - nodeHandle: number | React.ElementRef>, + scrollResponderScrollNativeHandleToKeyboard: ( + nodeHandle: number | HostInstance, additionalOffset?: number, preventNegativeScrollOffset?: boolean, - ) => void = ( - nodeHandle: number | React.ElementRef>, + ) => void = ( + nodeHandle: number | HostInstance, additionalOffset?: number, preventNegativeScrollOffset?: boolean, ) => { @@ -1014,22 +988,22 @@ class ScrollView extends React.Component { * @platform ios */ scrollResponderZoomTo: ( - rect: {| + rect: { x: number, y: number, width: number, height: number, animated?: boolean, - |}, + }, animated?: boolean, // deprecated, put this inside the rect argument instead ) => void = ( - rect: {| + rect: { x: number, y: number, width: number, height: number, animated?: boolean, - |}, + }, animated?: boolean, // deprecated, put this inside the rect argument instead ) => { invariant( @@ -1180,14 +1154,9 @@ class ScrollView extends React.Component { _handleScroll = (e: ScrollEvent) => { this._observedScrollSinceBecomingResponder = true; this.props.onScroll && this.props.onScroll(e); - - this.#onScrollEmitter?.emit('scroll', { - x: e.nativeEvent.contentOffset.x, - y: e.nativeEvent.contentOffset.y, - }); }; - _handleLayout = (e: LayoutEvent) => { + _handleLayout = (e: LayoutChangeEvent) => { if (this.props.invertStickyHeaders === true) { this.setState({layoutHeight: e.nativeEvent.layout.height}); } @@ -1196,7 +1165,7 @@ class ScrollView extends React.Component { } }; - _handleContentOnLayout = (e: LayoutEvent) => { + _handleContentOnLayout = (e: LayoutChangeEvent) => { const {width, height} = e.nativeEvent.layout; this.props.onContentSizeChange && this.props.onContentSizeChange(width, height); @@ -1207,45 +1176,36 @@ class ScrollView extends React.Component { (instance: InnerViewInstance): InnerViewInstance => instance, ); - _scrollView: RefForwarder< - TScrollViewNativeImperativeHandle, - PublicScrollViewInstance | null, - > = createRefForwarder(nativeImperativeHandle => { - const nativeInstance = nativeImperativeHandle.componentRef.current; - if (nativeInstance == null) { - return null; - } - - // This is a hack. Ideally we would forwardRef to the underlying - // host component. However, since ScrollView has it's own methods that can be - // called as well, if we used the standard forwardRef then these - // methods wouldn't be accessible and thus be a breaking change. - // - // Therefore we edit ref to include ScrollView's public methods so that - // they are callable from the ref. - - // $FlowFixMe[prop-missing] - Known issue with appending custom methods. - const publicInstance: PublicScrollViewInstance = Object.assign( - nativeInstance, - { - getScrollResponder: this.getScrollResponder, - getScrollableNode: this.getScrollableNode, - getInnerViewNode: this.getInnerViewNode, - getInnerViewRef: this.getInnerViewRef, - getNativeScrollRef: this.getNativeScrollRef, - scrollTo: this.scrollTo, - scrollToEnd: this.scrollToEnd, - flashScrollIndicators: this.flashScrollIndicators, - scrollResponderZoomTo: this.scrollResponderZoomTo, - // TODO: Replace unstable_subscribeToOnScroll once scrollView.addEventListener('scroll', (e: ScrollEvent) => {}, {passive: false}); - unstable_subscribeToOnScroll: this._subscribeToOnScroll, - scrollResponderScrollNativeHandleToKeyboard: - this.scrollResponderScrollNativeHandleToKeyboard, - }, - ); + _scrollView: RefForwarder = + createRefForwarder(nativeInstance => { + // This is a hack. Ideally we would forwardRef to the underlying + // host component. However, since ScrollView has it's own methods that can be + // called as well, if we used the standard forwardRef then these + // methods wouldn't be accessible and thus be a breaking change. + // + // Therefore we edit ref to include ScrollView's public methods so that + // they are callable from the ref. + + // $FlowFixMe[prop-missing] - Known issue with appending custom methods. + const publicInstance: PublicScrollViewInstance = Object.assign( + nativeInstance, + { + getScrollResponder: this.getScrollResponder, + getScrollableNode: this.getScrollableNode, + getInnerViewNode: this.getInnerViewNode, + getInnerViewRef: this.getInnerViewRef, + getNativeScrollRef: this.getNativeScrollRef, + scrollTo: this.scrollTo, + scrollToEnd: this.scrollToEnd, + flashScrollIndicators: this.flashScrollIndicators, + scrollResponderZoomTo: this.scrollResponderZoomTo, + scrollResponderScrollNativeHandleToKeyboard: + this.scrollResponderScrollNativeHandleToKeyboard, + }, + ); - return publicInstance; - }); + return publicInstance; + }); /** * Warning, this may be called several times for a single keyboard opening. @@ -1378,7 +1338,9 @@ class ScrollView extends React.Component { /** * Invoke this from an `onResponderGrant` event. */ - _handleResponderGrant: (e: PressEvent) => void = (e: PressEvent) => { + _handleResponderGrant: (e: GestureResponderEvent) => void = ( + e: GestureResponderEvent, + ) => { this._observedScrollSinceBecomingResponder = false; this.props.onResponderGrant && this.props.onResponderGrant(e); this._becameResponderWhileAnimating = this._isAnimating(); @@ -1399,7 +1361,9 @@ class ScrollView extends React.Component { /** * Invoke this from an `onResponderRelease` event. */ - _handleResponderRelease: (e: PressEvent) => void = (e: PressEvent) => { + _handleResponderRelease: (e: GestureResponderEvent) => void = ( + e: GestureResponderEvent, + ) => { this._isTouching = e.nativeEvent.touches.length !== 0; this.props.onResponderRelease && this.props.onResponderRelease(e); @@ -1484,8 +1448,8 @@ class ScrollView extends React.Component { * true. * */ - _handleStartShouldSetResponder: (e: PressEvent) => boolean = ( - e: PressEvent, + _handleStartShouldSetResponder: (e: GestureResponderEvent) => boolean = ( + e: GestureResponderEvent, ) => { // Allow any event touch pass through if the default pan responder is disabled if (this.props.disableScrollViewPanResponder === true) { @@ -1514,55 +1478,54 @@ class ScrollView extends React.Component { * * Invoke this from an `onStartShouldSetResponderCapture` event. */ - _handleStartShouldSetResponderCapture: (e: PressEvent) => boolean = ( - e: PressEvent, - ) => { - // The scroll view should receive taps instead of its descendants if: - // * it is already animating/decelerating - if (this._isAnimating()) { - return true; - } + _handleStartShouldSetResponderCapture: (e: GestureResponderEvent) => boolean = + (e: GestureResponderEvent) => { + // The scroll view should receive taps instead of its descendants if: + // * it is already animating/decelerating + if (this._isAnimating()) { + return true; + } - // Allow any event touch pass through if the default pan responder is disabled - if (this.props.disableScrollViewPanResponder === true) { - return false; - } + // Allow any event touch pass through if the default pan responder is disabled + if (this.props.disableScrollViewPanResponder === true) { + return false; + } - // * the keyboard is up, keyboardShouldPersistTaps is 'never' (the default), - // and a new touch starts with a non-textinput target (in which case the - // first tap should be sent to the scroll view and dismiss the keyboard, - // then the second tap goes to the actual interior view) - const {keyboardShouldPersistTaps} = this.props; - const keyboardNeverPersistTaps = - !keyboardShouldPersistTaps || keyboardShouldPersistTaps === 'never'; + // * the keyboard is up, keyboardShouldPersistTaps is 'never' (the default), + // and a new touch starts with a non-textinput target (in which case the + // first tap should be sent to the scroll view and dismiss the keyboard, + // then the second tap goes to the actual interior view) + const {keyboardShouldPersistTaps} = this.props; + const keyboardNeverPersistTaps = + !keyboardShouldPersistTaps || keyboardShouldPersistTaps === 'never'; + + if (typeof e.target === 'number') { + if (__DEV__) { + console.error( + 'Did not expect event target to be a number. Should have been a native component', + ); + } - if (typeof e.target === 'number') { - if (__DEV__) { - console.error( - 'Did not expect event target to be a number. Should have been a native component', - ); + return false; } - return false; - } - - // Let presses through if the soft keyboard is detached from the viewport - if (this._softKeyboardIsDetached()) { - return false; - } + // Let presses through if the soft keyboard is detached from the viewport + if (this._softKeyboardIsDetached()) { + return false; + } - if ( - keyboardNeverPersistTaps && - this._keyboardIsDismissible() && - e.target != null && - // $FlowFixMe[incompatible-call] - !TextInputState.isTextInput(e.target) - ) { - return true; - } + if ( + keyboardNeverPersistTaps && + this._keyboardIsDismissible() && + e.target != null && + // $FlowFixMe[incompatible-call] + !TextInputState.isTextInput(e.target) + ) { + return true; + } - return false; - }; + return false; + }; /** * Do we consider there to be a dismissible soft-keyboard open? @@ -1606,9 +1569,11 @@ class ScrollView extends React.Component { /** * Invoke this from an `onTouchEnd` event. * - * @param {PressEvent} e Event. + * @param {GestureResponderEvent} e Event. */ - _handleTouchEnd: (e: PressEvent) => void = (e: PressEvent) => { + _handleTouchEnd: (e: GestureResponderEvent) => void = ( + e: GestureResponderEvent, + ) => { const nativeEvent = e.nativeEvent; this._isTouching = nativeEvent.touches.length !== 0; @@ -1636,9 +1601,11 @@ class ScrollView extends React.Component { /** * Invoke this from an `onTouchCancel` event. * - * @param {PressEvent} e Event. + * @param {GestureResponderEvent} e Event. */ - _handleTouchCancel: (e: PressEvent) => void = (e: PressEvent) => { + _handleTouchCancel: (e: GestureResponderEvent) => void = ( + e: GestureResponderEvent, + ) => { this._isTouching = false; this.props.onTouchCancel && this.props.onTouchCancel(e); }; @@ -1652,9 +1619,11 @@ class ScrollView extends React.Component { * responder). The `onResponderReject` won't fire in that case - it only * fires when a *current* responder rejects our request. * - * @param {PressEvent} e Touch Start event. + * @param {GestureResponderEvent} e Touch Start event. */ - _handleTouchStart: (e: PressEvent) => void = (e: PressEvent) => { + _handleTouchStart: (e: GestureResponderEvent) => void = ( + e: GestureResponderEvent, + ) => { this._isTouching = true; this.props.onTouchStart && this.props.onTouchStart(e); }; @@ -1668,9 +1637,11 @@ class ScrollView extends React.Component { * responder). The `onResponderReject` won't fire in that case - it only * fires when a *current* responder rejects our request. * - * @param {PressEvent} e Touch Start event. + * @param {GestureResponderEvent} e Touch Start event. */ - _handleTouchMove: (e: PressEvent) => void = (e: PressEvent) => { + _handleTouchMove: (e: GestureResponderEvent) => void = ( + e: GestureResponderEvent, + ) => { this.props.onTouchMove && this.props.onTouchMove(e); }; @@ -1865,8 +1836,9 @@ class ScrollView extends React.Component { } const refreshControl = this.props.refreshControl; - const scrollViewRef: React.RefSetter = - this._scrollView.getForwardingRef(this.props.scrollViewRef); + const scrollViewRef = this._scrollView.getForwardingRef( + this.props.scrollViewRef, + ); if (refreshControl) { if (Platform.OS === 'ios') { diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollViewCommands.js b/packages/react-native/Libraries/Components/ScrollView/ScrollViewCommands.js index 6ff89b42017f40..2e5385d25abe6c 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewCommands.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewCommands.js @@ -14,7 +14,7 @@ import type {Double} from '../../Types/CodegenTypes'; import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; import * as React from 'react'; -type ScrollViewNativeComponentType = HostComponent; +type ScrollViewNativeComponentType = HostComponent<{...}>; interface NativeCommands { +flashScrollIndicators: ( viewRef: React.ElementRef, diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollViewContext.js b/packages/react-native/Libraries/Components/ScrollView/ScrollViewContext.js index 4c194ad54941e5..e08fda8bc656f5 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewContext.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewContext.js @@ -18,5 +18,7 @@ if (__DEV__) { } export default ScrollViewContext; +// $FlowFixMe[incompatible-type] frozen objects are readonly export const HORIZONTAL: Value = Object.freeze({horizontal: true}); +// $FlowFixMe[incompatible-type] frozen objects are readonly export const VERTICAL: Value = Object.freeze({horizontal: false}); diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js index 3776962c861d24..748a5bae47d186 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js @@ -42,10 +42,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = }, validAttributes: { contentOffset: { - diff: require('../../Utilities/differ/pointsDiffer'), + diff: require('../../Utilities/differ/pointsDiffer').default, }, decelerationRate: true, - enableSyncOnScroll: true, // Fabric only. disableIntervalMomentum: true, maintainVisibleContentPosition: true, pagingEnabled: true, @@ -115,6 +114,14 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = topScrollToTop: { registrationName: 'onScrollToTop', }, + // [macOS + topInvertedDidChange: { + registrationName: 'onInvertedDidChange', + }, + topPreferredScrollerStyleDidChange: { + registrationName: 'onPreferredScrollerStyleDidChange', + }, + // macOS] }, validAttributes: { alwaysBounceHorizontal: true, @@ -127,15 +134,14 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = canCancelContentTouches: true, centerContent: true, contentInset: { - diff: require('../../Utilities/differ/insetsDiffer'), + diff: require('../../Utilities/differ/insetsDiffer').default, }, contentOffset: { - diff: require('../../Utilities/differ/pointsDiffer'), + diff: require('../../Utilities/differ/pointsDiffer').default, }, contentInsetAdjustmentBehavior: true, decelerationRate: true, endDraggingSensitivityMultiplier: true, - enableSyncOnScroll: true, // Fabric only. directionalLockEnabled: true, disableIntervalMomentum: true, indicatorStyle: true, @@ -149,7 +155,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = scrollEnabled: true, scrollEventThrottle: true, scrollIndicatorInsets: { - diff: require('../../Utilities/differ/insetsDiffer'), + diff: require('../../Utilities/differ/insetsDiffer').default, }, scrollToOverflowEnabled: true, scrollsToTop: true, diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollViewStickyHeader.js b/packages/react-native/Libraries/Components/ScrollView/ScrollViewStickyHeader.js index 052f01eb164d0e..1615f3fe0b9671 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewStickyHeader.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewStickyHeader.js @@ -38,10 +38,10 @@ type Instance = { ... }; -const ScrollViewStickyHeaderWithForwardedRef: React.AbstractComponent< - Props, - Instance, -> = React.forwardRef(function ScrollViewStickyHeader(props, forwardedRef) { +const ScrollViewStickyHeaderWithForwardedRef: component( + ref: React.RefSetter, + ...props: Props +) = React.forwardRef(function ScrollViewStickyHeader(props, forwardedRef) { const { inverted, scrollViewHeight, @@ -65,8 +65,8 @@ const ScrollViewStickyHeaderWithForwardedRef: React.AbstractComponent< ref.setNextHeaderY = setNextHeaderLayoutY; setIsFabric(isFabricPublicInstance(ref)); }, []); - const ref: (React.ElementRef | null) => void = - // $FlowFixMe[incompatible-type] - Ref is mutated by `callbackRef`. + const ref: React.RefSetter> = + // $FlowFixMe[prop-missing] - Instance is mutated to have `setNextHeaderY`. useMergeRefs(callbackRef, forwardedRef); const offset = useMemo( @@ -275,12 +275,12 @@ const ScrollViewStickyHeaderWithForwardedRef: React.AbstractComponent< : null; return ( - /* $FlowFixMe[prop-missing] passthroughAnimatedPropExplicitValues isn't properly - included in the Animated.View flow type. */ ; -type AndroidProps = $ReadOnly<{| +type AndroidProps = $ReadOnly<{ /** * The background color of the status bar. * @platform android @@ -69,9 +69,9 @@ type AndroidProps = $ReadOnly<{| * @platform android */ translucent?: ?boolean, -|}>; +}>; -type IOSProps = $ReadOnly<{| +type IOSProps = $ReadOnly<{ /** * If the network activity indicator should be visible. * @@ -85,9 +85,9 @@ type IOSProps = $ReadOnly<{| * @platform ios */ showHideTransition?: ?('fade' | 'slide' | 'none'), -|}>; +}>; -type Props = $ReadOnly<{| +type Props = $ReadOnly<{ ...AndroidProps, ...IOSProps, /** @@ -103,7 +103,7 @@ type Props = $ReadOnly<{| * Sets the color of the status bar text. */ barStyle?: ?('default' | 'light-content' | 'dark-content'), -|}>; +}>; /** * Merges the prop stack with the default values. @@ -478,4 +478,4 @@ class StatusBar extends React.Component { } } -module.exports = StatusBar; +export default StatusBar; diff --git a/packages/react-native/Libraries/Components/Switch/Switch.js b/packages/react-native/Libraries/Components/Switch/Switch.js index eb0f18521e47c8..b1aa2383defc87 100644 --- a/packages/react-native/Libraries/Components/Switch/Switch.js +++ b/packages/react-native/Libraries/Components/Switch/Switch.js @@ -130,12 +130,14 @@ const returnsTrue = () => true; ``` */ -const SwitchWithForwardedRef: React.AbstractComponent< - Props, - React.ElementRef< - typeof SwitchNativeComponent | typeof AndroidSwitchNativeComponent, - >, -> = React.forwardRef(function Switch(props, forwardedRef): React.Node { +type SwitchRef = React.ElementRef< + typeof SwitchNativeComponent | typeof AndroidSwitchNativeComponent, +>; + +const SwitchWithForwardedRef: component( + ref: React.RefSetter, + ...props: Props +) = React.forwardRef(function Switch(props, forwardedRef): React.Node { const { disabled, ios_backgroundColor, diff --git a/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js b/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js index 89b438e54944ef..a97e82e86c0444 100644 --- a/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js +++ b/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js @@ -87,7 +87,7 @@ type Props = $ReadOnly<{| backgroundColor?: ?ColorValue, |}>; -const InputAccessoryView: React.AbstractComponent = (props: Props) => { +const InputAccessoryView: React.ComponentType = (props: Props) => { const {width} = useWindowDimensions(); if (Platform.OS === 'ios') { diff --git a/packages/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js b/packages/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js index 2c96ead11b89dc..257e4956abfee0 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js @@ -18,7 +18,7 @@ import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentR import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; import RCTTextInputViewConfig from './RCTTextInputViewConfig'; -type NativeType = HostComponent; +type NativeType = HostComponent<{...}>; type NativeCommands = TextInputNativeCommands; @@ -35,11 +35,11 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { }, }; -const MultilineTextInputNativeComponent: HostComponent = - NativeComponentRegistry.get( +const MultilineTextInputNativeComponent: HostComponent<{...}> = + NativeComponentRegistry.get<{...}>( 'RCTMultilineTextInputView', () => __INTERNAL_VIEW_CONFIG, ); // flowlint-next-line unclear-type:off -export default ((MultilineTextInputNativeComponent: any): HostComponent); +export default ((MultilineTextInputNativeComponent: any): HostComponent<{...}>); diff --git a/packages/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js b/packages/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js index c14e92102cb911..a94bdd00b793e7 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js @@ -18,7 +18,7 @@ import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentR import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; import RCTTextInputViewConfig from './RCTTextInputViewConfig'; -type NativeType = HostComponent; +type NativeType = HostComponent<{...}>; type NativeCommands = TextInputNativeCommands; @@ -31,11 +31,13 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { ...RCTTextInputViewConfig, }; -const SinglelineTextInputNativeComponent: HostComponent = - NativeComponentRegistry.get( +const SinglelineTextInputNativeComponent: HostComponent<{...}> = + NativeComponentRegistry.get<{...}>( 'RCTSinglelineTextInputView', () => __INTERNAL_VIEW_CONFIG, ); // flowlint-next-line unclear-type:off -export default ((SinglelineTextInputNativeComponent: any): HostComponent); +export default ((SinglelineTextInputNativeComponent: any): HostComponent<{ + ... +}>); diff --git a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js index 602ad01d448521..108f881da5e49a 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -85,6 +85,9 @@ const RCTTextInputViewConfig = { topContentSizeChange: { registrationName: 'onContentSizeChange', }, + topPaste: { + registrationName: 'onPaste', + }, topAutoCorrectChange: { registrationName: 'onAutoCorrectChange', }, @@ -123,6 +126,7 @@ const RCTTextInputViewConfig = { }, editable: true, inputAccessoryViewID: true, + inputAccessoryViewButtonLabel: true, caretHidden: true, enablesReturnKeyAutomatically: true, placeholderTextColor: { @@ -153,15 +157,18 @@ const RCTTextInputViewConfig = { showSoftInputOnFocus: true, autoFocus: true, lineBreakStrategyIOS: true, + lineBreakModeIOS: true, smartInsertDelete: true, // [macOS clearTextOnSubmit: true, grammarCheck: true, hideVerticalScrollIndicator: true, + href: true, pastedTypes: true, submitKeyEvents: true, tooltip: true, cursorColor: {process: require('../../StyleSheet/processColor').default}, + disableWritingTools: true, // macOS] ...ConditionallyIgnoredEventHandlers({ onChange: true, diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index 97bd846a374a18..06d104f3a2d9e9 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -8,7 +8,7 @@ * @format */ -import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +import type {HostInstance} from '../../Renderer/shims/ReactNativeTypes'; import type { PressEvent, ScrollEvent, @@ -22,7 +22,6 @@ import { type ViewStyleProp, } from '../../StyleSheet/StyleSheet'; import * as React from 'react'; -type ComponentRef = React.ElementRef>; type ReactRefSetter = {current: null | T, ...} | ((ref: null | T) => mixed); @@ -248,7 +247,12 @@ export type TextContentType = | 'birthdate' | 'birthdateDay' | 'birthdateMonth' - | 'birthdateYear'; + | 'birthdateYear' + | 'cellularEID' + | 'cellularIMEI' + | 'dateTime' + | 'flightNumber' + | 'shipmentTrackingNumber'; export type enterKeyHintType = | 'enter' @@ -312,6 +316,12 @@ type IOSProps = $ReadOnly<{| */ inputAccessoryViewID?: ?string, + /** + * An optional label that overrides the default input accessory view button label. + * @platform ios + */ + inputAccessoryViewButtonLabel?: ?string, + /** * Determines the color of the keyboard. * @platform ios @@ -362,6 +372,19 @@ type IOSProps = $ReadOnly<{| */ lineBreakStrategyIOS?: ?('none' | 'standard' | 'hangul-word' | 'push-out'), + /** + * Set line break mode on iOS. + * @platform ios + */ + lineBreakModeIOS?: ?( + | 'wordWrapping' + | 'char' + | 'clip' + | 'head' + | 'middle' + | 'tail' + ), + /** * If `false`, the iOS system will not insert an extra space after a paste operation * neither delete one or two spaces after a cut or delete operation. @@ -751,9 +774,7 @@ export type Props = $ReadOnly<{| */ editable?: ?boolean, - forwardedRef?: ?ReactRefSetter< - React.ElementRef> & ImperativeMethods, - >, + forwardedRef?: ?ReactRefSetter, /** * `enterKeyHint` defines what action label (or icon) to present for the enter key on virtual keyboards. @@ -1099,7 +1120,7 @@ export type Props = $ReadOnly<{| type ImperativeMethods = $ReadOnly<{| clear: () => void, isFocused: () => boolean, - getNativeRef: () => ?React.ElementRef>, + getNativeRef: () => ?HostInstance, setSelection: (start: number, end: number) => void, setGhostText: (ghostText: ?string) => void, // [macOS] |}>; @@ -1215,22 +1236,18 @@ type ImperativeMethods = $ReadOnly<{| * or control this param programmatically with native code. * */ -type InternalTextInput = (props: Props) => React.Node; +type InternalTextInput = component( + ref: React.RefSetter<$ReadOnly<{...HostInstance, ...ImperativeMethods}>>, + ...Props +); export type TextInputComponentStatics = $ReadOnly<{| State: $ReadOnly<{| - currentlyFocusedInput: () => ?ComponentRef, + currentlyFocusedInput: () => ?HostInstance, currentlyFocusedField: () => ?number, - focusTextInput: (textField: ?ComponentRef) => void, - blurTextInput: (textField: ?ComponentRef) => void, + focusTextInput: (textField: ?HostInstance) => void, + blurTextInput: (textField: ?HostInstance) => void, |}>, |}>; -export type TextInputType = React.AbstractComponent< - React.ElementConfig, - $ReadOnly<{| - ...React.ElementRef>, - ...ImperativeMethods, - |}>, -> & - TextInputComponentStatics; +export type TextInputType = InternalTextInput & TextInputComponentStatics; diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index c2cfd2b2f2602b..5fe230c1611378 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -8,12 +8,12 @@ * @format */ -import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +import type {HostInstance} from '../../Renderer/shims/ReactNativeTypes'; import type {____TextStyle_Internal as TextStyleInternal} from '../../StyleSheet/StyleSheetTypes'; import type { - PressEvent, + GestureResponderEvent, ScrollEvent, - SyntheticEvent, + NativeSyntheticEvent, } from '../../Types/CoreEventTypes'; import type {ViewProps} from '../View/ViewPropTypes'; import type {TextInputType} from './TextInput.flow'; @@ -37,10 +37,10 @@ import * as React from 'react'; import {useCallback, useLayoutEffect, useRef, useState} from 'react'; type ReactRefSetter = {current: null | T, ...} | ((ref: null | T) => mixed); -type TextInputInstance = React.ElementRef> & { +type TextInputInstance = HostInstance & { +clear: () => void, +isFocused: () => boolean, - +getNativeRef: () => ?React.ElementRef>, + +getNativeRef: () => ?HostInstance, +setSelection: (start: number, end: number) => void, +setGhostText: (ghostText: ?string) => void, // [macOS] }; @@ -67,108 +67,114 @@ if (Platform.OS === 'android') { require('./RCTMultilineTextInputNativeComponent').Commands; } -export type ChangeEvent = SyntheticEvent< - $ReadOnly<{| - eventCount: number, - target: number, - text: string, - |}>, ->; +export type TextInputChangeEventData = $ReadOnly<{ + eventCount: number, + target: number, + text: string, +}>; -export type TextInputEvent = SyntheticEvent< - $ReadOnly<{| +export type TextInputChangeEvent = + NativeSyntheticEvent; + +export type TextInputEvent = NativeSyntheticEvent< + $ReadOnly<{ eventCount: number, previousText: string, - range: $ReadOnly<{| + range: $ReadOnly<{ start: number, end: number, - |}>, + }>, target: number, text: string, - |}>, + }>, >; -export type ContentSizeChangeEvent = SyntheticEvent< - $ReadOnly<{| - target: number, - contentSize: $ReadOnly<{| - width: number, - height: number, - |}>, - |}>, ->; +export type TextInputContentSizeChangeEventData = $ReadOnly<{ + target: number, + contentSize: $ReadOnly<{ + width: number, + height: number, + }>, +}>; -type TargetEvent = SyntheticEvent< - $ReadOnly<{| - target: number, - |}>, ->; +export type TextInputContentSizeChangeEvent = + NativeSyntheticEvent; + +export type TargetEvent = $ReadOnly<{ + target: number, +}>; -export type BlurEvent = TargetEvent; -export type FocusEvent = TargetEvent; +export type TextInputFocusEventData = TargetEvent; -type Selection = $ReadOnly<{| +export type TextInputBlurEvent = NativeSyntheticEvent; +export type TextInputFocusEvent = NativeSyntheticEvent; + +type Selection = $ReadOnly<{ start: number, end: number, -|}>; +}>; -export type SelectionChangeEvent = SyntheticEvent< - $ReadOnly<{| - selection: Selection, - target: number, - |}>, ->; +export type TextInputSelectionChangeEventData = $ReadOnly<{ + ...TargetEvent, + selection: Selection, +}>; -export type KeyPressEvent = SyntheticEvent< - $ReadOnly<{| - key: string, - target?: ?number, - eventCount?: ?number, - |}>, ->; +export type TextInputSelectionChangeEvent = + NativeSyntheticEvent; -export type EditingEvent = SyntheticEvent< - $ReadOnly<{| - eventCount: number, - text: string, - target: number, - |}>, ->; +export type TextInputKeyPressEventData = $ReadOnly<{ + ...TargetEvent, + key: string, + target?: ?number, + eventCount: number, +}>; + +export type TextInputKeyPressEvent = + NativeSyntheticEvent; + +export type TextInputEndEditingEventData = $ReadOnly<{ + ...TargetEvent, + eventCount: number, + text: string, +}>; + +export type TextInputEditingEvent = + NativeSyntheticEvent; // [macOS macOS-only -export type SettingChangeEvent = SyntheticEvent< - $ReadOnly<{| +export type SettingChangeEvent = NativeSyntheticEvent< + $ReadOnly<{ enabled: boolean, - |}>, + }>, >; -export type PasteEvent = SyntheticEvent< - $ReadOnly<{| - dataTransfer: {| - files: $ReadOnlyArray<{| +export type PasteEvent = NativeSyntheticEvent< + $ReadOnly<{ + dataTransfer: { + files: $ReadOnlyArray<{ height: number, size: number, type: string, uri: string, width: number, - |}>, - items: $ReadOnlyArray<{| + }>, + items: $ReadOnlyArray<{ kind: string, type: string, - |}>, + }>, types: $ReadOnlyArray, - |}, - |}>, + }, + }>, >; -export type SubmitKeyEvent = $ReadOnly<{| +export type SubmitKeyEvent = $ReadOnly<{ key: string, altKey?: ?boolean, ctrlKey?: ?boolean, metaKey?: ?boolean, shiftKey?: ?boolean, functionKey?: ?boolean, -|}>; +}>; // macOS] // [macOS @@ -197,26 +203,31 @@ type DataDetectorTypesType = // macOS] export type KeyboardType = - // Cross Platform | 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'number-pad' | 'decimal-pad' - | 'url' - // iOS-only + | 'url'; + +export type KeyboardTypeIOS = | 'ascii-capable' | 'numbers-and-punctuation' | 'name-phone-pad' | 'twitter' | 'web-search' // iOS 10+ only - | 'ascii-capable-number-pad' - // Android-only - | 'visible-password'; + | 'ascii-capable-number-pad'; -export type InputMode = +export type KeyboardTypeAndroid = 'visible-password'; + +export type KeyboardTypeOptions = + | KeyboardType + | KeyboardTypeIOS + | KeyboardTypeAndroid; + +export type InputModeOptions = | 'none' | 'text' | 'decimal' @@ -226,17 +237,9 @@ export type InputMode = | 'email' | 'url'; -export type ReturnKeyType = - // Cross Platform - | 'done' - | 'go' - | 'next' - | 'search' - | 'send' - // Android-only - | 'none' - | 'previous' - // iOS-only +export type ReturnKeyType = 'done' | 'go' | 'next' | 'search' | 'send'; + +export type ReturnKeyTypeIOS = | 'default' | 'emergency-call' | 'google' @@ -244,6 +247,13 @@ export type ReturnKeyType = | 'route' | 'yahoo'; +export type ReturnKeyTypeAndroid = 'none' | 'previous'; + +export type ReturnKeyTypeOptions = + | ReturnKeyType + | ReturnKeyTypeIOS + | ReturnKeyTypeAndroid; + export type SubmitBehavior = 'submit' | 'blurAndSubmit' | 'newline'; export type AutoCapitalize = 'none' | 'sentences' | 'words' | 'characters'; @@ -289,23 +299,27 @@ export type TextContentType = | 'birthdate' | 'birthdateDay' | 'birthdateMonth' - | 'birthdateYear'; + | 'birthdateYear' + | 'cellularEID' + | 'cellularIMEI' + | 'dateTime' + | 'flightNumber' + | 'shipmentTrackingNumber'; -export type enterKeyHintType = - // Cross Platform - | 'done' - | 'go' - | 'next' - | 'search' - | 'send' - // Android-only - | 'previous' - // iOS-only - | 'enter'; +export type EnterKeyHintTypeAndroid = 'previous'; + +export type EnterKeyHintTypeIOS = 'enter'; + +export type EnterKeyHintType = 'done' | 'go' | 'next' | 'search' | 'send'; + +export type EnterKeyHintTypeOptions = + | EnterKeyHintType + | EnterKeyHintTypeAndroid + | EnterKeyHintTypeIOS; type PasswordRules = string; -type IOSProps = $ReadOnly<{| +export type TextInputIOSProps = $ReadOnly<{ /** * When the clear button should appear on the right side of the text view. * This property is supported only for single-line TextInput component. @@ -356,6 +370,12 @@ type IOSProps = $ReadOnly<{| */ inputAccessoryViewID?: ?string, + /** + * An optional label that overrides the default input accessory view button label. + * @platform ios + */ + inputAccessoryViewButtonLabel?: ?string, + /** * Determines the color of the keyboard. * @platform ios @@ -409,6 +429,19 @@ type IOSProps = $ReadOnly<{| */ lineBreakStrategyIOS?: ?('none' | 'standard' | 'hangul-word' | 'push-out'), + /** + * Set line break mode on iOS. + * @platform ios + */ + lineBreakModeIOS?: ?( + | 'wordWrapping' + | 'char' + | 'clip' + | 'head' + | 'middle' + | 'tail' + ), + /** * If `false`, the iOS system will not insert an extra space after a paste operation * neither delete one or two spaces after a cut or delete operation. @@ -418,10 +451,10 @@ type IOSProps = $ReadOnly<{| * @platform ios */ smartInsertDelete?: ?boolean, -|}>; +}>; // [macOS -type MacOSProps = $ReadOnly<{| +export type TextInputMacOSProps = $ReadOnly<{ /** * If `true`, clears the text field synchronously before `onSubmitEditing` is emitted. * @@ -506,10 +539,10 @@ type MacOSProps = $ReadOnly<{| * @platform macos */ tooltip?: ?string, -|}>; +}>; // macOS] -type AndroidProps = $ReadOnly<{| +export type TextInputAndroidProps = $ReadOnly<{ /** * When provided it will set the color of the cursor (or "caret") in the component. * Unlike the behavior of `selectionColor` the cursor color will be set independently @@ -594,16 +627,16 @@ type AndroidProps = $ReadOnly<{| * @platform android */ underlineColorAndroid?: ?ColorValue, -|}>; +}>; export type PasteType = 'fileUrl' | 'image' | 'string'; // [macOS] export type PastedTypesType = PasteType | $ReadOnlyArray; // [macOS] -export type Props = $ReadOnly<{| - ...$Diff>, - ...IOSProps, - ...MacOSProps, // [macOS] - ...AndroidProps, +export type TextInputProps = $ReadOnly<{ + ...$Diff>, + ...TextInputIOSProps, + ...TextInputMacOSProps, // [macOS] + ...TextInputAndroidProps, /** * Can tell `TextInput` to automatically capitalize certain characters. @@ -925,17 +958,17 @@ export type Props = $ReadOnly<{| /** * Called when a single tap gesture is detected. */ - onPress?: ?(event: PressEvent) => mixed, + onPress?: ?(event: GestureResponderEvent) => mixed, /** * Called when a touch is engaged. */ - onPressIn?: ?(event: PressEvent) => mixed, + onPressIn?: ?(event: GestureResponderEvent) => mixed, /** * Called when a touch is released. */ - onPressOut?: ?(event: PressEvent) => mixed, + onPressOut?: ?(event: GestureResponderEvent) => mixed, /** * Callback that is called when the text input selection is changed. @@ -1018,10 +1051,10 @@ export type Props = $ReadOnly<{| * The start and end of the text input's selection. Set start and end to * the same value to position the cursor. */ - selection?: ?$ReadOnly<{| + selection?: ?$ReadOnly<{ start: number, end?: ?number, - |}>, + }>, /** * The highlight and cursor color of the text input. @@ -1102,7 +1135,7 @@ export type Props = $ReadOnly<{| * unwanted edits without flicker. */ value?: ?Stringish, -|}>; +}>; type ViewCommands = $NonMaybeType< | typeof AndroidTextInputCommands @@ -1110,10 +1143,10 @@ type ViewCommands = $NonMaybeType< | typeof RCTSinglelineTextInputNativeCommands, >; -type LastNativeSelection = {| +type LastNativeSelection = { selection: Selection, mostRecentEventCount: number, -|}; +}; const emptyFunctionThatReturnsTrue = () => true; @@ -1133,7 +1166,7 @@ function useTextInputStateSynchronization_STATE({ props: Props, mostRecentEventCount: number, selection: ?Selection, - inputRef: React.RefObject>>, + inputRef: React.RefObject, text: string, viewCommands: ViewCommands, }): { @@ -1214,7 +1247,7 @@ function useTextInputStateSynchronization_REFS({ props: Props, mostRecentEventCount: number, selection: ?Selection, - inputRef: React.RefObject>>, + inputRef: React.RefObject, text: string, viewCommands: ViewCommands, }): { @@ -1427,7 +1460,7 @@ function InternalTextInput(props: Props): React.Node { ...otherProps } = props; - const inputRef = useRef>>(null); + const inputRef = useRef(null); const selection: ?Selection = propsSelection == null @@ -1521,13 +1554,6 @@ function InternalTextInput(props: Props): React.Node { ); } }, - // TODO: Fix this returning true on null === null, when no input is focused - isFocused(): boolean { - return TextInputState.currentlyFocusedInput() === inputRef.current; - }, - getNativeRef(): ?React.ElementRef> { - return inputRef.current; - }, setSelection(start: number, end: number): void { if (inputRef.current != null) { viewCommands.setTextAndSelection( @@ -1539,6 +1565,13 @@ function InternalTextInput(props: Props): React.Node { ); } }, + // TODO: Fix this returning true on null === null, when no input is focused + isFocused(): boolean { + return TextInputState.currentlyFocusedInput() === inputRef.current; + }, + getNativeRef(): ?HostInstance { + return inputRef.current; + }, // [macOS setGhostText(ghostText: ?string): void { if (inputRef.current != null) { @@ -1649,7 +1682,7 @@ function InternalTextInput(props: Props): React.Node { const config = React.useMemo( () => ({ hitSlop, - onPress: (event: PressEvent) => { + onPress: (event: GestureResponderEvent) => { onPress?.(event); if (editable !== false) { if (inputRef.current != null) { @@ -1684,7 +1717,7 @@ function InternalTextInput(props: Props): React.Node { // TextInput handles onBlur and onFocus events // so omitting onBlur and onFocus pressability handlers here. - const {onBlur, onFocus, ...eventHandlers} = usePressability(config) || {}; + const {onBlur, onFocus, ...eventHandlers} = usePressability(config); let _accessibilityState; if ( @@ -1942,11 +1975,11 @@ const autoCompleteWebToTextContentTypeMap = { username: 'username', }; -const ExportedForwardRef: React.AbstractComponent< - React.ElementConfig, - TextInputInstance, +const ExportedForwardRef: component( + ref: React.RefSetter, + ...props: React.ElementConfig // $FlowFixMe[incompatible-call] -> = React.forwardRef(function TextInput( +) = React.forwardRef(function TextInput( { allowFontScaling = true, rejectResponderTermination = true, @@ -2015,14 +2048,14 @@ ExportedForwardRef.State = { blurTextInput: TextInputState.blurTextInput, }; -export type TextInputComponentStatics = $ReadOnly<{| - State: $ReadOnly<{| +export type TextInputComponentStatics = $ReadOnly<{ + State: $ReadOnly<{ currentlyFocusedInput: typeof TextInputState.currentlyFocusedInput, currentlyFocusedField: typeof TextInputState.currentlyFocusedField, focusTextInput: typeof TextInputState.focusTextInput, blurTextInput: typeof TextInputState.blurTextInput, - |}>, -|}>; + }>, +}>; const styles = StyleSheet.create({ multilineDefault: { @@ -2041,4 +2074,4 @@ const verticalAlignToTextAlignVerticalMap = { }; // $FlowFixMe[unclear-type] Unclear type. Using `any` type is not safe. -module.exports = ((ExportedForwardRef: any): TextInputType); +export default ExportedForwardRef as any as TextInputType; diff --git a/packages/react-native/Libraries/Components/TextInput/TextInputState.js b/packages/react-native/Libraries/Components/TextInput/TextInputState.js index 1fe8a152b57f5c..98cfa67aaaf345 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInputState.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInputState.js @@ -13,7 +13,7 @@ // through here. import type { - HostComponent, + HostInstance, MeasureInWindowOnSuccessCallback, MeasureLayoutOnSuccessCallback, MeasureOnSuccessCallback, @@ -23,25 +23,23 @@ import {Commands as AndroidTextInputCommands} from '../../Components/TextInput/A import {Commands as iOSTextInputCommands} from '../../Components/TextInput/RCTSingelineTextInputNativeComponent'; const {findNodeHandle} = require('../../ReactNative/RendererProxy'); -const Platform = require('../../Utilities/Platform'); -const React = require('react'); -type ComponentRef = React.ElementRef>; +const Platform = require('../../Utilities/Platform').default; -let currentlyFocusedInputRef: ?ComponentRef = null; +let currentlyFocusedInputRef: ?HostInstance = null; const inputs = new Set<{ blur(): void, focus(): void, measure(callback: MeasureOnSuccessCallback): void, measureInWindow(callback: MeasureInWindowOnSuccessCallback): void, measureLayout( - relativeToNativeNode: number | React.ElementRef>, + relativeToNativeNode: number | HostInstance, onSuccess: MeasureLayoutOnSuccessCallback, onFail?: () => void, ): void, setNativeProps(nativeProps: {...}): void, }>(); -function currentlyFocusedInput(): ?ComponentRef { +function currentlyFocusedInput(): ?HostInstance { return currentlyFocusedInputRef; } @@ -59,13 +57,13 @@ function currentlyFocusedField(): ?number { return findNodeHandle(currentlyFocusedInputRef); } -function focusInput(textField: ?ComponentRef): void { +function focusInput(textField: ?HostInstance): void { if (currentlyFocusedInputRef !== textField && textField != null) { currentlyFocusedInputRef = textField; } } -function blurInput(textField: ?ComponentRef): void { +function blurInput(textField: ?HostInstance): void { if (currentlyFocusedInputRef === textField && textField != null) { currentlyFocusedInputRef = null; } @@ -92,7 +90,7 @@ function blurField(textFieldID: ?number) { * Focuses the specified text field * noop if the text field was already focused or if the field is not editable */ -function focusTextInput(textField: ?ComponentRef) { +function focusTextInput(textField: ?HostInstance) { if (typeof textField === 'number') { if (__DEV__) { console.error( @@ -131,7 +129,7 @@ function focusTextInput(textField: ?ComponentRef) { * Unfocuses the specified text field * noop if it wasn't focused */ -function blurTextInput(textField: ?ComponentRef) { +function blurTextInput(textField: ?HostInstance) { if (typeof textField === 'number') { if (__DEV__) { console.error( @@ -157,7 +155,7 @@ function blurTextInput(textField: ?ComponentRef) { } } -function registerInput(textField: ComponentRef) { +function registerInput(textField: HostInstance) { if (typeof textField === 'number') { if (__DEV__) { console.error( @@ -171,7 +169,7 @@ function registerInput(textField: ComponentRef) { inputs.add(textField); } -function unregisterInput(textField: ComponentRef) { +function unregisterInput(textField: HostInstance) { if (typeof textField === 'number') { if (__DEV__) { console.error( @@ -184,7 +182,7 @@ function unregisterInput(textField: ComponentRef) { inputs.delete(textField); } -function isTextInput(textField: ComponentRef): boolean { +function isTextInput(textField: HostInstance): boolean { if (typeof textField === 'number') { if (__DEV__) { console.error( @@ -198,7 +196,7 @@ function isTextInput(textField: ComponentRef): boolean { return inputs.has(textField); } -module.exports = { +const TextInputState = { currentlyFocusedInput, focusInput, blurInput, @@ -212,3 +210,5 @@ module.exports = { unregisterInput, isTextInput, }; + +export default TextInputState; diff --git a/packages/react-native/Libraries/Components/Touchable/BoundingDimensions.js b/packages/react-native/Libraries/Components/Touchable/BoundingDimensions.js index 87d9279bc68637..6bad6725057291 100644 --- a/packages/react-native/Libraries/Components/Touchable/BoundingDimensions.js +++ b/packages/react-native/Libraries/Components/Touchable/BoundingDimensions.js @@ -5,9 +5,11 @@ * LICENSE file in the root directory of this source tree. * * @format + * @flow strict-local */ 'use strict'; + import PooledClass from './PooledClass'; const twoArgumentPooler = PooledClass.twoArgumentPooler; @@ -19,11 +21,14 @@ const twoArgumentPooler = PooledClass.twoArgumentPooler; * @param {number} height Height of bounding rectangle. * @constructor BoundingDimensions */ -function BoundingDimensions(width, height) { +// $FlowFixMe[missing-this-annot] +function BoundingDimensions(width: number, height: number) { this.width = width; this.height = height; } +// $FlowFixMe[prop-missing] +// $FlowFixMe[missing-this-annot] BoundingDimensions.prototype.destructor = function () { this.width = null; this.height = null; @@ -33,13 +38,16 @@ BoundingDimensions.prototype.destructor = function () { * @param {HTMLElement} element Element to return `BoundingDimensions` for. * @return {BoundingDimensions} Bounding dimensions of `element`. */ -BoundingDimensions.getPooledFromElement = function (element) { +BoundingDimensions.getPooledFromElement = function ( + element: HTMLElement, +): typeof BoundingDimensions { + // $FlowFixMe[prop-missing] return BoundingDimensions.getPooled( element.offsetWidth, element.offsetHeight, ); }; -PooledClass.addPoolingTo(BoundingDimensions, twoArgumentPooler); +PooledClass.addPoolingTo(BoundingDimensions as $FlowFixMe, twoArgumentPooler); module.exports = BoundingDimensions; diff --git a/packages/react-native/Libraries/Components/Touchable/Position.js b/packages/react-native/Libraries/Components/Touchable/Position.js index adbbd170c0d0c5..06d9e8958aa3ae 100644 --- a/packages/react-native/Libraries/Components/Touchable/Position.js +++ b/packages/react-native/Libraries/Components/Touchable/Position.js @@ -5,9 +5,11 @@ * LICENSE file in the root directory of this source tree. * * @format + * @flow strict-local */ 'use strict'; + import PooledClass from './PooledClass'; const twoArgumentPooler = PooledClass.twoArgumentPooler; @@ -20,16 +22,19 @@ const twoArgumentPooler = PooledClass.twoArgumentPooler; * @param {number} windowStartKey Key that window starts at. * @param {number} windowEndKey Key that window ends at. */ -function Position(left, top) { +// $FlowFixMe[missing-this-annot] +function Position(left: number, top: number) { this.left = left; this.top = top; } +// $FlowFixMe[prop-missing] +// $FlowFixMe[missing-this-annot] Position.prototype.destructor = function () { this.left = null; this.top = null; }; -PooledClass.addPoolingTo(Position, twoArgumentPooler); +PooledClass.addPoolingTo(Position as $FlowFixMe, twoArgumentPooler); module.exports = Position; diff --git a/packages/react-native/Libraries/Components/Touchable/Touchable.js b/packages/react-native/Libraries/Components/Touchable/Touchable.js index ed735af973c757..728d6b3617ac28 100644 --- a/packages/react-native/Libraries/Components/Touchable/Touchable.js +++ b/packages/react-native/Libraries/Components/Touchable/Touchable.js @@ -10,7 +10,7 @@ import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType'; import type {ColorValue} from '../../StyleSheet/StyleSheet'; -import type {PressEvent} from '../../Types/CoreEventTypes'; +import type {GestureResponderEvent} from '../../Types/CoreEventTypes'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import UIManager from '../../ReactNative/UIManager'; @@ -23,7 +23,7 @@ import * as React from 'react'; const extractSingleTouch = (nativeEvent: { +altKey?: ?boolean, // [macOS] +button?: ?number, // [macOS] - +changedTouches: $ReadOnlyArray, + +changedTouches: $ReadOnlyArray, +ctrlKey?: ?boolean, // [macOS] +force?: number, +identifier: number, @@ -35,7 +35,7 @@ const extractSingleTouch = (nativeEvent: { +shiftKey?: ?boolean, // [macOS] +target: ?number, +timestamp: number, - +touches: $ReadOnlyArray, + +touches: $ReadOnlyArray, }) => { const touches = nativeEvent.touches; const changedTouches = nativeEvent.changedTouches; @@ -403,7 +403,7 @@ const TouchableMixin = { touchableGetInitialState: function (): { touchable: { touchState: ?State, - responderID: ?PressEvent['currentTarget'], + responderID: ?GestureResponderEvent['currentTarget'], }, } { return { @@ -439,12 +439,12 @@ const TouchableMixin = { /** * Place as callback for a DOM element's `onResponderGrant` event. - * @param {SyntheticEvent} e Synthetic event from event system. + * @param {NativeSyntheticEvent} e Synthetic event from event system. * */ /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by * Flow's LTI update could not be added via codemod */ - touchableHandleResponderGrant: function (e: PressEvent) { + touchableHandleResponderGrant: function (e: GestureResponderEvent) { const dispatchID = e.currentTarget; // Since e is used in a callback invoked on another event loop // (as in setTimeout etc), we need to call e.persist() on the @@ -487,7 +487,7 @@ const TouchableMixin = { */ /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by * Flow's LTI update could not be added via codemod */ - touchableHandleResponderRelease: function (e: PressEvent) { + touchableHandleResponderRelease: function (e: GestureResponderEvent) { this.pressInLocation = null; this._receiveSignal(Signals.RESPONDER_RELEASE, e); }, @@ -497,7 +497,7 @@ const TouchableMixin = { */ /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by * Flow's LTI update could not be added via codemod */ - touchableHandleResponderTerminate: function (e: PressEvent) { + touchableHandleResponderTerminate: function (e: GestureResponderEvent) { this.pressInLocation = null; this._receiveSignal(Signals.RESPONDER_TERMINATED, e); }, @@ -507,7 +507,7 @@ const TouchableMixin = { */ /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by * Flow's LTI update could not be added via codemod */ - touchableHandleResponderMove: function (e: PressEvent) { + touchableHandleResponderMove: function (e: GestureResponderEvent) { // Measurement may not have returned yet. if (!this.state.touchable.positionOnActivate) { return; @@ -718,13 +718,17 @@ const TouchableMixin = { return; } this.state.touchable.positionOnActivate && + // $FlowFixMe[prop-missing] Position.release(this.state.touchable.positionOnActivate); this.state.touchable.dimensionsOnActivate && + // $FlowFixMe[prop-missing] BoundingDimensions.release(this.state.touchable.dimensionsOnActivate); + // $FlowFixMe[prop-missing] this.state.touchable.positionOnActivate = Position.getPooled( globalX, globalY, ); + // $FlowFixMe[prop-missing] this.state.touchable.dimensionsOnActivate = BoundingDimensions.getPooled( w, h, @@ -733,14 +737,14 @@ const TouchableMixin = { /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by * Flow's LTI update could not be added via codemod */ - _handleDelay: function (e: PressEvent) { + _handleDelay: function (e: GestureResponderEvent) { this.touchableDelayTimeout = null; this._receiveSignal(Signals.DELAY, e); }, /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by * Flow's LTI update could not be added via codemod */ - _handleLongDelay: function (e: PressEvent) { + _handleLongDelay: function (e: GestureResponderEvent) { this.longPressDelayTimeout = null; const curState = this.state.touchable.touchState; if ( @@ -761,7 +765,7 @@ const TouchableMixin = { */ /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by * Flow's LTI update could not be added via codemod */ - _receiveSignal: function (signal: Signal, e: PressEvent) { + _receiveSignal: function (signal: Signal, e: GestureResponderEvent) { const responderID = this.state.touchable.responderID; const curState = this.state.touchable.touchState; const nextState = Transitions[curState] && Transitions[curState][signal]; @@ -816,7 +820,7 @@ const TouchableMixin = { /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by * Flow's LTI update could not be added via codemod */ - _savePressInLocation: function (e: PressEvent) { + _savePressInLocation: function (e: GestureResponderEvent) { const touch = extractSingleTouch(e.nativeEvent); const pageX = touch && touch.pageX; const pageY = touch && touch.pageY; @@ -853,7 +857,7 @@ const TouchableMixin = { curState: State, nextState: State, signal: Signal, - e: PressEvent, + e: GestureResponderEvent, ) { const curIsHighlight = this._isHighlight(curState); const newIsHighlight = this._isHighlight(nextState); @@ -912,14 +916,14 @@ const TouchableMixin = { /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by * Flow's LTI update could not be added via codemod */ - _startHighlight: function (e: PressEvent) { + _startHighlight: function (e: GestureResponderEvent) { this._savePressInLocation(e); this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(e); }, /* $FlowFixMe[missing-this-annot] The 'this' type annotation(s) required by * Flow's LTI update could not be added via codemod */ - _endHighlight: function (e: PressEvent) { + _endHighlight: function (e: GestureResponderEvent) { if (this.touchableHandleActivePressOut) { if ( this.touchableGetPressOutDelayMS && diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableBounce.js b/packages/react-native/Libraries/Components/Touchable/TouchableBounce.js index 854c11b6a01b11..8ca2a62a30c216 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableBounce.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableBounce.js @@ -19,7 +19,7 @@ import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import Platform from '../../Utilities/Platform'; import * as React from 'react'; -type Props = $ReadOnly<{| +type Props = $ReadOnly<{ ...React.ElementConfig, onPressAnimationComplete?: ?() => void, @@ -28,13 +28,13 @@ type Props = $ReadOnly<{| releaseVelocity?: ?number, style?: ?ViewStyleProp, - hostRef: React.Ref, -|}>; + hostRef: React.RefSetter>, +}>; -type State = $ReadOnly<{| +type State = $ReadOnly<{ pressability: Pressability, scale: Animated.Value, -|}>; +}>; class TouchableBounce extends React.Component { state: State = { @@ -209,6 +209,7 @@ class TouchableBounce extends React.Component { onBlur={this.props.onBlur} draggedTypes={this.props.draggedTypes} // macOS] + // $FlowFixMe[prop-missing] ref={this.props.hostRef} {...eventHandlersWithoutBlurAndFocus}> {this.props.children} @@ -233,6 +234,9 @@ class TouchableBounce extends React.Component { } } -module.exports = (React.forwardRef((props, hostRef) => ( +export default (React.forwardRef((props, hostRef) => ( -)): React.AbstractComponent<$ReadOnly<$Diff>>); +)): component( + ref: React.RefSetter, + ...props: $ReadOnly<$Diff> +)); diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js index d89ba6b3076fb2..023a408c0f1518 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js @@ -20,19 +20,19 @@ import StyleSheet, {type ViewStyleProp} from '../../StyleSheet/StyleSheet'; import Platform from '../../Utilities/Platform'; import * as React from 'react'; -type AndroidProps = $ReadOnly<{| +type AndroidProps = $ReadOnly<{ nextFocusDown?: ?number, nextFocusForward?: ?number, nextFocusLeft?: ?number, nextFocusRight?: ?number, nextFocusUp?: ?number, -|}>; +}>; -type IOSProps = $ReadOnly<{| +type IOSProps = $ReadOnly<{ hasTVPreferredFocus?: ?boolean, -|}>; +}>; -type Props = $ReadOnly<{| +type Props = $ReadOnly<{ ...React.ElementConfig, ...AndroidProps, ...IOSProps, @@ -44,18 +44,18 @@ type Props = $ReadOnly<{| onHideUnderlay?: ?() => void, testOnly_pressed?: ?boolean, - hostRef: React.Ref, -|}>; + hostRef: React.RefSetter>, +}>; -type ExtraStyles = $ReadOnly<{| +type ExtraStyles = $ReadOnly<{ child: ViewStyleProp, underlay: ViewStyleProp, -|}>; +}>; -type State = $ReadOnly<{| +type State = $ReadOnly<{ pressability: Pressability, extraStyles: ?ExtraStyles, -|}>; +}>; /** * A wrapper for making views respond properly to touches. @@ -400,13 +400,13 @@ class TouchableHighlight extends React.Component { } } -const Touchable: React.AbstractComponent< - $ReadOnly<$Diff|}>>, - React.ElementRef, -> = React.forwardRef((props, hostRef) => ( +const Touchable: component( + ref: React.RefSetter>, + ...props: $ReadOnly<$Diff> +) = React.forwardRef((props, hostRef) => ( )); Touchable.displayName = 'TouchableHighlight'; -module.exports = Touchable; +export default Touchable; diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.js b/packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.js index 0fb5f86702e417..73cd082bc7df7a 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableNativeFeedback.js @@ -8,7 +8,7 @@ * @format */ -import type {PressEvent} from '../../Types/CoreEventTypes'; +import type {GestureResponderEvent} from '../../Types/CoreEventTypes'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; import View from '../../Components/View/View'; @@ -23,7 +23,7 @@ import {Commands} from '../View/ViewNativeComponent'; import invariant from 'invariant'; import * as React from 'react'; -type Props = $ReadOnly<{| +type Props = $ReadOnly<{ ...React.ElementConfig, /** @@ -33,19 +33,19 @@ type Props = $ReadOnly<{| * methods to generate that dictionary. */ background?: ?( - | $ReadOnly<{| + | $ReadOnly<{ type: 'ThemeAttrAndroid', attribute: | 'selectableItemBackground' | 'selectableItemBackgroundBorderless', rippleRadius: ?number, - |}> - | $ReadOnly<{| + }> + | $ReadOnly<{ type: 'RippleAndroid', color: ?number, borderless: boolean, rippleRadius: ?number, - |}> + }> ), /** @@ -89,22 +89,22 @@ type Props = $ReadOnly<{| * versions, this will fallback to background. */ useForeground?: ?boolean, -|}>; +}>; -type State = $ReadOnly<{| +type State = $ReadOnly<{ pressability: Pressability, -|}>; +}>; class TouchableNativeFeedback extends React.Component { /** * Creates a value for the `background` prop that uses the Android theme's * default background for selectable elements. */ - static SelectableBackground: (rippleRadius: ?number) => $ReadOnly<{| + static SelectableBackground: (rippleRadius: ?number) => $ReadOnly<{ attribute: 'selectableItemBackground', type: 'ThemeAttrAndroid', rippleRadius: ?number, - |}> = (rippleRadius: ?number) => ({ + }> = (rippleRadius: ?number) => ({ type: 'ThemeAttrAndroid', attribute: 'selectableItemBackground', rippleRadius, @@ -114,11 +114,11 @@ class TouchableNativeFeedback extends React.Component { * Creates a value for the `background` prop that uses the Android theme's * default background for borderless selectable elements. Requires API 21+. */ - static SelectableBackgroundBorderless: (rippleRadius: ?number) => $ReadOnly<{| + static SelectableBackgroundBorderless: (rippleRadius: ?number) => $ReadOnly<{ attribute: 'selectableItemBackgroundBorderless', type: 'ThemeAttrAndroid', rippleRadius: ?number, - |}> = (rippleRadius: ?number) => ({ + }> = (rippleRadius: ?number) => ({ type: 'ThemeAttrAndroid', attribute: 'selectableItemBackgroundBorderless', rippleRadius, @@ -133,12 +133,12 @@ class TouchableNativeFeedback extends React.Component { color: string, borderless: boolean, rippleRadius: ?number, - ) => $ReadOnly<{| + ) => $ReadOnly<{ borderless: boolean, color: ?number, rippleRadius: ?number, type: 'RippleAndroid', - |}> = (color: string, borderless: boolean, rippleRadius: ?number) => { + }> = (color: string, borderless: boolean, rippleRadius: ?number) => { const processedColor = processColor(color); invariant( processedColor == null || typeof processedColor === 'number', @@ -220,7 +220,7 @@ class TouchableNativeFeedback extends React.Component { } } - _dispatchHotspotUpdate(event: PressEvent): void { + _dispatchHotspotUpdate(event: GestureResponderEvent): void { if (Platform.OS === 'android') { const {locationX, locationY} = event.nativeEvent; const hostComponentRef = findHostInstance_DEPRECATED(this); @@ -363,4 +363,4 @@ const getBackgroundProp = TouchableNativeFeedback.displayName = 'TouchableNativeFeedback'; -module.exports = TouchableNativeFeedback; +export default TouchableNativeFeedback; diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js index 4f85be8d15ae48..528770e92adc1a 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js @@ -21,29 +21,29 @@ import flattenStyle from '../../StyleSheet/flattenStyle'; import Platform from '../../Utilities/Platform'; import * as React from 'react'; -type TVProps = $ReadOnly<{| +type TVProps = $ReadOnly<{ hasTVPreferredFocus?: ?boolean, nextFocusDown?: ?number, nextFocusForward?: ?number, nextFocusLeft?: ?number, nextFocusRight?: ?number, nextFocusUp?: ?number, -|}>; +}>; -type Props = $ReadOnly<{| +type Props = $ReadOnly<{ ...React.ElementConfig, ...TVProps, activeOpacity?: ?number, style?: ?ViewStyleProp, - hostRef?: ?React.Ref, -|}>; + hostRef?: ?React.RefSetter>, +}>; -type State = $ReadOnly<{| +type State = $ReadOnly<{ anim: Animated.Value, pressability: Pressability, -|}>; +}>; /** * A wrapper for making views respond properly to touches. @@ -317,6 +317,7 @@ class TouchableOpacity extends React.Component { onBlur={this.props.onBlur} draggedTypes={this.props.draggedTypes} // macOS] + // $FlowFixMe[prop-missing] ref={this.props.hostRef} {...eventHandlersWithoutBlurAndFocus}> {this.props.children} @@ -352,13 +353,13 @@ class TouchableOpacity extends React.Component { } } -const Touchable: React.AbstractComponent< - Props, - React.ElementRef, -> = React.forwardRef((props, ref) => ( +const Touchable: component( + ref: React.RefSetter>, + ...props: Props +) = React.forwardRef((props, ref) => ( )); Touchable.displayName = 'TouchableOpacity'; -module.exports = Touchable; +export default Touchable; diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js index bca323667515ae..320b666976512b 100755 --- a/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -19,9 +19,9 @@ import type {EdgeInsetsOrSizeProp} from '../../StyleSheet/EdgeInsetsPropType'; import type { BlurEvent, FocusEvent, - LayoutEvent, + LayoutChangeEvent, MouseEvent, // [macOS] - PressEvent, + GestureResponderEvent, } from '../../Types/CoreEventTypes'; // [macOS import type {DraggedTypesType} from '../View/DraggedType'; // [macOS] @@ -32,7 +32,7 @@ import usePressability from '../../Pressability/usePressability'; import * as React from 'react'; import {useMemo} from 'react'; -type Props = $ReadOnly<{| +type Props = $ReadOnly<{ accessibilityActions?: ?$ReadOnlyArray, accessibilityElementsHidden?: ?boolean, accessibilityHint?: ?Stringish, @@ -76,11 +76,11 @@ type Props = $ReadOnly<{| onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed, onBlur?: ?(event: BlurEvent) => void, // [macOS] onFocus?: ?(event: FocusEvent) => void, // [macOS] - onLayout?: ?(event: LayoutEvent) => mixed, - onLongPress?: ?(event: PressEvent) => mixed, - onPress?: ?(event: PressEvent) => mixed, - onPressIn?: ?(event: PressEvent) => mixed, - onPressOut?: ?(event: PressEvent) => mixed, + onLayout?: ?(event: LayoutChangeEvent) => mixed, + onLongPress?: ?(event: GestureResponderEvent) => mixed, + onPress?: ?(event: GestureResponderEvent) => mixed, + onPressIn?: ?(event: GestureResponderEvent) => mixed, + onPressOut?: ?(event: GestureResponderEvent) => mixed, // [macOS acceptsFirstMouse?: ?boolean, enableFocusRing?: ?boolean, @@ -96,7 +96,7 @@ type Props = $ReadOnly<{| rejectResponderTermination?: ?boolean, testID?: ?string, touchSoundDisabled?: ?boolean, -|}>; +}>; const PASSTHROUGH_PROPS = [ 'accessibilityActions', @@ -133,7 +133,7 @@ const PASSTHROUGH_PROPS = [ 'testID', ]; -module.exports = function TouchableWithoutFeedback(props: Props): React.Node { +export default function TouchableWithoutFeedback(props: Props): React.Node { const { disabled, rejectResponderTermination, @@ -218,8 +218,7 @@ module.exports = function TouchableWithoutFeedback(props: Props): React.Node { // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before // adopting `Pressability`, so preserve that behavior. - const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = - eventHandlers || {}; + const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = eventHandlers; const elementProps: {[string]: mixed, ...} = { ...eventHandlersWithoutBlurAndFocus, @@ -265,4 +264,4 @@ module.exports = function TouchableWithoutFeedback(props: Props): React.Node { // $FlowFixMe[incompatible-call] return React.cloneElement(element, elementProps, ...children); -}; +} diff --git a/packages/react-native/Libraries/Components/View/DraggedType.js b/packages/react-native/Libraries/Components/View/DraggedType.js index dc02cf4cb6c02d..ce6d21ff0a7353 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) 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. @@ -16,6 +16,4 @@ export type DraggedType = 'fileUrl'; export type DraggedTypesType = DraggedType | $ReadOnlyArray; -module.exports = { - DraggedTypes: ['fileUrl'], -}; +export const DraggedTypes = ['fileUrl']; diff --git a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js index c186586d9a0983..b57e482dea0167 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js @@ -36,6 +36,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { borderRightWidth: true, borderStartWidth: true, borderTopWidth: true, + boxSizing: true, columnGap: true, borderWidth: true, bottom: true, @@ -125,7 +126,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { /** * MixBlendMode */ - experimental_mixBlendMode: true, + mixBlendMode: true, /** * Isolation @@ -174,6 +175,10 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = { borderTopStartRadius: true, cursor: true, opacity: true, + outlineColor: colorAttributes, + outlineOffset: true, + outlineStyle: true, + outlineWidth: true, pointerEvents: true, /** diff --git a/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js index 7c33b48c95180c..fecdc5bf207569 100644 --- a/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/packages/react-native/Libraries/Components/View/ReactNativeViewAttributes.js @@ -73,4 +73,4 @@ const ReactNativeViewAttributes = { RCTView: RCTView, }; -module.exports = ReactNativeViewAttributes; +export default ReactNativeViewAttributes; diff --git a/packages/react-native/Libraries/Components/View/View.js b/packages/react-native/Libraries/Components/View/View.js index ff32a275841bb0..31c98a501c426c 100644 --- a/packages/react-native/Libraries/Components/View/View.js +++ b/packages/react-native/Libraries/Components/View/View.js @@ -23,10 +23,10 @@ export type Props = ViewProps; * * @see https://reactnative.dev/docs/view */ -const View: React.AbstractComponent< - ViewProps, - React.ElementRef, -> = React.forwardRef( +const View: component( + ref: React.RefSetter>, + ...props: ViewProps +) = React.forwardRef( ( { accessibilityElementsHidden, diff --git a/packages/react-native/Libraries/Components/View/ViewAccessibility.js b/packages/react-native/Libraries/Components/View/ViewAccessibility.js index 3809dc15230a92..5725159e750d98 100644 --- a/packages/react-native/Libraries/Components/View/ViewAccessibility.js +++ b/packages/react-native/Libraries/Components/View/ViewAccessibility.js @@ -10,7 +10,7 @@ 'use strict'; -import type {SyntheticEvent} from '../../Types/CoreEventTypes'; +import type {NativeSyntheticEvent} from '../../Types/CoreEventTypes'; // This must be kept in sync with the AccessibilityRolesMask in RCTViewManager.m export type AccessibilityRole = @@ -132,7 +132,7 @@ export type AccessibilityActionInfo = $ReadOnly<{ }>; // The info included in the event sent to onAccessibilityAction -export type AccessibilityActionEvent = SyntheticEvent< +export type AccessibilityActionEvent = NativeSyntheticEvent< $ReadOnly<{actionName: string, ...}>, >; @@ -145,7 +145,7 @@ export type AccessibilityState = { ... }; -export type AccessibilityValue = $ReadOnly<{| +export type AccessibilityValue = $ReadOnly<{ /** * The minimum value of this component's range. (should be an integer) */ @@ -165,4 +165,4 @@ export type AccessibilityValue = $ReadOnly<{| * A textual description of this component's value. (will override minimum, current, and maximum if set) */ text?: Stringish, -|}>; +}>; diff --git a/packages/react-native/Libraries/Components/View/ViewNativeComponent.js b/packages/react-native/Libraries/Components/View/ViewNativeComponent.js index f1f5b1ecc9904a..b32b9065db73ec 100644 --- a/packages/react-native/Libraries/Components/View/ViewNativeComponent.js +++ b/packages/react-native/Libraries/Components/View/ViewNativeComponent.js @@ -10,113 +10,21 @@ import type { HostComponent, - PartialViewConfig, + HostInstance, } from '../../Renderer/shims/ReactNativeTypes'; import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry'; import codegenNativeCommands from '../../Utilities/codegenNativeCommands'; -import Platform from '../../Utilities/Platform'; import {type ViewProps as Props} from './ViewPropTypes'; -import * as React from 'react'; - -export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = - Platform.OS === 'android' - ? { - uiViewClassName: 'RCTView', - validAttributes: { - // ReactClippingViewManager @ReactProps - removeClippedSubviews: true, - - // ReactViewManager @ReactProps - accessible: true, - hasTVPreferredFocus: true, - nextFocusDown: true, - nextFocusForward: true, - nextFocusLeft: true, - nextFocusRight: true, - nextFocusUp: true, - - borderRadius: true, - borderTopLeftRadius: true, - borderTopRightRadius: true, - borderBottomRightRadius: true, - borderBottomLeftRadius: true, - borderTopStartRadius: true, - borderTopEndRadius: true, - borderBottomStartRadius: true, - borderBottomEndRadius: true, - borderEndEndRadius: true, - borderEndStartRadius: true, - borderStartEndRadius: true, - borderStartStartRadius: true, - borderStyle: true, - hitSlop: true, - pointerEvents: true, - nativeBackgroundAndroid: true, - nativeForegroundAndroid: true, - needsOffscreenAlphaCompositing: true, - - borderWidth: true, - borderLeftWidth: true, - borderRightWidth: true, - borderTopWidth: true, - borderBottomWidth: true, - borderStartWidth: true, - borderEndWidth: true, - - borderColor: { - process: require('../../StyleSheet/processColor').default, - }, - borderLeftColor: { - process: require('../../StyleSheet/processColor').default, - }, - borderRightColor: { - process: require('../../StyleSheet/processColor').default, - }, - borderTopColor: { - process: require('../../StyleSheet/processColor').default, - }, - borderBottomColor: { - process: require('../../StyleSheet/processColor').default, - }, - borderStartColor: { - process: require('../../StyleSheet/processColor').default, - }, - borderEndColor: { - process: require('../../StyleSheet/processColor').default, - }, - borderBlockColor: { - process: require('../../StyleSheet/processColor').default, - }, - borderBlockEndColor: { - process: require('../../StyleSheet/processColor').default, - }, - borderBlockStartColor: { - process: require('../../StyleSheet/processColor').default, - }, - focusable: true, - overflow: true, - backfaceVisibility: true, - experimental_layoutConformance: true, - }, - } - : { - uiViewClassName: 'RCTView', - }; const ViewNativeComponent: HostComponent = - NativeComponentRegistry.get('RCTView', () => __INTERNAL_VIEW_CONFIG); + NativeComponentRegistry.get('RCTView', () => ({ + uiViewClassName: 'RCTView', + })); interface NativeCommands { - +hotspotUpdate: ( - viewRef: React.ElementRef>, - x: number, - y: number, - ) => void; - +setPressed: ( - viewRef: React.ElementRef>, - pressed: boolean, - ) => void; + +hotspotUpdate: (viewRef: HostInstance, x: number, y: number) => void; + +setPressed: (viewRef: HostInstance, pressed: boolean) => void; } export const Commands: NativeCommands = codegenNativeCommands({ diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js index b10219ceb1884d..54465dd2578279 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -18,11 +18,11 @@ import type { // [macOS] HandledKeyEvent, KeyEvent, - Layout, - LayoutEvent, + LayoutRectangle, + LayoutChangeEvent, MouseEvent, PointerEvent, - PressEvent, + GestureResponderEvent, ScrollEvent, // [macOS] } from '../../Types/CoreEventTypes'; @@ -37,10 +37,10 @@ import type { } from './ViewAccessibility'; import type {Node} from 'react'; -export type ViewLayout = Layout; -export type ViewLayoutEvent = LayoutEvent; +export type ViewLayout = LayoutRectangle; +export type ViewLayoutEvent = LayoutChangeEvent; -type DirectEventProps = $ReadOnly<{| +type DirectEventProps = $ReadOnly<{ /** * When `accessible` is true, the system will try to invoke this function * when the user performs an accessibility custom action. @@ -81,7 +81,7 @@ type DirectEventProps = $ReadOnly<{| * * See https://reactnative.dev/docs/view#onlayout */ - onLayout?: ?(event: LayoutEvent) => mixed, + onLayout?: ?(event: LayoutChangeEvent) => mixed, /** * When `accessible` is `true`, the system will invoke this function when the @@ -98,9 +98,9 @@ type DirectEventProps = $ReadOnly<{| * See https://reactnative.dev/docs/view#onaccessibilityescape */ onAccessibilityEscape?: ?() => mixed, -|}>; +}>; -export type KeyboardEventProps = $ReadOnly<{| +export type KeyboardEventProps = $ReadOnly<{ /** * Called after a key down event is detected. */ @@ -124,16 +124,16 @@ export type KeyboardEventProps = $ReadOnly<{| * @platform macos */ keyUpEvents?: ?Array, -|}>; +}>; // macOS] -type MouseEventProps = $ReadOnly<{| +type MouseEventProps = $ReadOnly<{ onMouseEnter?: ?(event: MouseEvent) => void, onMouseLeave?: ?(event: MouseEvent) => void, -|}>; +}>; // Experimental/Work in Progress Pointer Event Callbacks (not yet ready for use) -type PointerEventProps = $ReadOnly<{| +type PointerEventProps = $ReadOnly<{ onClick?: ?(event: PointerEvent) => void, onClickCapture?: ?(event: PointerEvent) => void, onPointerEnter?: ?(event: PointerEvent) => void, @@ -156,32 +156,32 @@ type PointerEventProps = $ReadOnly<{| onGotPointerCaptureCapture?: ?(e: PointerEvent) => void, onLostPointerCapture?: ?(e: PointerEvent) => void, onLostPointerCaptureCapture?: ?(e: PointerEvent) => void, -|}>; +}>; -type FocusEventProps = $ReadOnly<{| +type FocusEventProps = $ReadOnly<{ onBlur?: ?(event: BlurEvent) => void, onBlurCapture?: ?(event: BlurEvent) => void, onFocus?: ?(event: FocusEvent) => void, onFocusCapture?: ?(event: FocusEvent) => void, -|}>; - -type TouchEventProps = $ReadOnly<{| - onTouchCancel?: ?(e: PressEvent) => void, - onTouchCancelCapture?: ?(e: PressEvent) => void, - onTouchEnd?: ?(e: PressEvent) => void, - onTouchEndCapture?: ?(e: PressEvent) => void, - onTouchMove?: ?(e: PressEvent) => void, - onTouchMoveCapture?: ?(e: PressEvent) => void, - onTouchStart?: ?(e: PressEvent) => void, - onTouchStartCapture?: ?(e: PressEvent) => void, -|}>; +}>; + +type TouchEventProps = $ReadOnly<{ + onTouchCancel?: ?(e: GestureResponderEvent) => void, + onTouchCancelCapture?: ?(e: GestureResponderEvent) => void, + onTouchEnd?: ?(e: GestureResponderEvent) => void, + onTouchEndCapture?: ?(e: GestureResponderEvent) => void, + onTouchMove?: ?(e: GestureResponderEvent) => void, + onTouchMoveCapture?: ?(e: GestureResponderEvent) => void, + onTouchStart?: ?(e: GestureResponderEvent) => void, + onTouchStartCapture?: ?(e: GestureResponderEvent) => void, +}>; /** * For most touch interactions, you'll simply want to wrap your component in * `TouchableHighlight` or `TouchableOpacity`. Check out `Touchable.js`, * `ScrollResponder.js` and `ResponderEventPlugin.js` for more discussion. */ -type GestureResponderEventProps = $ReadOnly<{| +type GestureResponderEventProps = $ReadOnly<{ /** * Does this view want to "claim" touch responsiveness? This is called for * every touch move on the `View` when it is not the responder. @@ -191,7 +191,7 @@ type GestureResponderEventProps = $ReadOnly<{| * * See https://reactnative.dev/docs/view#onmoveshouldsetresponder */ - onMoveShouldSetResponder?: ?(e: PressEvent) => boolean, + onMoveShouldSetResponder?: ?(e: GestureResponderEvent) => boolean, /** * If a parent `View` wants to prevent a child `View` from becoming responder @@ -202,7 +202,7 @@ type GestureResponderEventProps = $ReadOnly<{| * * See https://reactnative.dev/docs/view#onMoveShouldsetrespondercapture */ - onMoveShouldSetResponderCapture?: ?(e: PressEvent) => boolean, + onMoveShouldSetResponderCapture?: ?(e: GestureResponderEvent) => boolean, /** * The View is now responding for touch events. This is the time to highlight @@ -216,7 +216,7 @@ type GestureResponderEventProps = $ReadOnly<{| * * See https://reactnative.dev/docs/view#onrespondergrant */ - onResponderGrant?: ?(e: PressEvent) => void | boolean, + onResponderGrant?: ?(e: GestureResponderEvent) => void | boolean, /** * The user is moving their finger. @@ -226,7 +226,7 @@ type GestureResponderEventProps = $ReadOnly<{| * * See https://reactnative.dev/docs/view#onrespondermove */ - onResponderMove?: ?(e: PressEvent) => void, + onResponderMove?: ?(e: GestureResponderEvent) => void, /** * Another responder is already active and will not release it to that `View` @@ -237,7 +237,7 @@ type GestureResponderEventProps = $ReadOnly<{| * * See https://reactnative.dev/docs/view#onresponderreject */ - onResponderReject?: ?(e: PressEvent) => void, + onResponderReject?: ?(e: GestureResponderEvent) => void, /** * Fired at the end of the touch. @@ -247,10 +247,10 @@ type GestureResponderEventProps = $ReadOnly<{| * * See https://reactnative.dev/docs/view#onresponderrelease */ - onResponderRelease?: ?(e: PressEvent) => void, + onResponderRelease?: ?(e: GestureResponderEvent) => void, - onResponderStart?: ?(e: PressEvent) => void, - onResponderEnd?: ?(e: PressEvent) => void, + onResponderStart?: ?(e: GestureResponderEvent) => void, + onResponderEnd?: ?(e: GestureResponderEvent) => void, /** * The responder has been taken from the `View`. Might be taken by other @@ -263,7 +263,7 @@ type GestureResponderEventProps = $ReadOnly<{| * * See https://reactnative.dev/docs/view#onresponderterminate */ - onResponderTerminate?: ?(e: PressEvent) => void, + onResponderTerminate?: ?(e: GestureResponderEvent) => void, /** * Some other `View` wants to become responder and is asking this `View` to @@ -274,7 +274,7 @@ type GestureResponderEventProps = $ReadOnly<{| * * See https://reactnative.dev/docs/view#onresponderterminationrequest */ - onResponderTerminationRequest?: ?(e: PressEvent) => boolean, + onResponderTerminationRequest?: ?(e: GestureResponderEvent) => boolean, /** * Does this view want to become responder on the start of a touch? @@ -284,7 +284,7 @@ type GestureResponderEventProps = $ReadOnly<{| * * See https://reactnative.dev/docs/view#onstartshouldsetresponder */ - onStartShouldSetResponder?: ?(e: PressEvent) => boolean, + onStartShouldSetResponder?: ?(e: GestureResponderEvent) => boolean, /** * If a parent `View` wants to prevent a child `View` from becoming responder @@ -295,24 +295,24 @@ type GestureResponderEventProps = $ReadOnly<{| * * See https://reactnative.dev/docs/view#onstartshouldsetrespondercapture */ - onStartShouldSetResponderCapture?: ?(e: PressEvent) => boolean, -|}>; + onStartShouldSetResponderCapture?: ?(e: GestureResponderEvent) => boolean, +}>; -type AndroidDrawableThemeAttr = $ReadOnly<{| +type AndroidDrawableThemeAttr = $ReadOnly<{ type: 'ThemeAttrAndroid', attribute: string, -|}>; +}>; -type AndroidDrawableRipple = $ReadOnly<{| +type AndroidDrawableRipple = $ReadOnly<{ type: 'RippleAndroid', color?: ?number, borderless?: ?boolean, rippleRadius?: ?number, -|}>; +}>; type AndroidDrawable = AndroidDrawableThemeAttr | AndroidDrawableRipple; -type AndroidViewProps = $ReadOnly<{| +type AndroidViewProps = $ReadOnly<{ /** * Identifies the element that labels the element it is applied to. When the assistive technology focuses on the component with this props, * the text is read aloud. The value should should match the nativeID of the related element. @@ -440,10 +440,10 @@ type AndroidViewProps = $ReadOnly<{| * * @platform android */ - onClick?: ?(event: PressEvent) => mixed, -|}>; + onClick?: ?(event: GestureResponderEvent) => mixed, +}>; -type IOSViewProps = $ReadOnly<{| +type IOSViewProps = $ReadOnly<{ /** * Prevents view from being inverted if set to true and color inversion is turned on. * @@ -512,10 +512,10 @@ type IOSViewProps = $ReadOnly<{| * See https://reactnative.dev/docs/view#shouldrasterizeios */ shouldRasterizeIOS?: ?boolean, -|}>; +}>; // [macOS -type MacOSViewProps = $ReadOnly<{| +type MacOSViewProps = $ReadOnly<{ /** * Fired when a file is dragged into the view via the mouse. * @@ -594,10 +594,10 @@ type MacOSViewProps = $ReadOnly<{| * @platform macos */ inverted?: ?boolean, -|}>; +}>; // macOS] -export type ViewProps = $ReadOnly<{| +export type ViewProps = $ReadOnly<{ ...DirectEventProps, ...GestureResponderEventProps, ...MouseEventProps, @@ -699,9 +699,6 @@ export type ViewProps = $ReadOnly<{| * optimization. Set this property to `false` to disable this optimization and * ensure that this `View` exists in the native view hierarchy. * - * @platform android - * In Fabric, this prop is used in ios as well. - * * See https://reactnative.dev/docs/view#collapsable */ collapsable?: ?boolean, @@ -713,15 +710,6 @@ export type ViewProps = $ReadOnly<{| */ collapsableChildren?: ?boolean, - /** - * Contols whether this view, and its transitive children, are laid in a way - * consistent with web browsers ('strict'), or consistent with existing - * React Native code which may rely on incorrect behavior ('classic'). - * - * This prop only works when using Fabric. - */ - experimental_layoutConformance?: ?('strict' | 'classic'), - /** * Used to locate this view from native classes. Has precedence over `nativeID` prop. * @@ -788,4 +776,4 @@ export type ViewProps = $ReadOnly<{| * See https://reactnative.dev/docs/view#removeclippedsubviews */ removeClippedSubviews?: ?boolean, -|}>; +}>; diff --git a/packages/react-native/Libraries/Core/ExceptionsManager.js b/packages/react-native/Libraries/Core/ExceptionsManager.js index eac2be19b981f1..ecb2171961e9c5 100644 --- a/packages/react-native/Libraries/Core/ExceptionsManager.js +++ b/packages/react-native/Libraries/Core/ExceptionsManager.js @@ -22,10 +22,11 @@ type ExceptionDecorator = ExceptionData => ExceptionData; let userExceptionDecorator: ?ExceptionDecorator; let inUserExceptionDecorator = false; -// This Symbol is used to decorate an ExtendedError with extra data in select usecases. +// This string is used to decorate an ExtendedError with extra data in select usecases. // Note that data passed using this method should be strictly contained, // as data that's not serializable/too large may cause issues with passing the error to the native code. -const decoratedExtraDataKey: symbol = Symbol('decoratedExtraDataKey'); +// TODO(T204185517): We should use a Symbol for this, but jsi through jsc doesn't support it yet. +const decoratedExtraDataKey = 'RN$ErrorExtraDataKey'; /** * Allows the app to add information to the exception report before it is sent @@ -120,6 +121,12 @@ function reportException( const NativeExceptionsManager = require('./NativeExceptionsManager').default; if (NativeExceptionsManager) { + if (isFatal) { + if (global.RN$hasHandledFatalException?.()) { + return; + } + global.RN$notifyOfFatalException?.(); + } NativeExceptionsManager.reportException(data); } } @@ -140,24 +147,31 @@ let inExceptionHandler = false; * Logs exceptions to the (native) console and displays them */ function handleException(e: mixed, isFatal: boolean) { - let error: Error; - if (e instanceof Error) { - error = e; - } else { - // Workaround for reporting errors caused by `throw 'some string'` - // Unfortunately there is no way to figure out the stacktrace in this - // case, so if you ended up here trying to trace an error, look for - // `throw ''` somewhere in your codebase. - error = new SyntheticError(e); - } - try { - inExceptionHandler = true; - /* $FlowFixMe[class-object-subtyping] added when improving typing for this - * parameters */ - // $FlowFixMe[incompatible-call] - reportException(error, isFatal, /*reportToConsole*/ true); - } finally { - inExceptionHandler = false; + // TODO(T196834299): We should really use a c++ turbomodule for this + const reportToConsole = true; + if ( + !global.RN$handleException || + !global.RN$handleException(e, isFatal, reportToConsole) + ) { + let error: Error; + if (e instanceof Error) { + error = e; + } else { + // Workaround for reporting errors caused by `throw 'some string'` + // Unfortunately there is no way to figure out the stacktrace in this + // case, so if you ended up here trying to trace an error, look for + // `throw ''` somewhere in your codebase. + error = new SyntheticError(e); + } + try { + inExceptionHandler = true; + /* $FlowFixMe[class-object-subtyping] added when improving typing for this + * parameters */ + // $FlowFixMe[incompatible-call] + reportException(error, isFatal, reportToConsole); + } finally { + inExceptionHandler = false; + } } } @@ -169,7 +183,7 @@ function reactConsoleErrorHandler(...args) { if (!console.reportErrorsAsExceptions) { return; } - if (inExceptionHandler) { + if (inExceptionHandler || global.RN$inExceptionHandler?.()) { // The fundamental trick here is that are multiple entry point to logging errors: // (see D19743075 for more background) // @@ -223,14 +237,21 @@ function reactConsoleErrorHandler(...args) { error.name = 'console.error'; } - reportException( - /* $FlowFixMe[class-object-subtyping] added when improving typing for this - * parameters */ - // $FlowFixMe[incompatible-call] - error, - false, // isFatal - false, // reportToConsole - ); + const isFatal = false; + const reportToConsole = false; + if ( + !global.RN$handleException || + !global.RN$handleException(error, isFatal, reportToConsole) + ) { + reportException( + /* $FlowFixMe[class-object-subtyping] added when improving typing for this + * parameters */ + // $FlowFixMe[incompatible-call] + error, + isFatal, + reportToConsole, + ); + } } /** diff --git a/packages/react-native/Libraries/Core/setUpBatchedBridge.js b/packages/react-native/Libraries/Core/setUpBatchedBridge.js index abac069677bc15..d2768a32eeac60 100644 --- a/packages/react-native/Libraries/Core/setUpBatchedBridge.js +++ b/packages/react-native/Libraries/Core/setUpBatchedBridge.js @@ -10,36 +10,12 @@ 'use strict'; -let registerModule; -if (global.RN$Bridgeless === true && global.RN$registerCallableModule) { - registerModule = global.RN$registerCallableModule; -} else { - const BatchedBridge = require('../BatchedBridge/BatchedBridge'); - registerModule = ( - moduleName: - | $TEMPORARY$string<'GlobalPerformanceLogger'> - | $TEMPORARY$string<'HMRClient'> - | $TEMPORARY$string<'HeapCapture'> - | $TEMPORARY$string<'JSTimers'> - | $TEMPORARY$string<'RCTDeviceEventEmitter'> - | $TEMPORARY$string<'RCTLog'> - | $TEMPORARY$string<'RCTNativeAppEventEmitter'> - | $TEMPORARY$string<'SamplingProfiler'> - | $TEMPORARY$string<'Systrace'>, - /* $FlowFixMe[missing-local-annot] The type annotation(s) required by - * Flow's LTI update could not be added via codemod */ - factory, - ) => BatchedBridge.registerLazyCallableModule(moduleName, factory); -} +import registerModule from './registerCallableModule'; registerModule('Systrace', () => require('../Performance/Systrace')); if (!(global.RN$Bridgeless === true)) { registerModule('JSTimers', () => require('./Timers/JSTimers')); } -registerModule('HeapCapture', () => require('../HeapCapture/HeapCapture')); -registerModule('SamplingProfiler', () => - require('../Performance/SamplingProfiler'), -); registerModule('RCTLog', () => require('../Utilities/RCTLog')); registerModule( 'RCTDeviceEventEmitter', diff --git a/packages/react-native/Libraries/Core/setUpReactDevTools.js b/packages/react-native/Libraries/Core/setUpReactDevTools.js index c70279ba27fc21..56c0228f834ec3 100644 --- a/packages/react-native/Libraries/Core/setUpReactDevTools.js +++ b/packages/react-native/Libraries/Core/setUpReactDevTools.js @@ -10,14 +10,44 @@ 'use strict'; -import type {Domain} from '../../src/private/fusebox/setUpFuseboxReactDevToolsDispatcher'; +import type {Domain} from '../../src/private/debugging/setUpFuseboxReactDevToolsDispatcher'; +import type {Spec as NativeReactDevToolsRuntimeSettingsModuleSpec} from '../../src/private/fusebox/specs/NativeReactDevToolsRuntimeSettingsModule'; if (__DEV__) { // Register dispatcher on global, which can be used later by Chrome DevTools frontend - require('../../src/private/fusebox/setUpFuseboxReactDevToolsDispatcher'); + require('../../src/private/debugging/setUpFuseboxReactDevToolsDispatcher'); + const { + initialize, + connectToDevTools, + connectWithCustomMessagingProtocol, + } = require('react-devtools-core'); + + const reactDevToolsSettingsManager = require('../../src/private/debugging/ReactDevToolsSettingsManager'); + const serializedHookSettings = + reactDevToolsSettingsManager.getGlobalHookSettings(); + const maybeReactDevToolsRuntimeSettingsModuleModule = + require('../../src/private/fusebox/specs/NativeReactDevToolsRuntimeSettingsModule').default; + + let hookSettings = null; + if (serializedHookSettings != null) { + try { + const parsedSettings = JSON.parse(serializedHookSettings); + hookSettings = parsedSettings; + } catch { + console.error( + 'Failed to parse persisted React DevTools hook settings. React DevTools will be initialized with default settings.', + ); + } + } + + const { + isProfiling: shouldStartProfilingNow, + profilingSettings: initialProfilingSettings, + } = readReloadAndProfileConfig(maybeReactDevToolsRuntimeSettingsModuleModule); // Install hook before React is loaded. - const reactDevTools = require('react-devtools-core'); + initialize(hookSettings, shouldStartProfilingNow, initialProfilingSettings); + // This should be defined in DEV, otherwise error is expected. const fuseboxReactDevToolsDispatcher = global.__FUSEBOX_REACT_DEVTOOLS_DISPATCHER__; @@ -25,9 +55,14 @@ if (__DEV__) { fuseboxReactDevToolsDispatcher.BINDING_NAME; const ReactNativeStyleAttributes = require('../Components/View/ReactNativeStyleAttributes'); - const devToolsSettingsManager = require('../DevToolsSettings/DevToolsSettingsManager'); const resolveRNStyle = require('../StyleSheet/flattenStyle'); + function handleReactDevToolsSettingsUpdate(settings: Object) { + reactDevToolsSettingsManager.setGlobalHookSettings( + JSON.stringify(settings), + ); + } + let disconnect = null; function disconnectBackendFromReactDevToolsInFuseboxIfNeeded() { if (disconnect != null) { @@ -37,7 +72,15 @@ if (__DEV__) { } function connectToReactDevToolsInFusebox(domain: Domain) { - disconnect = reactDevTools.connectWithCustomMessagingProtocol({ + const { + isReloadAndProfileSupported, + isProfiling, + onReloadAndProfile, + onReloadAndProfileFlagsReset, + } = readReloadAndProfileConfig( + maybeReactDevToolsRuntimeSettingsModuleModule, + ); + disconnect = connectWithCustomMessagingProtocol({ onSubscribe: listener => { domain.onMessage.addEventListener(listener); }, @@ -47,9 +90,13 @@ if (__DEV__) { onMessage: (event, payload) => { domain.sendMessage({event, payload}); }, - settingsManager: devToolsSettingsManager, nativeStyleEditorValidAttributes: Object.keys(ReactNativeStyleAttributes), resolveRNStyle, + onSettingsUpdated: handleReactDevToolsSettingsUpdate, + isReloadAndProfileSupported, + isProfiling, + onReloadAndProfile, + onReloadAndProfileFlagsReset, }); } @@ -101,14 +148,26 @@ if (__DEV__) { isWebSocketOpen = true; }); - reactDevTools.connectToDevTools({ + const { + isReloadAndProfileSupported, + isProfiling, + onReloadAndProfile, + onReloadAndProfileFlagsReset, + } = readReloadAndProfileConfig( + maybeReactDevToolsRuntimeSettingsModuleModule, + ); + connectToDevTools({ isAppActive, resolveRNStyle, nativeStyleEditorValidAttributes: Object.keys( ReactNativeStyleAttributes, ), websocket: ws, - devToolsSettingsManager, + onSettingsUpdated: handleReactDevToolsSettingsUpdate, + isReloadAndProfileSupported, + isProfiling, + onReloadAndProfile, + onReloadAndProfileFlagsReset, }); } } @@ -140,3 +199,43 @@ if (__DEV__) { ); connectToWSBasedReactDevToolsFrontend(); // Try connecting once on load } + +function readReloadAndProfileConfig( + maybeModule: ?NativeReactDevToolsRuntimeSettingsModuleSpec, +) { + const isReloadAndProfileSupported = maybeModule != null; + const config = maybeModule?.getReloadAndProfileConfig(); + const isProfiling = config?.shouldReloadAndProfile === true; + const profilingSettings = { + recordChangeDescriptions: config?.recordChangeDescriptions === true, + recordTimeline: false, + }; + const onReloadAndProfile = (recordChangeDescriptions: boolean) => { + if (maybeModule == null) { + return; + } + + maybeModule.setReloadAndProfileConfig({ + shouldReloadAndProfile: true, + recordChangeDescriptions, + }); + }; + const onReloadAndProfileFlagsReset = () => { + if (maybeModule == null) { + return; + } + + maybeModule.setReloadAndProfileConfig({ + shouldReloadAndProfile: false, + recordChangeDescriptions: false, + }); + }; + + return { + isReloadAndProfileSupported, + isProfiling, + profilingSettings, + onReloadAndProfile, + onReloadAndProfileFlagsReset, + }; +} diff --git a/packages/react-native/Libraries/Core/setUpSegmentFetcher.js b/packages/react-native/Libraries/Core/setUpSegmentFetcher.js index 2a7115c1d45781..54d9e37a6a0c33 100644 --- a/packages/react-native/Libraries/Core/setUpSegmentFetcher.js +++ b/packages/react-native/Libraries/Core/setUpSegmentFetcher.js @@ -42,6 +42,7 @@ function __fetchSegment( const error = new Error(errorObject.message); (error: any).code = errorObject.code; // flowlint-line unclear-type: off callback(error); + return; } callback(null); diff --git a/packages/react-native/Libraries/Core/setUpTimers.js b/packages/react-native/Libraries/Core/setUpTimers.js index ac346f61b2b68e..7dc59d9e1fb094 100644 --- a/packages/react-native/Libraries/Core/setUpTimers.js +++ b/packages/react-native/Libraries/Core/setUpTimers.js @@ -21,6 +21,17 @@ if (__DEV__) { } } +const isEventLoopEnabled = (() => { + if (NativeReactNativeFeatureFlags == null) { + return false; + } + + return ( + ReactNativeFeatureFlags.enableBridgelessArchitecture() && + !ReactNativeFeatureFlags.disableEventLoopOnBridgeless() + ); +})(); + // In bridgeless mode, timers are host functions installed from cpp. if (global.RN$Bridgeless !== true) { /** @@ -29,14 +40,14 @@ if (global.RN$Bridgeless !== true) { */ const defineLazyTimer = ( name: - | $TEMPORARY$string<'cancelAnimationFrame'> - | $TEMPORARY$string<'cancelIdleCallback'> - | $TEMPORARY$string<'clearInterval'> - | $TEMPORARY$string<'clearTimeout'> - | $TEMPORARY$string<'requestAnimationFrame'> - | $TEMPORARY$string<'requestIdleCallback'> - | $TEMPORARY$string<'setInterval'> - | $TEMPORARY$string<'setTimeout'>, + | 'cancelAnimationFrame' + | 'cancelIdleCallback' + | 'clearInterval' + | 'clearTimeout' + | 'requestAnimationFrame' + | 'requestIdleCallback' + | 'setInterval' + | 'setTimeout', ) => { polyfillGlobal(name, () => require('./Timers/JSTimers')[name]); }; @@ -48,12 +59,7 @@ if (global.RN$Bridgeless !== true) { defineLazyTimer('cancelAnimationFrame'); defineLazyTimer('requestIdleCallback'); defineLazyTimer('cancelIdleCallback'); -} else if ( - // TODO remove this condition when bridgeless == modern scheduler everywhere. - NativeReactNativeFeatureFlags != null && - // eslint-disable-next-line react-hooks/rules-of-hooks -- false positive due to `use` prefix - ReactNativeFeatureFlags.useModernRuntimeScheduler() -) { +} else if (isEventLoopEnabled) { polyfillGlobal( 'requestIdleCallback', () => @@ -72,10 +78,7 @@ if (global.RN$Bridgeless !== true) { // We need to check if the native module is available before accessing the // feature flag, because otherwise the API would throw an error in the legacy // architecture in OSS, where the native module isn't available. -if ( - NativeReactNativeFeatureFlags != null && - ReactNativeFeatureFlags.enableMicrotasks() -) { +if (isEventLoopEnabled) { // This is the flag that tells React to use `queueMicrotask` to batch state // updates, instead of using the scheduler to schedule a regular task. // We use a global variable because we don't currently have any other diff --git a/packages/react-native/Libraries/Debugging/DebuggingOverlay.js b/packages/react-native/Libraries/Debugging/DebuggingOverlay.js index d4b7ea8d9438cb..3d0c52ae583c10 100644 --- a/packages/react-native/Libraries/Debugging/DebuggingOverlay.js +++ b/packages/react-native/Libraries/Debugging/DebuggingOverlay.js @@ -102,10 +102,9 @@ const styles = StyleSheet.create({ }, }); -const DebuggingOverlayWithForwardedRef: React.AbstractComponent< - {}, - DebuggingOverlayHandle, - React.Node, -> = React.forwardRef(DebuggingOverlay); +const DebuggingOverlayWithForwardedRef: component( + ref: React.RefSetter, + ...props: {} +) = React.forwardRef(DebuggingOverlay); export default DebuggingOverlayWithForwardedRef; diff --git a/packages/react-native/Libraries/EventEmitter/RCTEventEmitter.js b/packages/react-native/Libraries/EventEmitter/RCTEventEmitter.js index 27519d2d95595b..d191b20baaa500 100644 --- a/packages/react-native/Libraries/EventEmitter/RCTEventEmitter.js +++ b/packages/react-native/Libraries/EventEmitter/RCTEventEmitter.js @@ -10,15 +10,11 @@ 'use strict'; -const BatchedBridge = require('../BatchedBridge/BatchedBridge'); +import registerCallableModule from '../Core/registerCallableModule'; const RCTEventEmitter = { register(eventEmitter: any) { - if (global.RN$Bridgeless) { - global.RN$registerCallableModule('RCTEventEmitter', () => eventEmitter); - } else { - BatchedBridge.registerCallableModule('RCTEventEmitter', eventEmitter); - } + registerCallableModule('RCTEventEmitter', eventEmitter); }, }; diff --git a/packages/react-native/Libraries/Image/AssetSourceResolver.js b/packages/react-native/Libraries/Image/AssetSourceResolver.js index e279212b49e5e5..c37fbe1fceb5e3 100644 --- a/packages/react-native/Libraries/Image/AssetSourceResolver.js +++ b/packages/react-native/Libraries/Image/AssetSourceResolver.js @@ -53,6 +53,13 @@ function getAssetPathInDrawableFolder(asset: PackagerAsset): string { return drawableFolder + '/' + fileName + '.' + asset.type; } +/** + * Returns true if the asset can be loaded over the network. + */ +function assetSupportsNetworkLoads(asset: PackagerAsset): boolean { + return !(asset.type === 'xml' && Platform.OS === 'android'); +} + class AssetSourceResolver { serverUrl: ?string; // where the jsbundle is being run from @@ -67,7 +74,11 @@ class AssetSourceResolver { } isLoadedFromServer(): boolean { - return !!this.serverUrl; + return ( + this.serverUrl != null && + this.serverUrl !== '' && + assetSupportsNetworkLoads(this.asset) + ); } isLoadedFromFileSystem(): boolean { diff --git a/packages/react-native/Libraries/Image/Image.android.js b/packages/react-native/Libraries/Image/Image.android.js index 4fa0bdc087a1ce..08fbe9d1faca02 100644 --- a/packages/react-native/Libraries/Image/Image.android.js +++ b/packages/react-native/Libraries/Image/Image.android.js @@ -133,7 +133,6 @@ let BaseImage: AbstractImageAndroid = React.forwardRef( width: undefined, height: undefined, }; - const defaultSource = resolveAssetSource(props.defaultSource); const loadingIndicatorSource = resolveAssetSource( props.loadingIndicatorSource, ); @@ -166,11 +165,9 @@ let BaseImage: AbstractImageAndroid = React.forwardRef( sources = [source]; } - const {height, width, ...restProps} = props; - const {onLoadStart, onLoad, onLoadEnd, onError} = props; const nativeProps = { - ...restProps, + ...props, style, shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd || onError), // Both iOS and C++ sides expect to have "source" prop, whereas on Android it's "src" @@ -181,7 +178,6 @@ let BaseImage: AbstractImageAndroid = React.forwardRef( /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found * when making Flow check .android.js files. */ headers: (source?.[0]?.headers || source?.headers: ?{[string]: string}), - defaultSrc: defaultSource ? defaultSource.uri : null, loadingIndicatorSrc: loadingIndicatorSource ? loadingIndicatorSource.uri : null, diff --git a/packages/react-native/Libraries/Image/Image.ios.js b/packages/react-native/Libraries/Image/Image.ios.js index 5d843e9ec603cd..493cd73bb2c29d 100644 --- a/packages/react-native/Libraries/Image/Image.ios.js +++ b/packages/react-native/Libraries/Image/Image.ios.js @@ -146,9 +146,7 @@ let BaseImage: AbstractImageIOS = React.forwardRef((props, forwardedRef) => { 'aria-expanded': ariaExpanded, 'aria-selected': ariaSelected, accessibilityRole, // [macOS] - height, src, - width, ...restProps } = props; @@ -253,4 +251,4 @@ const styles = StyleSheet.create({ }, }); -module.exports = Image; +export default Image; diff --git a/packages/react-native/Libraries/Image/Image.macos.js b/packages/react-native/Libraries/Image/Image.macos.js index 631b1b1792d471..5d843e9ec603cd 100644 --- a/packages/react-native/Libraries/Image/Image.macos.js +++ b/packages/react-native/Libraries/Image/Image.macos.js @@ -1,15 +1,256 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * 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. * - * @flow + * @flow strict-local * @format */ -// [macOS] +import type {ImageStyleProp} from '../StyleSheet/StyleSheet'; +import type {RootTag} from '../Types/RootTagTypes'; +import type {AbstractImageIOS, ImageIOS} from './ImageTypes.flow'; +import type {ImageSize} from './NativeImageLoaderAndroid'; + +import {createRootTag} from '../ReactNative/RootTag'; +import flattenStyle from '../StyleSheet/flattenStyle'; +import StyleSheet from '../StyleSheet/StyleSheet'; +import ImageAnalyticsTagContext from './ImageAnalyticsTagContext'; +import { + unstable_getImageComponentDecorator, + useWrapRefWithImageAttachedCallbacks, +} from './ImageInjection'; +import {getImageSourcesFromImageProps} from './ImageSourceUtils'; +import {convertObjectFitToResizeMode} from './ImageUtils'; +import ImageViewNativeComponent from './ImageViewNativeComponent'; +import NativeImageLoaderIOS from './NativeImageLoaderIOS'; +import resolveAssetSource from './resolveAssetSource'; +import * as React from 'react'; + +function getSize( + uri: string, + success?: (width: number, height: number) => void, + failure?: (error: mixed) => void, +): void | Promise { + const promise = NativeImageLoaderIOS.getSize(uri).then(([width, height]) => ({ + width, + height, + })); + if (typeof success !== 'function') { + return promise; + } + promise + .then(sizes => success(sizes.width, sizes.height)) + .catch( + failure || + function () { + console.warn('Failed to get size for image: ' + uri); + }, + ); +} + +function getSizeWithHeaders( + uri: string, + headers: {[string]: string, ...}, + success?: (width: number, height: number) => void, + failure?: (error: mixed) => void, +): void | Promise { + const promise = NativeImageLoaderIOS.getSizeWithHeaders(uri, headers); + if (typeof success !== 'function') { + return promise; + } + promise + .then(sizes => success(sizes.width, sizes.height)) + .catch( + failure || + function () { + console.warn('Failed to get size for image: ' + uri); + }, + ); +} + +function prefetchWithMetadata( + url: string, + queryRootName: string, + rootTag?: ?RootTag, +): Promise { + if (NativeImageLoaderIOS.prefetchImageWithMetadata) { + // number params like rootTag cannot be nullable before TurboModules is available + return NativeImageLoaderIOS.prefetchImageWithMetadata( + url, + queryRootName, + // NOTE: RootTag type + rootTag != null ? rootTag : createRootTag(0), + ); + } else { + return NativeImageLoaderIOS.prefetchImage(url); + } +} + +function prefetch(url: string): Promise { + return NativeImageLoaderIOS.prefetchImage(url); +} + +async function queryCache( + urls: Array, +): Promise<{[string]: 'memory' | 'disk' | 'disk/memory', ...}> { + return NativeImageLoaderIOS.queryCache(urls); +} + +/** + * A React component for displaying different types of images, + * including network images, static resources, temporary local images, and + * images from local disk, such as the camera roll. + * + * See https://reactnative.dev/docs/image + */ +let BaseImage: AbstractImageIOS = React.forwardRef((props, forwardedRef) => { + const source = getImageSourcesFromImageProps(props) || { + uri: undefined, + width: undefined, + height: undefined, + }; + + let style: ImageStyleProp; + let sources; + if (Array.isArray(source)) { + style = [styles.base, props.style]; + sources = source; + } else { + const {uri} = source; + if (uri === '') { + console.warn('source.uri should not be an empty string'); + } + const width = source.width ?? props.width; + const height = source.height ?? props.height; + style = [{width, height}, styles.base, props.style]; + sources = [source]; + } + + const flattenedStyle = flattenStyle(style); + const objectFit = convertObjectFitToResizeMode(flattenedStyle?.objectFit); + const resizeMode = + objectFit || props.resizeMode || flattenedStyle?.resizeMode || 'cover'; + const tintColor = props.tintColor ?? flattenedStyle?.tintColor; + + if (props.children != null) { + throw new Error( + 'The component cannot contain children. If you want to render content on top of the image, consider using the component or absolute positioning.', + ); + } + const { + 'aria-busy': ariaBusy, + 'aria-checked': ariaChecked, + 'aria-disabled': ariaDisabled, + 'aria-expanded': ariaExpanded, + 'aria-selected': ariaSelected, + accessibilityRole, // [macOS] + height, + src, + width, + ...restProps + } = props; + + const _accessibilityState = { + busy: ariaBusy ?? props.accessibilityState?.busy, + checked: ariaChecked ?? props.accessibilityState?.checked, + disabled: ariaDisabled ?? props.accessibilityState?.disabled, + expanded: ariaExpanded ?? props.accessibilityState?.expanded, + selected: ariaSelected ?? props.accessibilityState?.selected, + }; + const accessibilityLabel = props['aria-label'] ?? props.accessibilityLabel; + + const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef); + + return ( + + {analyticTag => { + return ( + + ); + }} + + ); +}); + +const imageComponentDecorator = unstable_getImageComponentDecorator(); +if (imageComponentDecorator != null) { + BaseImage = imageComponentDecorator(BaseImage); +} + +// $FlowExpectedError[incompatible-type] Eventually we need to move these functions from statics of the component to exports in the module. +const Image: ImageIOS = BaseImage; + +Image.displayName = 'Image'; + +/** + * Retrieve the width and height (in pixels) of an image prior to displaying it. + * + * See https://reactnative.dev/docs/image#getsize + */ +// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time. +Image.getSize = getSize; + +/** + * Retrieve the width and height (in pixels) of an image prior to displaying it + * with the ability to provide the headers for the request. + * + * See https://reactnative.dev/docs/image#getsizewithheaders + */ +// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time. +Image.getSizeWithHeaders = getSizeWithHeaders; + +/** + * Prefetches a remote image for later use by downloading it to the disk + * cache. + * + * See https://reactnative.dev/docs/image#prefetch + */ +// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time. +Image.prefetch = prefetch; + +/** + * Prefetches a remote image for later use by downloading it to the disk + * cache, and adds metadata for queryRootName and rootTag. + * + * See https://reactnative.dev/docs/image#prefetch + */ +// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time. +Image.prefetchWithMetadata = prefetchWithMetadata; + +/** + * Performs cache interrogation. + * + * See https://reactnative.dev/docs/image#querycache + */ +// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time. +Image.queryCache = queryCache; + +/** + * Resolves an asset reference into an object. + * + * See https://reactnative.dev/docs/image#resolveassetsource + */ +// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time. +Image.resolveAssetSource = resolveAssetSource; + +const styles = StyleSheet.create({ + base: { + overflow: 'hidden', + }, +}); -/* $FlowFixMe allow macOS to share iOS file */ -const Image = require('./Image.ios'); module.exports = Image; diff --git a/packages/react-native/Libraries/Image/ImageBackground.js b/packages/react-native/Libraries/Image/ImageBackground.js index 2a3082e09559a3..3e983dc0050ef7 100644 --- a/packages/react-native/Libraries/Image/ImageBackground.js +++ b/packages/react-native/Libraries/Image/ImageBackground.js @@ -8,10 +8,7 @@ * @format */ -'use strict'; - -import type {ViewProps} from '../Components/View/ViewPropTypes'; -import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; +import type {HostInstance} from '../Renderer/shims/ReactNativeTypes'; import type {ImageBackgroundProps} from './ImageProps'; import View from '../Components/View/View'; @@ -55,7 +52,7 @@ class ImageBackground extends React.Component { _viewRef: ?React.ElementRef = null; - _captureRef = (ref: null | React.ElementRef>) => { + _captureRef = (ref: null | HostInstance) => { this._viewRef = ref; }; diff --git a/packages/react-native/Libraries/Image/ImageProps.js b/packages/react-native/Libraries/Image/ImageProps.js index 30c5ddb644554a..c1a59de7c7682e 100644 --- a/packages/react-native/Libraries/Image/ImageProps.js +++ b/packages/react-native/Libraries/Image/ImageProps.js @@ -19,8 +19,9 @@ import type { } from '../StyleSheet/StyleSheet'; import type {LayoutEvent, SyntheticEvent} from '../Types/CoreEventTypes'; import typeof Image from './Image'; +import type {ImageResizeMode} from './ImageResizeMode'; import type {ImageSource} from './ImageSource'; -import type {Node, Ref} from 'react'; +import type {ElementRef, Node, RefSetter} from 'react'; export type ImageLoadEvent = SyntheticEvent< $ReadOnly<{| @@ -65,7 +66,7 @@ type AndroidImageProps = $ReadOnly<{| * dimensions differ from the image view's dimensions. Defaults to `'auto'`. * See https://reactnative.dev/docs/image#resizemethod-android */ - resizeMethod?: ?('auto' | 'resize' | 'scale'), + resizeMethod?: ?('auto' | 'resize' | 'scale' | 'none'), /** * When the `resizeMethod` is set to `resize`, the destination dimensions are @@ -77,7 +78,7 @@ type AndroidImageProps = $ReadOnly<{| resizeMultiplier?: ?number, |}>; -export type ImageProps = {| +export type ImageProps = $ReadOnly<{| ...$Diff>, ...IOSImageProps, ...AndroidImageProps, @@ -234,7 +235,7 @@ export type ImageProps = {| * * See https://reactnative.dev/docs/image#resizemode */ - resizeMode?: ?('cover' | 'contain' | 'stretch' | 'repeat' | 'center'), + resizeMode?: ?ImageResizeMode, /** * A unique identifier for this element to be used in UI Automation @@ -271,7 +272,7 @@ export type ImageProps = {| * Specifies the Tooltip for the view */ tooltip?: ?string, -|}; +|}>; export type ImageBackgroundProps = $ReadOnly<{| ...ImageProps, @@ -296,5 +297,5 @@ export type ImageBackgroundProps = $ReadOnly<{| * * See https://reactnative.dev/docs/imagebackground#imageref */ - imageRef?: Ref, + imageRef?: RefSetter>, |}>; diff --git a/packages/react-native/Libraries/Image/ImageResizeMode.js b/packages/react-native/Libraries/Image/ImageResizeMode.js index b63627b793bd43..50001ce552c82f 100644 --- a/packages/react-native/Libraries/Image/ImageResizeMode.js +++ b/packages/react-native/Libraries/Image/ImageResizeMode.js @@ -33,4 +33,7 @@ export type ImageResizeMode = // Resize by stretching it to fill the entire frame of the view without // clipping. This may change the aspect ratio of the image, distorting it. - | 'stretch'; + | 'stretch' + + // The image will not be resized at all. + | 'none'; diff --git a/packages/react-native/Libraries/Image/ImageSource.js b/packages/react-native/Libraries/Image/ImageSource.js index bbb32572125c11..71de1497cccba2 100644 --- a/packages/react-native/Libraries/Image/ImageSource.js +++ b/packages/react-native/Libraries/Image/ImageSource.js @@ -65,8 +65,6 @@ export interface ImageURISource { * its age or expiration date. If there is no existing data in the cache corresponding * to a URL load request, no attempt is made to load the data from the originating source, * and the load is considered to have failed. - * - * @platform ios */ +cache?: ?('default' | 'reload' | 'force-cache' | 'only-if-cached'); diff --git a/packages/react-native/Libraries/Image/ImageTypes.flow.js b/packages/react-native/Libraries/Image/ImageTypes.flow.js index 7ea0f71a5707ef..eff14cf1d01634 100644 --- a/packages/react-native/Libraries/Image/ImageTypes.flow.js +++ b/packages/react-native/Libraries/Image/ImageTypes.flow.js @@ -56,18 +56,20 @@ type ImageComponentStaticsAndroid = $ReadOnly<{ abortPrefetch(requestId: number): void, }>; -export type AbstractImageAndroid = React.AbstractComponent< - ImagePropsType, - | React.ElementRef - | React.ElementRef, ->; +export type AbstractImageAndroid = component( + ref: React.RefSetter< + | React.ElementRef + | React.ElementRef, + >, + ...props: ImagePropsType +); export type ImageAndroid = AbstractImageAndroid & ImageComponentStaticsAndroid; -export type AbstractImageIOS = React.AbstractComponent< - ImagePropsType, - React.ElementRef, ->; +export type AbstractImageIOS = component( + ref: React.RefSetter>, + ...props: ImagePropsType +); export type ImageIOS = AbstractImageIOS & ImageComponentStaticsIOS; diff --git a/packages/react-native/Libraries/Image/ImageUtils.js b/packages/react-native/Libraries/Image/ImageUtils.js index 732b5733bc6e81..c0e00bb534a84d 100644 --- a/packages/react-native/Libraries/Image/ImageUtils.js +++ b/packages/react-native/Libraries/Image/ImageUtils.js @@ -8,15 +8,18 @@ * @format */ -type ResizeMode = 'cover' | 'contain' | 'stretch' | 'repeat' | 'center'; +import type {ImageResizeMode} from './ImageResizeMode'; -const objectFitMap: {[string]: ResizeMode} = { +const objectFitMap: {[string]: ImageResizeMode} = { contain: 'contain', cover: 'cover', fill: 'stretch', 'scale-down': 'contain', + none: 'none', }; -export function convertObjectFitToResizeMode(objectFit: ?string): ?ResizeMode { +export function convertObjectFitToResizeMode( + objectFit: ?string, +): ?ImageResizeMode { return objectFit != null ? objectFitMap[objectFit] : undefined; } diff --git a/packages/react-native/Libraries/Image/ImageViewNativeComponent.js b/packages/react-native/Libraries/Image/ImageViewNativeComponent.js index 29ff80a61401de..22ac430c2190ef 100644 --- a/packages/react-native/Libraries/Image/ImageViewNativeComponent.js +++ b/packages/react-native/Libraries/Image/ImageViewNativeComponent.js @@ -11,6 +11,7 @@ import type {ViewProps} from '../Components/View/ViewPropTypes'; import type { HostComponent, + HostInstance, PartialViewConfig, } from '../Renderer/shims/ReactNativeTypes'; import type { @@ -20,7 +21,6 @@ import type { } from '../StyleSheet/StyleSheet'; import type {ResolvedAssetSource} from './AssetSourceResolver'; import type {ImageProps} from './ImageProps'; -import type {ElementRef} from 'react'; import * as NativeComponentRegistry from '../NativeComponent/NativeComponentRegistry'; import {ConditionallyIgnoredEventHandlers} from '../NativeComponent/ViewConfigIgnore'; @@ -48,7 +48,7 @@ type Props = $ReadOnly<{ interface NativeCommands { +setIsVisible_EXPERIMENTAL: ( - viewRef: ElementRef>, + viewRef: HostInstance, isVisible: boolean, time: number, ) => void; @@ -82,6 +82,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = }, validAttributes: { blurRadius: true, + defaultSource: { + process: require('./resolveAssetSource'), + }, internal_analyticTag: true, resizeMethod: true, resizeMode: true, @@ -100,7 +103,6 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = borderRadius: true, headers: true, shouldNotifyLoadEvents: true, - defaultSrc: true, overlayColor: { process: require('../StyleSheet/processColor').default, }, diff --git a/packages/react-native/Libraries/Image/RCTImageCache.mm b/packages/react-native/Libraries/Image/RCTImageCache.mm index 947bf378b99fbe..034e08dcac119b 100644 --- a/packages/react-native/Libraries/Image/RCTImageCache.mm +++ b/packages/react-native/Libraries/Image/RCTImageCache.mm @@ -18,8 +18,13 @@ #import +#if !TARGET_OS_OSX // [macOS] static NSUInteger RCTMaxCacheableDecodedImageSizeInBytes = 2 * 1024 * 1024; static NSUInteger RCTImageCacheTotalCostLimit = 20 * 1024 * 1024; +#else // [macOS +static NSUInteger RCTMaxCacheableDecodedImageSizeInBytes = 20 * 1024 * 1024; +static NSUInteger RCTImageCacheTotalCostLimit = 100 * 1024 * 1024; +#endif // macOS] void RCTSetImageCacheLimits(NSUInteger maxCacheableDecodedImageSizeInBytes, NSUInteger imageCacheTotalCostLimit) { diff --git a/packages/react-native/Libraries/Image/RCTImageLoader.mm b/packages/react-native/Libraries/Image/RCTImageLoader.mm index 078254ce200ddd..401f56de032473 100644 --- a/packages/react-native/Libraries/Image/RCTImageLoader.mm +++ b/packages/react-native/Libraries/Image/RCTImageLoader.mm @@ -621,6 +621,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; @@ -628,7 +637,7 @@ - (RCTImageURLLoaderRequest *)_loadImageOrDataWithURLRequest:(NSURLRequest *)req return; } - if (loadHandler) { + if (loadHandler && !useDefaultLoading) { dispatch_block_t cancelLoadLocal; if ([loadHandler conformsToProtocol:@protocol(RCTImageURLLoaderWithAttribution)]) { RCTImageURLLoaderRequest *loaderRequest = [(id)loadHandler @@ -1035,6 +1044,7 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data if (!_isLoaderSetup) { [self setUp]; } + dispatch_async(_URLRequestQueue, ^{ // The decode operation retains the compressed image data until it's // complete, so we'll mark it as having started, in order to block diff --git a/packages/react-native/Libraries/Image/RCTImageURLLoader.h b/packages/react-native/Libraries/Image/RCTImageURLLoader.h index 755e1adc5ae561..0359e18123cd42 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; +#ifdef 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 requst. + */ +- (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 825017e91792e2..f1cb99eb7e5ce5 100644 --- a/packages/react-native/Libraries/Image/RCTImageView.mm +++ b/packages/react-native/Libraries/Image/RCTImageView.mm @@ -147,6 +147,11 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge #endif // macOS] _imageView = [RCTUIImageViewAnimated new]; _imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; +#if TARGET_OS_OSX + _resizeMode = RCTResizeModeCover; + _imageView.contentMode = (UIViewContentMode)RCTResizeModeCover; + [_imageView unregisterDraggedTypes]; +#endif [self addSubview:_imageView]; #if !TARGET_OS_OSX // [macOS] @@ -195,10 +200,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 +285,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 +555,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 +606,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 efd888d6641ff3..1ae506d8571bb0 100644 --- a/packages/react-native/Libraries/Image/RCTResizeMode.h +++ b/packages/react-native/Libraries/Image/RCTResizeMode.h @@ -8,17 +8,11 @@ #import typedef NS_ENUM(NSInteger, RCTResizeMode) { -#if !TARGET_OS_OSX // [macOS] RCTResizeModeCover = UIViewContentModeScaleAspectFill, RCTResizeModeContain = UIViewContentModeScaleAspectFit, RCTResizeModeStretch = UIViewContentModeScaleToFill, RCTResizeModeCenter = UIViewContentModeCenter, -#else // [macOS - RCTResizeModeCover = -2, // Not supported by NSImageView - RCTResizeModeContain = NSImageScaleProportionallyUpOrDown, - RCTResizeModeStretch = NSImageScaleAxesIndependently, - RCTResizeModeCenter = NSImageScaleNone, // assumes NSImageAlignmentCenter -#endif // macOS] + RCTResizeModeNone = UIViewContentModeTopLeft, RCTResizeModeRepeat = -1, // Use negative values to avoid conflicts with iOS enum values. }; @@ -37,6 +31,9 @@ static inline RCTResizeMode RCTResizeModeFromUIViewContentMode(UIViewContentMode case UIViewContentModeCenter: return RCTResizeModeCenter; break; + case UIViewContentModeTopLeft: + return RCTResizeModeNone; + break; #if !TARGET_OS_OSX // [macOS] case UIViewContentModeRedraw: case UIViewContentModeTop: diff --git a/packages/react-native/Libraries/Image/nativeImageSource.js b/packages/react-native/Libraries/Image/nativeImageSource.js index c44f665e83b47a..b04fda6500ab11 100644 --- a/packages/react-native/Libraries/Image/nativeImageSource.js +++ b/packages/react-native/Libraries/Image/nativeImageSource.js @@ -12,7 +12,7 @@ import type {ImageURISource} from './ImageSource'; import Platform from '../Utilities/Platform'; -type NativeImageSourceSpec = $ReadOnly<{| +type NativeImageSourceSpec = $ReadOnly<{ android?: string, ios?: string, macos?: string, // [macOS] @@ -22,7 +22,7 @@ type NativeImageSourceSpec = $ReadOnly<{| // https://reactnative.dev/docs/images#why-not-automatically-size-everything height: number, width: number, -|}>; +}>; /** * In hybrid apps, use `nativeImageSource` to access images that are already @@ -63,4 +63,4 @@ function nativeImageSource(spec: NativeImageSourceSpec): ImageURISource { }; } -module.exports = nativeImageSource; +export default nativeImageSource; diff --git a/packages/react-native/Libraries/Inspector/NetworkOverlay.js b/packages/react-native/Libraries/Inspector/NetworkOverlay.js index 9568b963f83eb6..82d4902bd0f7a8 100644 --- a/packages/react-native/Libraries/Inspector/NetworkOverlay.js +++ b/packages/react-native/Libraries/Inspector/NetworkOverlay.js @@ -10,7 +10,7 @@ 'use strict'; -import type {RenderItemProps} from '@react-native-mac/virtualized-lists'; // [macOS] +import type {RenderItemProps} from '@react-native/virtualized-lists'; // [macOS] const ScrollView = require('../Components/ScrollView/ScrollView'); const TouchableHighlight = require('../Components/Touchable/TouchableHighlight'); @@ -143,6 +143,7 @@ class NetworkOverlay extends React.Component { }); XHRInterceptor.setRequestHeaderCallback((header, value, xhr) => { + // $FlowFixMe[prop-missing] const xhrIndex = this._getRequestIndexByXHRID(xhr._index); if (xhrIndex === -1) { return; @@ -159,6 +160,7 @@ class NetworkOverlay extends React.Component { }); XHRInterceptor.setSendCallback((data, xhr) => { + // $FlowFixMe[prop-missing] const xhrIndex = this._getRequestIndexByXHRID(xhr._index); if (xhrIndex === -1) { return; @@ -173,6 +175,7 @@ class NetworkOverlay extends React.Component { XHRInterceptor.setHeaderReceivedCallback( (type, size, responseHeaders, xhr) => { + // $FlowFixMe[prop-missing] const xhrIndex = this._getRequestIndexByXHRID(xhr._index); if (xhrIndex === -1) { return; @@ -190,6 +193,7 @@ class NetworkOverlay extends React.Component { XHRInterceptor.setResponseCallback( (status, timeout, response, responseURL, responseType, xhr) => { + // $FlowFixMe[prop-missing] const xhrIndex = this._getRequestIndexByXHRID(xhr._index); if (xhrIndex === -1) { return; diff --git a/packages/react-native/Libraries/Inspector/ReactDevToolsOverlay.js b/packages/react-native/Libraries/Inspector/ReactDevToolsOverlay.js index 866c3d7d4e99f6..46b4979a5544b4 100644 --- a/packages/react-native/Libraries/Inspector/ReactDevToolsOverlay.js +++ b/packages/react-native/Libraries/Inspector/ReactDevToolsOverlay.js @@ -20,7 +20,6 @@ import StyleSheet from '../StyleSheet/StyleSheet'; import ElementBox from './ElementBox'; import * as React from 'react'; -const {findNodeHandle} = require('../ReactNative/RendererProxy'); const getInspectorDataForViewAtPoint = require('./getInspectorDataForViewAtPoint'); const {useEffect, useState, useCallback} = React; @@ -78,20 +77,15 @@ export default function ReactDevToolsOverlay({ x, y, viewData => { - const {touchedViewTag, closestInstance, frame} = viewData; - if (closestInstance != null || touchedViewTag != null) { - // We call `selectNode` for both non-fabric(viewTag) and fabric(instance), - // this makes sure it works for both architectures. - reactDevToolsAgent.selectNode(findNodeHandle(touchedViewTag)); - if (closestInstance != null) { - reactDevToolsAgent.selectNode(closestInstance); - } - setInspected({ - frame, - }); - return true; + const {frame, closestPublicInstance} = viewData; + + if (closestPublicInstance == null) { + return false; } - return false; + + reactDevToolsAgent.selectNode(closestPublicInstance); + setInspected({frame}); + return true; }, ); }, diff --git a/packages/react-native/Libraries/Inspector/getInspectorDataForViewAtPoint.js b/packages/react-native/Libraries/Inspector/getInspectorDataForViewAtPoint.js index e51946944578a4..daa4dd284076cf 100644 --- a/packages/react-native/Libraries/Inspector/getInspectorDataForViewAtPoint.js +++ b/packages/react-native/Libraries/Inspector/getInspectorDataForViewAtPoint.js @@ -9,18 +9,16 @@ */ import type { - HostComponent, + HostInstance, TouchedViewDataAtPoint, } from '../Renderer/shims/ReactNativeTypes'; const invariant = require('invariant'); -const React = require('react'); -export type HostRef = React.ElementRef>; export type ReactRenderer = { rendererConfig: { getInspectorDataForViewAtPoint: ( - inspectedView: ?HostRef, + inspectedView: ?HostInstance, locationX: number, locationY: number, callback: Function, @@ -52,7 +50,7 @@ function validateRenderers(): void { } module.exports = function getInspectorDataForViewAtPoint( - inspectedView: ?HostRef, + inspectedView: ?HostInstance, locationX: number, locationY: number, callback: (viewData: TouchedViewDataAtPoint) => boolean, diff --git a/packages/react-native/Libraries/Interaction/InteractionManager.js b/packages/react-native/Libraries/Interaction/InteractionManager.js index 7415f84266e374..3257921ec97f99 100644 --- a/packages/react-native/Libraries/Interaction/InteractionManager.js +++ b/packages/react-native/Libraries/Interaction/InteractionManager.js @@ -10,6 +10,7 @@ import type {Task} from './TaskQueue'; +import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags'; import EventEmitter from '../vendor/emitter/EventEmitter'; const BatchedBridge = require('../BatchedBridge/BatchedBridge'); @@ -208,4 +209,8 @@ function _processUpdate() { _deleteInteractionSet.clear(); } -module.exports = InteractionManager; +module.exports = ( + ReactNativeFeatureFlags.disableInteractionManager() + ? require('./InteractionManagerStub') + : InteractionManager +) as typeof InteractionManager; diff --git a/packages/react-native/Libraries/Interaction/TouchHistoryMath.js b/packages/react-native/Libraries/Interaction/TouchHistoryMath.js index aec0d66913137b..331737a054667a 100644 --- a/packages/react-native/Libraries/Interaction/TouchHistoryMath.js +++ b/packages/react-native/Libraries/Interaction/TouchHistoryMath.js @@ -5,8 +5,11 @@ * LICENSE file in the root directory of this source tree. * * @format + * @flow strict-local */ +// $FlowFixMe[definition-cycle] +// $FlowFixMe[recursive-definition] const TouchHistoryMath = { /** * This code is optimized and not intended to look beautiful. This allows @@ -25,11 +28,11 @@ const TouchHistoryMath = { * @return {number} value of centroid in specified dimension. */ centroidDimension: function ( - touchHistory, - touchesChangedAfter, - isXAxis, - ofCurrent, - ) { + touchHistory: TouchHistoryMath, + touchesChangedAfter: number, + isXAxis: boolean, + ofCurrent: boolean, + ): number { const touchBank = touchHistory.touchBank; let total = 0; let count = 0; @@ -82,9 +85,9 @@ const TouchHistoryMath = { }, currentCentroidXOfTouchesChangedAfter: function ( - touchHistory, - touchesChangedAfter, - ) { + touchHistory: TouchHistoryMath, + touchesChangedAfter: number, + ): number { return TouchHistoryMath.centroidDimension( touchHistory, touchesChangedAfter, @@ -94,9 +97,9 @@ const TouchHistoryMath = { }, currentCentroidYOfTouchesChangedAfter: function ( - touchHistory, - touchesChangedAfter, - ) { + touchHistory: TouchHistoryMath, + touchesChangedAfter: number, + ): number { return TouchHistoryMath.centroidDimension( touchHistory, touchesChangedAfter, @@ -106,9 +109,9 @@ const TouchHistoryMath = { }, previousCentroidXOfTouchesChangedAfter: function ( - touchHistory, - touchesChangedAfter, - ) { + touchHistory: TouchHistoryMath, + touchesChangedAfter: number, + ): number { return TouchHistoryMath.centroidDimension( touchHistory, touchesChangedAfter, @@ -118,9 +121,9 @@ const TouchHistoryMath = { }, previousCentroidYOfTouchesChangedAfter: function ( - touchHistory, - touchesChangedAfter, - ) { + touchHistory: TouchHistoryMath, + touchesChangedAfter: number, + ): number { return TouchHistoryMath.centroidDimension( touchHistory, touchesChangedAfter, @@ -129,7 +132,7 @@ const TouchHistoryMath = { ); }, - currentCentroidX: function (touchHistory) { + currentCentroidX: function (touchHistory: TouchHistoryMath): number { return TouchHistoryMath.centroidDimension( touchHistory, 0, // touchesChangedAfter @@ -138,7 +141,7 @@ const TouchHistoryMath = { ); }, - currentCentroidY: function (touchHistory) { + currentCentroidY: function (touchHistory: TouchHistoryMath): number { return TouchHistoryMath.centroidDimension( touchHistory, 0, // touchesChangedAfter diff --git a/packages/react-native/Libraries/JSInspector/NetworkAgent.js b/packages/react-native/Libraries/JSInspector/NetworkAgent.js index 5723af01de9213..7a599d64aac891 100644 --- a/packages/react-native/Libraries/JSInspector/NetworkAgent.js +++ b/packages/react-native/Libraries/JSInspector/NetworkAgent.js @@ -260,7 +260,7 @@ type EnableArgs = { }; class NetworkAgent extends InspectorAgent { - static DOMAIN: $TEMPORARY$string<'Network'> = 'Network'; + static DOMAIN: string = 'Network'; _sendEvent: EventSender; _interceptor: ?Interceptor; diff --git a/packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.js b/packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.js index 52d4ac7b818032..a14283e1e7ec0c 100644 --- a/packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.js +++ b/packages/react-native/Libraries/LayoutAnimation/LayoutAnimation.js @@ -121,7 +121,7 @@ const Presets = { 'opacity', ): LayoutAnimationConfig), linear: (create(500, 'linear', 'opacity'): LayoutAnimationConfig), - spring: { + spring: ({ duration: 700, create: { type: 'linear', @@ -135,7 +135,7 @@ const Presets = { type: 'linear', property: 'opacity', }, - }, + }: LayoutAnimationConfig), }; /** diff --git a/packages/react-native/Libraries/Linking/Linking.js b/packages/react-native/Libraries/Linking/Linking.js index 14f9f415bf4145..80565d144b55d6 100644 --- a/packages/react-native/Libraries/Linking/Linking.js +++ b/packages/react-native/Libraries/Linking/Linking.js @@ -134,4 +134,4 @@ class Linking extends NativeEventEmitter { } } -module.exports = (new Linking(): Linking); +export default (new Linking(): Linking); diff --git a/packages/react-native/Libraries/Lists/FillRateHelper.js b/packages/react-native/Libraries/Lists/FillRateHelper.js index d6fb7ffabca1ef..3469bdd2dea64c 100644 --- a/packages/react-native/Libraries/Lists/FillRateHelper.js +++ b/packages/react-native/Libraries/Lists/FillRateHelper.js @@ -10,10 +10,10 @@ 'use strict'; -import {typeof FillRateHelper as FillRateHelperType} from '@react-native-mac/virtualized-lists'; // [macOS] +import {typeof FillRateHelper as FillRateHelperType} from '@react-native/virtualized-lists'; // [macOS] const FillRateHelper: FillRateHelperType = - require('@react-native-mac/virtualized-lists').FillRateHelper; // [macOS] + require('@react-native/virtualized-lists').FillRateHelper; // [macOS] -export type {FillRateInfo} from '@react-native-mac/virtualized-lists'; // [macOS] +export type {FillRateInfo} from '@react-native/virtualized-lists'; // [macOS] module.exports = FillRateHelper; diff --git a/packages/react-native/Libraries/Lists/FlatList.js b/packages/react-native/Libraries/Lists/FlatList.js index c9ebb90f30c5cd..ec38cef63cc927 100644 --- a/packages/react-native/Libraries/Lists/FlatList.js +++ b/packages/react-native/Libraries/Lists/FlatList.js @@ -11,35 +11,35 @@ import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent'; import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; import type { - RenderItemProps, - RenderItemType, + ListRenderItem, + ListRenderItemInfo, ViewabilityConfigCallbackPair, ViewToken, -} from '@react-native-mac/virtualized-lists'; // [macOS] +} from '@react-native/virtualized-lists'; // [macOS] import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags'; import {type ScrollResponderType} from '../Components/ScrollView/ScrollView'; -import { - VirtualizedList, - keyExtractor as defaultKeyExtractor, -} from '@react-native-mac/virtualized-lists'; // [macOS] +import VirtualizedLists from '@react-native/virtualized-lists'; // [macOS] import memoizeOne from 'memoize-one'; -const View = require('../Components/View/View'); +const View = require('../Components/View/View').default; const StyleSheet = require('../StyleSheet/StyleSheet'); -const deepDiffer = require('../Utilities/differ/deepDiffer'); -const Platform = require('../Utilities/Platform'); +const deepDiffer = require('../Utilities/differ/deepDiffer').default; +const Platform = require('../Utilities/Platform').default; const invariant = require('invariant'); const React = require('react'); -type RequiredProps = {| +const VirtualizedList = VirtualizedLists.VirtualizedList; +const defaultKeyExtractor = VirtualizedLists.keyExtractor; + +type RequiredProps = { /** * An array (or array-like list) of items to render. Other data types can be * used by targeting VirtualizedList directly. */ data: ?$ReadOnly<$ArrayLike>, -|}; -type OptionalProps = {| +}; +type OptionalProps = { /** * Takes an item from `data` and renders it into the list. Example usage: * @@ -66,7 +66,7 @@ type OptionalProps = {| * `highlight` and `unhighlight` (which set the `highlighted: boolean` prop) are insufficient for * your use-case. */ - renderItem?: ?RenderItemType, + renderItem?: ?ListRenderItem, /** * Optional custom style for multi-item rows generated when numColumns > 1. */ @@ -173,7 +173,7 @@ type OptionalProps = {| * Enable an optimization to memoize the item renderer to prevent unnecessary rerenders. */ strictMode?: boolean, -|}; +}; /** * Default Props Helper Functions @@ -199,10 +199,10 @@ function isArrayLike(data: mixed): boolean { return typeof Object(data).length === 'number'; } -type FlatListProps = {| +type FlatListProps = { ...RequiredProps, ...OptionalProps, -|}; +}; type VirtualizedListProps = React.ElementConfig; @@ -516,10 +516,10 @@ class FlatList extends React.PureComponent, void> { this._checkProps(this.props); } - _listRef: ?React.ElementRef; + _listRef: ?VirtualizedList; _virtualizedListPairs: Array = []; - _captureRef = (ref: ?React.ElementRef) => { + _captureRef = (ref: ?VirtualizedList) => { this._listRef = ref; }; @@ -654,7 +654,7 @@ class FlatList extends React.PureComponent, void> { _renderer = ( ListItemComponent: ?(React.ComponentType | React.MixedElement), - renderItem: ?RenderItemType, + renderItem: ?ListRenderItem, columnWrapperStyle: ?ViewStyleProp, numColumns: ?number, extraData: ?any, @@ -662,7 +662,7 @@ class FlatList extends React.PureComponent, void> { ) => { const cols = numColumnsOrDefault(numColumns); - const render = (props: RenderItemProps): React.Node => { + const render = (props: ListRenderItemInfo): React.Node => { if (ListItemComponent) { // $FlowFixMe[not-a-component] Component isn't valid // $FlowFixMe[incompatible-type-arg] Component isn't valid @@ -676,7 +676,7 @@ class FlatList extends React.PureComponent, void> { } }; - const renderProp = (info: RenderItemProps) => { + const renderProp = (info: ListRenderItemInfo) => { if (cols > 1) { const {item, index} = info; invariant( @@ -751,4 +751,4 @@ const styles = StyleSheet.create({ row: {flexDirection: 'row'}, }); -module.exports = FlatList; +export default FlatList; diff --git a/packages/react-native/Libraries/Lists/SectionList.js b/packages/react-native/Libraries/Lists/SectionList.js index d4a895e25e2889..3a4faae0505610 100644 --- a/packages/react-native/Libraries/Lists/SectionList.js +++ b/packages/react-native/Libraries/Lists/SectionList.js @@ -15,10 +15,10 @@ import type { ScrollToLocationParamsType, SectionBase as _SectionBase, VirtualizedSectionListProps, -} from '@react-native-mac/virtualized-lists'; // [macOS] +} from '@react-native/virtualized-lists'; // [macOS] import Platform from '../Utilities/Platform'; -import {VirtualizedSectionList} from '@react-native-mac/virtualized-lists'; // [macOS] +import {VirtualizedSectionList} from '@react-native/virtualized-lists'; // [macOS] import * as React from 'react'; type Item = any; diff --git a/packages/react-native/Libraries/Lists/SectionListModern.js b/packages/react-native/Libraries/Lists/SectionListModern.js index c58d575274cb3b..644e2622817060 100644 --- a/packages/react-native/Libraries/Lists/SectionListModern.js +++ b/packages/react-native/Libraries/Lists/SectionListModern.js @@ -15,11 +15,11 @@ import type { ScrollToLocationParamsType, SectionBase as _SectionBase, VirtualizedSectionListProps, -} from '@react-native-mac/virtualized-lists'; // [macOS] -import type {AbstractComponent, ElementRef} from 'react'; +} from '@react-native/virtualized-lists'; // [macOS] +import type {ElementRef} from 'react'; import Platform from '../Utilities/Platform'; -import {VirtualizedSectionList} from '@react-native-mac/virtualized-lists'; // [macOS] +import {VirtualizedSectionList} from '@react-native/virtualized-lists'; // [macOS] import React, {forwardRef, useImperativeHandle, useRef} from 'react'; type Item = any; @@ -93,7 +93,7 @@ type OptionalProps> = {| removeClippedSubviews?: boolean, |}; -export type Props = {| +export type Props> = $ReadOnly<{| ...$Diff< VirtualizedSectionListProps, { @@ -115,7 +115,7 @@ export type Props = {| >, ...RequiredProps, ...OptionalProps, -|}; +|}>; /** * A performant interface for rendering sectioned lists, supporting the most handy features: @@ -172,10 +172,10 @@ export type Props = {| * Alternatively, you can provide a custom `keyExtractor` prop. * */ -const SectionList: AbstractComponent>, any> = forwardRef< - Props>, - any, ->((props, ref) => { +const SectionList: component( + ref?: React.RefSetter, + ...Props> +) = forwardRef>, any>((props, ref) => { const propsWithDefaults = { stickySectionHeadersEnabled: Platform.OS === 'ios', ...props, diff --git a/packages/react-native/Libraries/Lists/ViewabilityHelper.js b/packages/react-native/Libraries/Lists/ViewabilityHelper.js index 88d1d970744a7e..11c9e363f313ae 100644 --- a/packages/react-native/Libraries/Lists/ViewabilityHelper.js +++ b/packages/react-native/Libraries/Lists/ViewabilityHelper.js @@ -14,11 +14,11 @@ export type { ViewToken, ViewabilityConfig, ViewabilityConfigCallbackPair, -} from '@react-native-mac/virtualized-lists'; // [macOS] +} from '@react-native/virtualized-lists'; // [macOS] -import {typeof ViewabilityHelper as ViewabilityHelperType} from '@react-native-mac/virtualized-lists'; // [macOS] +import {typeof ViewabilityHelper as ViewabilityHelperType} from '@react-native/virtualized-lists'; // [macOS] const ViewabilityHelper: ViewabilityHelperType = - require('@react-native-mac/virtualized-lists').ViewabilityHelper; // [macOS] + require('@react-native/virtualized-lists').ViewabilityHelper; // [macOS] module.exports = ViewabilityHelper; diff --git a/packages/react-native/Libraries/Lists/VirtualizeUtils.js b/packages/react-native/Libraries/Lists/VirtualizeUtils.js index 0eeeb243c7ce87..0ccc2ef2825f19 100644 --- a/packages/react-native/Libraries/Lists/VirtualizeUtils.js +++ b/packages/react-native/Libraries/Lists/VirtualizeUtils.js @@ -10,9 +10,9 @@ 'use strict'; -import {typeof keyExtractor as KeyExtractorType} from '@react-native-mac/virtualized-lists'; // [macOS] +import {typeof keyExtractor as KeyExtractorType} from '@react-native/virtualized-lists'; // [macOS] const keyExtractor: KeyExtractorType = - require('@react-native-mac/virtualized-lists').keyExtractor; // [macOS] + require('@react-native/virtualized-lists').keyExtractor; // [macOS] module.exports = {keyExtractor}; diff --git a/packages/react-native/Libraries/Lists/VirtualizedList.js b/packages/react-native/Libraries/Lists/VirtualizedList.js index 00685d2d525bbc..cdf08fd1b6124b 100644 --- a/packages/react-native/Libraries/Lists/VirtualizedList.js +++ b/packages/react-native/Libraries/Lists/VirtualizedList.js @@ -10,14 +10,14 @@ 'use strict'; -import {typeof VirtualizedList as VirtualizedListType} from '@react-native-mac/virtualized-lists'; // [macOS] +import {typeof VirtualizedList as VirtualizedListType} from '@react-native/virtualized-lists'; // [macOS] const VirtualizedList: VirtualizedListType = - require('@react-native-mac/virtualized-lists').VirtualizedList; // [macOS] + require('@react-native/virtualized-lists').VirtualizedList; // [macOS] export type { RenderItemProps, RenderItemType, Separators, -} from '@react-native-mac/virtualized-lists'; // [macOS] +} from '@react-native/virtualized-lists'; // [macOS] module.exports = VirtualizedList; diff --git a/packages/react-native/Libraries/Lists/VirtualizedListContext.js b/packages/react-native/Libraries/Lists/VirtualizedListContext.js index 5e8972e6751c39..6edbf612091bbd 100644 --- a/packages/react-native/Libraries/Lists/VirtualizedListContext.js +++ b/packages/react-native/Libraries/Lists/VirtualizedListContext.js @@ -10,9 +10,9 @@ 'use strict'; -import {typeof VirtualizedListContextResetter as VirtualizedListContextResetterType} from '@react-native-mac/virtualized-lists'; // [macOS] +import {typeof VirtualizedListContextResetter as VirtualizedListContextResetterType} from '@react-native/virtualized-lists'; // [macOS] const VirtualizedListContextResetter: VirtualizedListContextResetterType = - require('@react-native-mac/virtualized-lists').VirtualizedListContextResetter; // [macOS] + require('@react-native/virtualized-lists').VirtualizedListContextResetter; // [macOS] module.exports = {VirtualizedListContextResetter}; diff --git a/packages/react-native/Libraries/Lists/VirtualizedSectionList.js b/packages/react-native/Libraries/Lists/VirtualizedSectionList.js index 2ebd31cf7fdb49..5432c33c932237 100644 --- a/packages/react-native/Libraries/Lists/VirtualizedSectionList.js +++ b/packages/react-native/Libraries/Lists/VirtualizedSectionList.js @@ -10,13 +10,13 @@ 'use strict'; -import {typeof VirtualizedSectionList as VirtualizedSectionListType} from '@react-native-mac/virtualized-lists'; // [macOS] +import {typeof VirtualizedSectionList as VirtualizedSectionListType} from '@react-native/virtualized-lists'; // [macOS] const VirtualizedSectionList: VirtualizedSectionListType = - require('@react-native-mac/virtualized-lists').VirtualizedSectionList; // [macOS] + require('@react-native/virtualized-lists').VirtualizedSectionList; // [macOS] export type { SectionBase, ScrollToLocationParamsType, -} from '@react-native-mac/virtualized-lists'; // [macOS] +} from '@react-native/virtualized-lists'; // [macOS] module.exports = VirtualizedSectionList; diff --git a/packages/react-native/Libraries/LogBox/Data/LogBoxData.js b/packages/react-native/Libraries/LogBox/Data/LogBoxData.js index 367a78dd70db24..918d1f836a646f 100644 --- a/packages/react-native/Libraries/LogBox/Data/LogBoxData.js +++ b/packages/react-native/Libraries/LogBox/Data/LogBoxData.js @@ -18,7 +18,7 @@ import type { Message, } from './parseLogBoxLog'; -import DebuggerSessionObserver from '../../../src/private/fusebox/FuseboxSessionObserver'; +import DebuggerSessionObserver from '../../../src/private/debugging/FuseboxSessionObserver'; import parseErrorStack from '../../Core/Devtools/parseErrorStack'; import NativeDevSettings from '../../NativeModules/specs/NativeDevSettings'; import NativeLogBox from '../../NativeModules/specs/NativeLogBox'; @@ -421,7 +421,7 @@ type State = $ReadOnly<{| selectedLogIndex: number, |}>; -type SubscribedComponent = React.AbstractComponent< +type SubscribedComponent = React.ComponentType< $ReadOnly<{| logs: $ReadOnlyArray, isDisabled: boolean, @@ -431,7 +431,7 @@ type SubscribedComponent = React.AbstractComponent< export function withSubscription( WrappedComponent: SubscribedComponent, -): React.AbstractComponent<{||}> { +): React.ComponentType<{||}> { class LogBoxStateSubscription extends React.Component { static getDerivedStateFromError(): {hasError: boolean} { return {hasError: true}; diff --git a/packages/react-native/Libraries/LogBox/LogBox.js b/packages/react-native/Libraries/LogBox/LogBox.js index e9987147da7bc9..473f1e1a5b2130 100644 --- a/packages/react-native/Libraries/LogBox/LogBox.js +++ b/packages/react-native/Libraries/LogBox/LogBox.js @@ -52,6 +52,17 @@ if (__DEV__) { isLogBoxInstalled = true; + if (global.RN$registerExceptionListener != null) { + global.RN$registerExceptionListener( + (error: ExtendedExceptionData & {preventDefault: () => mixed}) => { + if (global.RN$isRuntimeReady?.() || !error.isFatal) { + error.preventDefault(); + addException(error); + } + }, + ); + } + // Trigger lazy initialization of module. require('../NativeModules/specs/NativeLogBox'); @@ -122,13 +133,15 @@ if (__DEV__) { } }, - addException(error: ExtendedExceptionData): void { - if (isLogBoxInstalled) { - LogBoxData.addException(error); - } - }, + addException, }; + function addException(error: ExtendedExceptionData): void { + if (isLogBoxInstalled) { + LogBoxData.addException(error); + } + } + const isRCTLogAdviceWarning = (...args: Array) => { // RCTLogAdvice is a native logging function designed to show users // a message in the console, but not show it to them in Logbox. diff --git a/packages/react-native/Libraries/LogBox/LogBoxInspectorContainer.js b/packages/react-native/Libraries/LogBox/LogBoxInspectorContainer.js index 142cccaf44bfc2..3ffaab83616e60 100644 --- a/packages/react-native/Libraries/LogBox/LogBoxInspectorContainer.js +++ b/packages/react-native/Libraries/LogBox/LogBoxInspectorContainer.js @@ -65,4 +65,4 @@ export class _LogBoxInspectorContainer extends React.Component { export default (LogBoxData.withSubscription( _LogBoxInspectorContainer, -): React.AbstractComponent<{||}>); +): React.ComponentType<{||}>); diff --git a/packages/react-native/Libraries/LogBox/LogBoxNotificationContainer.js b/packages/react-native/Libraries/LogBox/LogBoxNotificationContainer.js index 8b498cea070b38..6f3a8918592459 100644 --- a/packages/react-native/Libraries/LogBox/LogBoxNotificationContainer.js +++ b/packages/react-native/Libraries/LogBox/LogBoxNotificationContainer.js @@ -8,13 +8,13 @@ * @format */ +import SafeAreaView from '../../src/private/components/SafeAreaView_INTERNAL_DO_NOT_USE'; import View from '../Components/View/View'; import StyleSheet from '../StyleSheet/StyleSheet'; import * as LogBoxData from './Data/LogBoxData'; import LogBoxLog from './Data/LogBoxLog'; import LogBoxLogNotification from './UI/LogBoxNotification'; import * as React from 'react'; -import SafeAreaView from '../../src/private/components/SafeAreaView_INTERNAL_DO_NOT_USE'; type Props = $ReadOnly<{| logs: $ReadOnlyArray, @@ -102,4 +102,4 @@ const styles = StyleSheet.create({ export default (LogBoxData.withSubscription( _LogBoxNotificationContainer, -): React.AbstractComponent<{||}>); +): React.ComponentType<{||}>); diff --git a/packages/react-native/Libraries/LogBox/UI/AnsiHighlight.js b/packages/react-native/Libraries/LogBox/UI/AnsiHighlight.js index 3378e7a3c9cd66..a71f033ed81e91 100644 --- a/packages/react-native/Libraries/LogBox/UI/AnsiHighlight.js +++ b/packages/react-native/Libraries/LogBox/UI/AnsiHighlight.js @@ -37,6 +37,8 @@ const COLORS = { 'ansi-bright-white': 'rgb(247, 247, 247)', }; +const LRM = '\u200E'; // Left-to-Right Mark + export default function Ansi({ text, style, @@ -80,25 +82,28 @@ export default function Ansi({ }; return ( - + {parsedLines.map((items, i) => ( - {items.map((bundle, key) => { - const textStyle = - bundle.fg && COLORS[bundle.fg] - ? { - backgroundColor: bundle.bg && COLORS[bundle.bg], - color: bundle.fg && COLORS[bundle.fg], - } - : { - backgroundColor: bundle.bg && COLORS[bundle.bg], - }; - return ( - - {getText(bundle.content, key)} - - ); - })} + + {LRM} + {items.map((bundle, key) => { + const textStyle = + bundle.fg && COLORS[bundle.fg] + ? { + backgroundColor: bundle.bg && COLORS[bundle.bg], + color: bundle.fg && COLORS[bundle.fg], + } + : { + backgroundColor: bundle.bg && COLORS[bundle.bg], + }; + return ( + + {getText(bundle.content, key)} + + ); + })} + ))} @@ -106,6 +111,10 @@ export default function Ansi({ } const styles = StyleSheet.create({ + container: { + minWidth: '100%', + direction: 'ltr', + }, line: { flexDirection: 'row', }, diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js index 17b1f3bc6ffb22..92af9dbb9e7c20 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js @@ -22,9 +22,9 @@ import LogBoxButton from './LogBoxButton'; import LogBoxInspectorSection from './LogBoxInspectorSection'; import * as LogBoxStyle from './LogBoxStyle'; import * as React from 'react'; -type Props = $ReadOnly<{| +type Props = $ReadOnly<{ codeFrame: ?CodeFrame, -|}>; +}>; function LogBoxInspectorCodeFrame(props: Props): React.Node { const codeFrame = props.codeFrame; @@ -59,7 +59,9 @@ function LogBoxInspectorCodeFrame(props: Props): React.Node { }> - + @@ -138,6 +140,9 @@ const styles = StyleSheet.create({ paddingTop: 10, paddingBottom: 10, }, + contentContainer: { + minWidth: '100%', + }, content: { color: LogBoxStyle.getTextColor(1), fontSize: 12, diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js index 5f52995d674700..bc012e0330fcc2 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js @@ -27,7 +27,7 @@ type Props = $ReadOnly<{ level: LogLevel, }>; -const LogBoxInspectorHeaderSafeArea: React.AbstractComponent = +const LogBoxInspectorHeaderSafeArea: React.ComponentType = Platform.OS === 'android' ? View : SafeAreaView; export default function LogBoxInspectorHeader(props: Props): React.Node { diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js index cb6fbb5c2d6029..8640e7a0e6d0e6 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js @@ -20,9 +20,9 @@ import LogBoxInspectorSection from './LogBoxInspectorSection'; import * as LogBoxStyle from './LogBoxStyle'; import * as React from 'react'; -type Props = $ReadOnly<{| +type Props = $ReadOnly<{ log: LogBoxLog, -|}>; +}>; const BEFORE_SLASH_RE = /^(.*)[\\/]/; diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js index 552bc21c9fd2c5..e555991536b52a 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js @@ -9,7 +9,7 @@ */ import type {StackFrame} from '../../Core/NativeExceptionsManager'; -import type {PressEvent} from '../../Types/CoreEventTypes'; +import type {GestureResponderEvent} from '../../Types/CoreEventTypes'; import View from '../../Components/View/View'; import StyleSheet from '../../StyleSheet/StyleSheet'; @@ -21,7 +21,7 @@ import * as React from 'react'; type Props = $ReadOnly<{ frame: StackFrame, - onPress?: ?(event: PressEvent) => void, + onPress?: ?(event: GestureResponderEvent) => void, }>; function LogBoxInspectorStackFrame(props: Props): React.Node { diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrames.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrames.js index e82d65093a4a0f..b3473bc03c1772 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrames.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorStackFrames.js @@ -138,7 +138,7 @@ function StackFrameList(props: { } function StackFrameFooter( - props: $TEMPORARY$object<{message: string, onPress: () => void}>, + props: $ReadOnly<{message: string, onPress: () => void}>, ) { return ( diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxMessage.js b/packages/react-native/Libraries/LogBox/UI/LogBoxMessage.js index 096960c0e68cf9..1e1fcc24e8ea6b 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxMessage.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxMessage.js @@ -59,7 +59,7 @@ function TappableLinks(props: { // URLs were detected. Construct array of Text nodes. - let fragments: Array = []; + const fragments: Array = []; let indexCounter = 0; let startIndex = 0; @@ -115,7 +115,7 @@ function LogBoxMessage(props: Props): React.Node { const elements = []; let length = 0; const createUnderLength = ( - key: string | $TEMPORARY$string<'-1'>, + key: string, message: string, style: void | TextStyleProp, ) => { diff --git a/packages/react-native/Libraries/Modal/Modal.js b/packages/react-native/Libraries/Modal/Modal.js index 02f579a8d0f90c..b5f6f6e5932118 100644 --- a/packages/react-native/Libraries/Modal/Modal.js +++ b/packages/react-native/Libraries/Modal/Modal.js @@ -17,7 +17,7 @@ import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import ModalInjection from './ModalInjection'; import NativeModalManager from './NativeModalManager'; import RCTModalHostView from './RCTModalHostViewNativeComponent'; -import {VirtualizedListContextResetter} from '@react-native-mac/virtualized-lists'; // [macOS] +import {VirtualizedListContextResetter} from '@react-native/virtualized-lists'; // [macOS] const ScrollView = require('../Components/ScrollView/ScrollView'); const View = require('../Components/View/View'); @@ -95,6 +95,14 @@ export type Props = $ReadOnly<{| */ statusBarTranslucent?: ?boolean, + /** + * The `navigationBarTranslucent` prop determines whether your modal should go under + * the system navigationbar. + * + * See https://reactnative.dev/docs/modal.html#navigationbartranslucent-android + */ + navigationBarTranslucent?: ?boolean, + /** * The `hardwareAccelerated` prop controls whether to force hardware * acceleration for the underlying window. @@ -157,6 +165,12 @@ export type Props = $ReadOnly<{| * See https://reactnative.dev/docs/modal#onorientationchange */ onOrientationChange?: ?DirectEventHandler, + + /** + * The `backdropColor` props sets the background color of the modal's container. + * Defaults to `white` if not provided and transparent is `false`. Ignored if `transparent` is `true`. + */ + backdropColor?: ?string, |}>; function confirmProps(props: Props) { @@ -170,6 +184,14 @@ function confirmProps(props: Props) { `Modal with '${props.presentationStyle}' presentation style and 'transparent' value is not supported.`, ); } + if ( + props.navigationBarTranslucent === true && + props.statusBarTranslucent !== true + ) { + console.warn( + 'Modal with translucent navigation bar and without translucent status bar is not supported.', + ); + } } } @@ -218,6 +240,9 @@ class Modal extends React.Component { } componentWillUnmount() { + if (Platform.OS === 'ios') { + this.setState({isRendered: false}); + } if (this._eventSubscription) { this._eventSubscription.remove(); } @@ -249,7 +274,9 @@ class Modal extends React.Component { const containerStyles = { backgroundColor: - this.props.transparent === true ? 'transparent' : 'white', + this.props.transparent === true + ? 'transparent' + : this.props.backdropColor ?? 'white', }; let animationType = this.props.animationType || 'none'; @@ -290,6 +317,7 @@ class Modal extends React.Component { onDismiss={onDismiss} visible={this.props.visible} statusBarTranslucent={this.props.statusBarTranslucent} + navigationBarTranslucent={this.props.navigationBarTranslucent} identifier={this._identifier} style={styles.modal} // $FlowFixMe[method-unbinding] added when improving typing for this parameters @@ -331,8 +359,7 @@ const styles = StyleSheet.create({ }, }); -const ExportedModal: React.AbstractComponent< - React.ElementConfig, -> = ModalInjection.unstable_Modal ?? Modal; +const ExportedModal: React.ComponentType> = + ModalInjection.unstable_Modal ?? Modal; module.exports = ExportedModal; diff --git a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm index 152a3bdbbe1c84..aa8f5774682df1 100644 --- a/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm +++ b/packages/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.mm @@ -57,7 +57,7 @@ @implementation RCTNativeAnimatedNodesManager { } - (instancetype)initWithBridge:(nullable RCTBridge *)bridge - surfacePresenter:(id)surfacePresenter; + surfacePresenter:(id)surfacePresenter { if ((self = [super init])) { _bridge = bridge; diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index bceab558d60ee6..d104a495f79e2b 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -227,6 +227,7 @@ const validAttributesForNonEventProps = { justifyContent: true, overflow: true, display: true, + boxSizing: true, margin: true, marginBlock: true, @@ -268,6 +269,11 @@ const validAttributesForNonEventProps = { borderLeftWidth: true, borderRightWidth: true, + outlineColor: {process: require('../StyleSheet/processColor').default}, + outlineOffset: true, + outlineStyle: true, + outlineWidth: true, + start: true, end: true, left: true, @@ -287,7 +293,70 @@ const validAttributesForNonEventProps = { style: ReactNativeStyleAttributes, - experimental_layoutConformance: true, + // ReactClippingViewManager @ReactProps + removeClippedSubviews: true, + + // ReactViewManager @ReactProps + accessible: true, + hasTVPreferredFocus: true, + nextFocusDown: true, + nextFocusForward: true, + nextFocusLeft: true, + nextFocusRight: true, + nextFocusUp: true, + + borderRadius: true, + borderTopLeftRadius: true, + borderTopRightRadius: true, + borderBottomRightRadius: true, + borderBottomLeftRadius: true, + borderTopStartRadius: true, + borderTopEndRadius: true, + borderBottomStartRadius: true, + borderBottomEndRadius: true, + borderEndEndRadius: true, + borderEndStartRadius: true, + borderStartEndRadius: true, + borderStartStartRadius: true, + borderStyle: true, + hitSlop: true, + pointerEvents: true, + nativeBackgroundAndroid: true, + nativeForegroundAndroid: true, + needsOffscreenAlphaCompositing: true, + + borderColor: { + process: require('../StyleSheet/processColor').default, + }, + borderLeftColor: { + process: require('../StyleSheet/processColor').default, + }, + borderRightColor: { + process: require('../StyleSheet/processColor').default, + }, + borderTopColor: { + process: require('../StyleSheet/processColor').default, + }, + borderBottomColor: { + process: require('../StyleSheet/processColor').default, + }, + borderStartColor: { + process: require('../StyleSheet/processColor').default, + }, + borderEndColor: { + process: require('../StyleSheet/processColor').default, + }, + borderBlockColor: { + process: require('../StyleSheet/processColor').default, + }, + borderBlockEndColor: { + process: require('../StyleSheet/processColor').default, + }, + borderBlockStartColor: { + process: require('../StyleSheet/processColor').default, + }, + focusable: true, + backfaceVisibility: true, }; // Props for bubbling and direct events diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js index d2259be0f8e94a..57f37818a327c8 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js @@ -339,6 +339,7 @@ const validAttributesForNonEventProps = { alignContent: true, position: true, aspectRatio: true, + boxSizing: true, // Also declared as ViewProps // overflow: true, @@ -347,8 +348,6 @@ const validAttributesForNonEventProps = { direction: true, style: ReactNativeStyleAttributes, - - experimental_layoutConformance: true, }; // Props for bubbling and direct events diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js index cd4007a16af4fe..fe95654cd3f214 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js @@ -8,20 +8,185 @@ * @flow strict-local */ -// [macOS] - import type {PartialViewConfigWithoutName} from './PlatformBaseViewConfig'; -/* $FlowFixMe allow macOS to share iOS file */ -import PlatformBaseViewConfigIos from './BaseViewConfig.ios'; -import {ConditionallyIgnoredEventHandlers} from './ViewConfigIgnore'; +import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags'; +import NativeReactNativeFeatureFlags from '../../src/private/featureflags/specs/NativeReactNativeFeatureFlags'; +import ReactNativeStyleAttributes from '../Components/View/ReactNativeStyleAttributes'; +import { + ConditionallyIgnoredEventHandlers, + DynamicallyInjectedByGestureHandler, +} from './ViewConfigIgnore'; const bubblingEventTypes = { - ...PlatformBaseViewConfigIos.bubblingEventTypes, + // Generic Events + topPress: { + phasedRegistrationNames: { + bubbled: 'onPress', + captured: 'onPressCapture', + }, + }, + topChange: { + phasedRegistrationNames: { + bubbled: 'onChange', + captured: 'onChangeCapture', + }, + }, + topFocus: { + phasedRegistrationNames: { + bubbled: 'onFocus', + captured: 'onFocusCapture', + }, + }, + topBlur: { + phasedRegistrationNames: { + bubbled: 'onBlur', + captured: 'onBlurCapture', + }, + }, + topSubmitEditing: { + phasedRegistrationNames: { + bubbled: 'onSubmitEditing', + captured: 'onSubmitEditingCapture', + }, + }, + topEndEditing: { + phasedRegistrationNames: { + bubbled: 'onEndEditing', + captured: 'onEndEditingCapture', + }, + }, + topKeyPress: { + phasedRegistrationNames: { + bubbled: 'onKeyPress', + captured: 'onKeyPressCapture', + }, + }, + + // Touch Events + topTouchStart: { + phasedRegistrationNames: { + bubbled: 'onTouchStart', + captured: 'onTouchStartCapture', + }, + }, + topTouchMove: { + phasedRegistrationNames: { + bubbled: 'onTouchMove', + captured: 'onTouchMoveCapture', + }, + }, + topTouchCancel: { + phasedRegistrationNames: { + bubbled: 'onTouchCancel', + captured: 'onTouchCancelCapture', + }, + }, + topTouchEnd: { + phasedRegistrationNames: { + bubbled: 'onTouchEnd', + captured: 'onTouchEndCapture', + }, + }, + + // Experimental/Work in Progress Pointer Events (not yet ready for use) + topClick: { + phasedRegistrationNames: { + captured: 'onClickCapture', + bubbled: 'onClick', + }, + }, + topPointerCancel: { + phasedRegistrationNames: { + captured: 'onPointerCancelCapture', + bubbled: 'onPointerCancel', + }, + }, + topPointerDown: { + phasedRegistrationNames: { + captured: 'onPointerDownCapture', + bubbled: 'onPointerDown', + }, + }, + topPointerMove: { + phasedRegistrationNames: { + captured: 'onPointerMoveCapture', + bubbled: 'onPointerMove', + }, + }, + topPointerUp: { + phasedRegistrationNames: { + captured: 'onPointerUpCapture', + bubbled: 'onPointerUp', + }, + }, + topPointerEnter: { + phasedRegistrationNames: { + captured: 'onPointerEnterCapture', + bubbled: 'onPointerEnter', + skipBubbling: true, + }, + }, + topPointerLeave: { + phasedRegistrationNames: { + captured: 'onPointerLeaveCapture', + bubbled: 'onPointerLeave', + skipBubbling: true, + }, + }, + topPointerOver: { + phasedRegistrationNames: { + captured: 'onPointerOverCapture', + bubbled: 'onPointerOver', + }, + }, + topPointerOut: { + phasedRegistrationNames: { + captured: 'onPointerOutCapture', + bubbled: 'onPointerOut', + }, + }, + topGotPointerCapture: { + phasedRegistrationNames: { + captured: 'onGotPointerCaptureCapture', + bubbled: 'onGotPointerCapture', + }, + }, + topLostPointerCapture: { + phasedRegistrationNames: { + captured: 'onLostPointerCaptureCapture', + bubbled: 'onLostPointerCapture', + }, + }, }; const directEventTypes = { - ...PlatformBaseViewConfigIos.directEventTypes, + topAccessibilityAction: { + registrationName: 'onAccessibilityAction', + }, + topAccessibilityTap: { + registrationName: 'onAccessibilityTap', + }, + topMagicTap: { + registrationName: 'onMagicTap', + }, + topAccessibilityEscape: { + registrationName: 'onAccessibilityEscape', + }, + topLayout: { + registrationName: 'onLayout', + }, + onGestureHandlerEvent: DynamicallyInjectedByGestureHandler({ + registrationName: 'onGestureHandlerEvent', + }), + onGestureHandlerStateChange: DynamicallyInjectedByGestureHandler({ + registrationName: 'onGestureHandlerStateChange', + }), + + // macOS-specific events + topDoubleClick: { + registrationName: 'onDoubleClick', + }, topDragEnter: { registrationName: 'onDragEnter', }, @@ -60,6 +225,51 @@ const validAttributesForNonEventProps = { // Props for bubbling and direct events const validAttributesForEventProps = ConditionallyIgnoredEventHandlers({ + onLayout: true, + onMagicTap: true, + + // Accessibility + onAccessibilityAction: true, + onAccessibilityEscape: true, + onAccessibilityTap: true, + + // PanResponder handlers + onMoveShouldSetResponder: true, + onMoveShouldSetResponderCapture: true, + onStartShouldSetResponder: true, + onStartShouldSetResponderCapture: true, + onResponderGrant: true, + onResponderReject: true, + onResponderStart: true, + onResponderEnd: true, + onResponderRelease: true, + onResponderMove: true, + onResponderTerminate: true, + onResponderTerminationRequest: true, + onShouldBlockNativeResponder: true, + + // Touch events + onTouchStart: true, + onTouchMove: true, + onTouchEnd: true, + onTouchCancel: true, + + // Pointer events + onClick: true, + onClickCapture: true, + onPointerUp: true, + onPointerDown: true, + onPointerCancel: true, + onPointerEnter: true, + onPointerMove: true, + onPointerLeave: true, + onPointerOver: true, + onPointerOut: true, + onGotPointerCapture: true, + onLostPointerCapture: true, + + // macOS-specific events + onDoubleClick: true, onBlur: true, onDragEnter: true, onDragLeave: true, @@ -72,18 +282,16 @@ const validAttributesForEventProps = ConditionallyIgnoredEventHandlers({ }); /** - * On macOS, view managers define all of a component's props. + * On iOS, view managers define all of a component's props. * All view managers extend RCTViewManager, and RCTViewManager declares these props. */ -const PlatformBaseViewConfigMacOS: PartialViewConfigWithoutName = { +const PlatformBaseViewConfigIos: PartialViewConfigWithoutName = { bubblingEventTypes, directEventTypes, validAttributes: { - ...PlatformBaseViewConfigIos.validAttributes, ...validAttributesForNonEventProps, - // $FlowFixMe[exponential-spread] ...validAttributesForEventProps, }, }; -export default PlatformBaseViewConfigMacOS; +export default PlatformBaseViewConfigIos; diff --git a/packages/react-native/Libraries/NativeComponent/NativeComponentRegistry.js b/packages/react-native/Libraries/NativeComponent/NativeComponentRegistry.js index 68695660da1312..a8842884bf6807 100644 --- a/packages/react-native/Libraries/NativeComponent/NativeComponentRegistry.js +++ b/packages/react-native/Libraries/NativeComponent/NativeComponentRegistry.js @@ -48,7 +48,7 @@ export function setRuntimeConfigProvider( * The supplied `viewConfigProvider` may or may not be invoked and utilized, * depending on how `setRuntimeConfigProvider` is configured. */ -export function get( +export function get( name: string, viewConfigProvider: () => PartialViewConfig, ): HostComponent { @@ -121,10 +121,10 @@ export function get( * that the return value of this is not `HostComponent` because the returned * component instance is not guaranteed to have native methods. */ -export function getWithFallback_DEPRECATED( +export function getWithFallback_DEPRECATED( name: string, viewConfigProvider: () => PartialViewConfig, -): React.AbstractComponent { +): React.ComponentType { if (getRuntimeConfig == null) { // `getRuntimeConfig == null` when static view configs are disabled // If `setRuntimeConfigProvider` is not configured, use native reflection. diff --git a/packages/react-native/Libraries/NativeComponent/StaticViewConfigValidator.js b/packages/react-native/Libraries/NativeComponent/StaticViewConfigValidator.js index 7251e319f776f0..ef34238fdb6ffa 100644 --- a/packages/react-native/Libraries/NativeComponent/StaticViewConfigValidator.js +++ b/packages/react-native/Libraries/NativeComponent/StaticViewConfigValidator.js @@ -9,7 +9,6 @@ */ import {type ViewConfig} from '../Renderer/shims/ReactNativeTypes'; -import {isIgnored} from './ViewConfigIgnore'; export type Difference = | { diff --git a/packages/react-native/Libraries/Network/FormData.js b/packages/react-native/Libraries/Network/FormData.js index 91735c6b033b19..b237b45ae24344 100644 --- a/packages/react-native/Libraries/Network/FormData.js +++ b/packages/react-native/Libraries/Network/FormData.js @@ -28,6 +28,15 @@ type FormDataPart = ... }; +/** + * Encode a FormData filename compliant with RFC 2183 + * + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#directives + */ +function encodeFilename(filename: string): string { + return encodeURIComponent(filename.replace(/\//g, '_')); +} + /** * Polyfill for XMLHttpRequest2 FormData API, allowing multipart POST requests * with mixed data (string, native files) to be submitted via XMLHttpRequest. @@ -82,9 +91,8 @@ class FormData { // content type (cf. web Blob interface.) if (typeof value === 'object' && !Array.isArray(value) && value) { if (typeof value.name === 'string') { - headers['content-disposition'] += `; filename="${ - value.name - }"; filename*=utf-8''${encodeURI(value.name)}`; + headers['content-disposition'] += + `; filename="${encodeFilename(value.name)}"`; } if (typeof value.type === 'string') { headers['content-type'] = value.type; diff --git a/packages/react-native/Libraries/Network/RCTNetworking.android.js b/packages/react-native/Libraries/Network/RCTNetworking.android.js index f45d2e284036e3..d5d782a21004d4 100644 --- a/packages/react-native/Libraries/Network/RCTNetworking.android.js +++ b/packages/react-native/Libraries/Network/RCTNetworking.android.js @@ -8,7 +8,9 @@ * @flow */ +import type {EventSubscription} from '../vendor/emitter/EventEmitter'; import type {RequestBody} from './convertRequestBody'; +import type {RCTNetworkingEventDefinitions} from './RCTNetworkingEventDefinitions.flow'; import type {NativeResponseType} from './XMLHttpRequest'; // Do not require the native RCTNetworking module directly! Use this wrapper module instead. @@ -35,19 +37,25 @@ function generateRequestId(): number { return _requestId++; } +const emitter = new NativeEventEmitter<$FlowFixMe>( + // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior + // If you want to use the native module on other platforms, please remove this condition and test its behavior + Platform.OS !== 'ios' ? null : NativeNetworkingAndroid, +); + /** - * This class is a wrapper around the native RCTNetworking module. It adds a necessary unique + * This object is a wrapper around the native RCTNetworking module. It adds a necessary unique * requestId to each network request that can be used to abort that request later on. */ -// FIXME: use typed events -class RCTNetworking extends NativeEventEmitter<$FlowFixMe> { - constructor() { - super( - // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior - // If you want to use the native module on other platforms, please remove this condition and test its behavior - Platform.OS !== 'ios' ? null : NativeNetworkingAndroid, - ); - } +const RCTNetworking = { + addListener>( + eventType: K, + listener: (...$ElementType) => mixed, + context?: mixed, + ): EventSubscription { + // $FlowFixMe[incompatible-call] + return emitter.addListener(eventType, listener, context); + }, sendRequest( method: string, @@ -81,15 +89,15 @@ class RCTNetworking extends NativeEventEmitter<$FlowFixMe> { withCredentials, ); callback(requestId); - } + }, abortRequest(requestId: number) { NativeNetworkingAndroid.abortRequest(requestId); - } + }, - clearCookies(callback: (result: boolean) => any) { + clearCookies(callback: (result: boolean) => void) { NativeNetworkingAndroid.clearCookies(callback); - } -} + }, +}; -export default (new RCTNetworking(): RCTNetworking); +export default RCTNetworking; diff --git a/packages/react-native/Libraries/Network/RCTNetworking.ios.js b/packages/react-native/Libraries/Network/RCTNetworking.ios.js index 4b49b505695e44..e5b2dd572ff740 100644 --- a/packages/react-native/Libraries/Network/RCTNetworking.ios.js +++ b/packages/react-native/Libraries/Network/RCTNetworking.ios.js @@ -14,54 +14,9 @@ import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import convertRequestBody, {type RequestBody} from './convertRequestBody'; import NativeNetworkingIOS from './NativeNetworkingIOS'; +import {type RCTNetworkingEventDefinitions} from './RCTNetworkingEventDefinitions.flow'; import {type NativeResponseType} from './XMLHttpRequest'; -type RCTNetworkingEventDefinitions = $ReadOnly<{ - didSendNetworkData: [ - [ - number, // requestId - number, // progress - number, // total - ], - ], - didReceiveNetworkResponse: [ - [ - number, // requestId - number, // status - ?{[string]: string}, // responseHeaders - ?string, // responseURL - ], - ], - didReceiveNetworkData: [ - [ - number, // requestId - string, // response - ], - ], - didReceiveNetworkIncrementalData: [ - [ - number, // requestId - string, // responseText - number, // progress - number, // total - ], - ], - didReceiveNetworkDataProgress: [ - [ - number, // requestId - number, // loaded - number, // total - ], - ], - didCompleteNetworkResponse: [ - [ - number, // requestId - string, // error - boolean, // timeOutError - ], - ], -}>; - const RCTNetworking = { addListener>( eventType: K, @@ -74,7 +29,7 @@ const RCTNetworking = { sendRequest( method: string, - trackingName: string, + trackingName: ?string, url: string, headers: {...}, data: RequestBody, diff --git a/packages/react-native/Libraries/Network/RCTNetworking.js.flow b/packages/react-native/Libraries/Network/RCTNetworking.js.flow index 45006af0ef16bf..077f58e21a8184 100644 --- a/packages/react-native/Libraries/Network/RCTNetworking.js.flow +++ b/packages/react-native/Libraries/Network/RCTNetworking.js.flow @@ -13,52 +13,7 @@ import type {EventSubscription} from '../vendor/emitter/EventEmitter'; import type {RequestBody} from './convertRequestBody'; import type {NativeResponseType} from './XMLHttpRequest'; - -type RCTNetworkingEventDefinitions = $ReadOnly<{ - didSendNetworkData: [ - [ - number, // requestId - number, // progress - number, // total - ], - ], - didReceiveNetworkResponse: [ - [ - number, // requestId - number, // status - ?{[string]: string}, // responseHeaders - ?string, // responseURL - ], - ], - didReceiveNetworkData: [ - [ - number, // requestId - string, // response - ], - ], - didReceiveNetworkIncrementalData: [ - [ - number, // requestId - string, // responseText - number, // progress - number, // total - ], - ], - didReceiveNetworkDataProgress: [ - [ - number, // requestId - number, // loaded - number, // total - ], - ], - didCompleteNetworkResponse: [ - [ - number, // requestId - string, // error - boolean, // timeOutError - ], - ], -}>; +import type {RCTNetworkingEventDefinitions} from './RCTNetworkingEventDefinitions.flow'; declare const RCTNetworking: interface { addListener>( diff --git a/packages/react-native/Libraries/Network/RCTNetworking.macos.js b/packages/react-native/Libraries/Network/RCTNetworking.macos.js index a05a885a336bc4..4b49b505695e44 100644 --- a/packages/react-native/Libraries/Network/RCTNetworking.macos.js +++ b/packages/react-native/Libraries/Network/RCTNetworking.macos.js @@ -1,15 +1,112 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * 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. * + * @flow strict-local * @format - * @flow */ -// [macOS] +'use strict'; -/* $FlowFixMe allow macOS to share iOS file */ -const RCTNetworking = require('./RCTNetworking.ios'); -module.exports = RCTNetworking; +import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; +import {type EventSubscription} from '../vendor/emitter/EventEmitter'; +import convertRequestBody, {type RequestBody} from './convertRequestBody'; +import NativeNetworkingIOS from './NativeNetworkingIOS'; +import {type NativeResponseType} from './XMLHttpRequest'; + +type RCTNetworkingEventDefinitions = $ReadOnly<{ + didSendNetworkData: [ + [ + number, // requestId + number, // progress + number, // total + ], + ], + didReceiveNetworkResponse: [ + [ + number, // requestId + number, // status + ?{[string]: string}, // responseHeaders + ?string, // responseURL + ], + ], + didReceiveNetworkData: [ + [ + number, // requestId + string, // response + ], + ], + didReceiveNetworkIncrementalData: [ + [ + number, // requestId + string, // responseText + number, // progress + number, // total + ], + ], + didReceiveNetworkDataProgress: [ + [ + number, // requestId + number, // loaded + number, // total + ], + ], + didCompleteNetworkResponse: [ + [ + number, // requestId + string, // error + boolean, // timeOutError + ], + ], +}>; + +const RCTNetworking = { + addListener>( + eventType: K, + listener: (...$ElementType) => mixed, + context?: mixed, + ): EventSubscription { + // $FlowFixMe[incompatible-call] + return RCTDeviceEventEmitter.addListener(eventType, listener, context); + }, + + sendRequest( + method: string, + trackingName: string, + url: string, + headers: {...}, + data: RequestBody, + responseType: NativeResponseType, + incrementalUpdates: boolean, + timeout: number, + callback: (requestId: number) => void, + withCredentials: boolean, + ) { + const body = convertRequestBody(data); + NativeNetworkingIOS.sendRequest( + { + method, + url, + data: {...body, trackingName}, + headers, + responseType, + incrementalUpdates, + timeout, + withCredentials, + }, + callback, + ); + }, + + abortRequest(requestId: number) { + NativeNetworkingIOS.abortRequest(requestId); + }, + + clearCookies(callback: (result: boolean) => void) { + NativeNetworkingIOS.clearCookies(callback); + }, +}; + +export default RCTNetworking; diff --git a/packages/react-native/Libraries/Network/XHRInterceptor.js b/packages/react-native/Libraries/Network/XHRInterceptor.js index 11fe55c531c800..cae26b4fd1e564 100644 --- a/packages/react-native/Libraries/Network/XHRInterceptor.js +++ b/packages/react-native/Libraries/Network/XHRInterceptor.js @@ -5,20 +5,57 @@ * LICENSE file in the root directory of this source tree. * * @format + * @flow strict-local */ 'use strict'; const XMLHttpRequest = require('./XMLHttpRequest'); +// $FlowFixMe[method-unbinding] const originalXHROpen = XMLHttpRequest.prototype.open; +// $FlowFixMe[method-unbinding] const originalXHRSend = XMLHttpRequest.prototype.send; +// $FlowFixMe[method-unbinding] const originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; -let openCallback; -let sendCallback; -let requestHeaderCallback; -let headerReceivedCallback; -let responseCallback; +type XHRInterceptorOpenCallback = ( + method: string, + url: string, + request: XMLHttpRequest, +) => void; + +type XHRInterceptorSendCallback = ( + data: string, + request: XMLHttpRequest, +) => void; + +type XHRInterceptorRequestHeaderCallback = ( + header: string, + value: string, + request: XMLHttpRequest, +) => void; + +type XHRInterceptorHeaderReceivedCallback = ( + responseContentType: string | void, + responseSize: number | void, + allHeaders: string, + request: XMLHttpRequest, +) => void; + +type XHRInterceptorResponseCallback = ( + status: number, + timeout: number, + response: string, + responseURL: string, + responseType: string, + request: XMLHttpRequest, +) => void; + +let openCallback: XHRInterceptorOpenCallback | null; +let sendCallback: XHRInterceptorSendCallback | null; +let requestHeaderCallback: XHRInterceptorRequestHeaderCallback | null; +let headerReceivedCallback: XHRInterceptorHeaderReceivedCallback | null; +let responseCallback: XHRInterceptorResponseCallback | null; let isInterceptorEnabled = false; @@ -33,39 +70,39 @@ const XHRInterceptor = { /** * Invoked before XMLHttpRequest.open(...) is called. */ - setOpenCallback(callback) { + setOpenCallback(callback: XHRInterceptorOpenCallback) { openCallback = callback; }, /** * Invoked before XMLHttpRequest.send(...) is called. */ - setSendCallback(callback) { + setSendCallback(callback: XHRInterceptorSendCallback) { sendCallback = callback; }, /** * Invoked after xhr's readyState becomes xhr.HEADERS_RECEIVED. */ - setHeaderReceivedCallback(callback) { + setHeaderReceivedCallback(callback: XHRInterceptorHeaderReceivedCallback) { headerReceivedCallback = callback; }, /** * Invoked after xhr's readyState becomes xhr.DONE. */ - setResponseCallback(callback) { + setResponseCallback(callback: XHRInterceptorResponseCallback) { responseCallback = callback; }, /** * Invoked before XMLHttpRequest.setRequestHeader(...) is called. */ - setRequestHeaderCallback(callback) { + setRequestHeaderCallback(callback: XHRInterceptorRequestHeaderCallback) { requestHeaderCallback = callback; }, - isInterceptorEnabled() { + isInterceptorEnabled(): boolean { return isInterceptorEnabled; }, @@ -75,7 +112,9 @@ const XHRInterceptor = { } // Override `open` method for all XHR requests to intercept the request // method and url, then pass them through the `openCallback`. - XMLHttpRequest.prototype.open = function (method, url) { + // $FlowFixMe[cannot-write] + // $FlowFixMe[missing-this-annot] + XMLHttpRequest.prototype.open = function (method: string, url: string) { if (openCallback) { openCallback(method, url, this); } @@ -84,7 +123,12 @@ const XHRInterceptor = { // Override `setRequestHeader` method for all XHR requests to intercept // the request headers, then pass them through the `requestHeaderCallback`. - XMLHttpRequest.prototype.setRequestHeader = function (header, value) { + // $FlowFixMe[cannot-write] + // $FlowFixMe[missing-this-annot] + XMLHttpRequest.prototype.setRequestHeader = function ( + header: string, + value: string, + ) { if (requestHeaderCallback) { requestHeaderCallback(header, value, this); } @@ -93,7 +137,9 @@ const XHRInterceptor = { // Override `send` method of all XHR requests to intercept the data sent, // register listeners to intercept the response, and invoke the callbacks. - XMLHttpRequest.prototype.send = function (data) { + // $FlowFixMe[cannot-write] + // $FlowFixMe[missing-this-annot] + XMLHttpRequest.prototype.send = function (data: string) { if (sendCallback) { sendCallback(data, this); } @@ -151,8 +197,11 @@ const XHRInterceptor = { return; } isInterceptorEnabled = false; + // $FlowFixMe[cannot-write] XMLHttpRequest.prototype.send = originalXHRSend; + // $FlowFixMe[cannot-write] XMLHttpRequest.prototype.open = originalXHROpen; + // $FlowFixMe[cannot-write] XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader; responseCallback = null; openCallback = null; diff --git a/packages/react-native/Libraries/Network/XMLHttpRequest.js b/packages/react-native/Libraries/Network/XMLHttpRequest.js index 458f0780a0f9a8..7697233ae0aa82 100644 --- a/packages/react-native/Libraries/Network/XMLHttpRequest.js +++ b/packages/react-native/Libraries/Network/XMLHttpRequest.js @@ -22,6 +22,7 @@ const base64 = require('base64-js'); const invariant = require('invariant'); const DEBUG_NETWORK_SEND_DELAY: false = false; // Set to a number of milliseconds when debugging +const LABEL_FOR_MISSING_URL_FOR_PROFILING = 'Unknown URL'; export type NativeResponseType = 'base64' | 'blob' | 'text'; export type ResponseType = @@ -101,6 +102,7 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { static DONE: number = DONE; static _interceptor: ?XHRInterceptor = null; + static _profiling: boolean = false; UNSENT: number = UNSENT; OPENED: number = OPENED; @@ -144,12 +146,17 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { _timedOut: boolean = false; _trackingName: string = 'unknown'; _incrementalEvents: boolean = false; + _startTime: ?number = null; _performanceLogger: IPerformanceLogger = GlobalPerformanceLogger; static setInterceptor(interceptor: ?XHRInterceptor) { XMLHttpRequest._interceptor = interceptor; } + static enableProfiling(enableProfiling: boolean): void { + XMLHttpRequest._profiling = enableProfiling; + } + constructor() { super(); this._reset(); @@ -356,6 +363,11 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { this._response += responseText; } + if (XMLHttpRequest._profiling) { + performance.mark( + 'Track:XMLHttpRequest:Incremental Data: ' + this._getMeasureURL(), + ); + } XMLHttpRequest._interceptor && XMLHttpRequest._interceptor.dataReceived(requestId, responseText); @@ -398,7 +410,13 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { this._clearSubscriptions(); this._requestId = null; this.setReadyState(this.DONE); - + if (XMLHttpRequest._profiling && this._startTime != null) { + const start = this._startTime; + performance.measure('Track:XMLHttpRequest:' + this._getMeasureURL(), { + start, + end: performance.now(), + }); + } if (error) { XMLHttpRequest._interceptor && XMLHttpRequest._interceptor.loadingFailed(requestId, error); @@ -572,6 +590,7 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { this._trackingName !== 'unknown' ? this._trackingName : this._url; this._perfKey = 'network_XMLHttpRequest_' + String(friendlyName); this._performanceLogger.startTimespan(this._perfKey); + this._startTime = performance.now(); invariant( this._method, 'XMLHttpRequest method needs to be defined (%s).', @@ -668,6 +687,12 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): typeof EventTarget) { } super.addEventListener(type, listener); } + + _getMeasureURL(): string { + return ( + this._trackingName ?? this._url ?? LABEL_FOR_MISSING_URL_FOR_PROFILING + ); + } } module.exports = XMLHttpRequest; diff --git a/packages/react-native/Libraries/NewAppScreen/components/HermesBadge.js b/packages/react-native/Libraries/NewAppScreen/components/HermesBadge.js index 8522aebd2c971d..0d2a8dea595772 100644 --- a/packages/react-native/Libraries/NewAppScreen/components/HermesBadge.js +++ b/packages/react-native/Libraries/NewAppScreen/components/HermesBadge.js @@ -40,8 +40,8 @@ const HermesBadge = (): Node => { const styles = StyleSheet.create({ badge: { position: 'absolute', - top: 8, right: 12, + bottom: 8, }, badgeText: { fontSize: 14, diff --git a/packages/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js b/packages/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js index fa9497607b4423..3a1964886a5e15 100644 --- a/packages/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js +++ b/packages/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js @@ -88,7 +88,7 @@ const PERMISSIONS = Object.freeze({ */ class PermissionsAndroid { - PERMISSIONS: {| + PERMISSIONS: $ReadOnly<{| ACCEPT_HANDOVER: string, ACCESS_BACKGROUND_LOCATION: string, ACCESS_COARSE_LOCATION: string, @@ -132,12 +132,12 @@ class PermissionsAndroid { WRITE_CALL_LOG: string, WRITE_CONTACTS: string, WRITE_EXTERNAL_STORAGE: string, - |} = PERMISSIONS; - RESULTS: {| + |}> = PERMISSIONS; + RESULTS: $ReadOnly<{| DENIED: 'denied', GRANTED: 'granted', NEVER_ASK_AGAIN: 'never_ask_again', - |} = PERMISSION_REQUEST_RESULT; + |}> = PERMISSION_REQUEST_RESULT; /** * DEPRECATED - use check diff --git a/packages/react-native/Libraries/Pressability/HoverState.js b/packages/react-native/Libraries/Pressability/HoverState.js index 8e5d6aec69e631..861d1c9e8a7414 100644 --- a/packages/react-native/Libraries/Pressability/HoverState.js +++ b/packages/react-native/Libraries/Pressability/HoverState.js @@ -12,6 +12,8 @@ import Platform from '../Utilities/Platform'; let isEnabled = false; +/* $FlowFixMe[incompatible-type] Error found due to incomplete typing of + * Platform.flow.js */ if (Platform.OS === 'web') { const canUseDOM = Boolean( typeof window !== 'undefined' && diff --git a/packages/react-native/Libraries/Pressability/Pressability.js b/packages/react-native/Libraries/Pressability/Pressability.js index 8102198cc21d2e..15499e3ab61c1c 100644 --- a/packages/react-native/Libraries/Pressability/Pressability.js +++ b/packages/react-native/Libraries/Pressability/Pressability.js @@ -8,13 +8,13 @@ * @format */ -import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; +import type {HostInstance} from '../../src/private/types/HostInstance'; import type { BlurEvent, FocusEvent, + GestureResponderEvent, KeyEvent, MouseEvent, - PressEvent, // [macOS] } from '../Types/CoreEventTypes'; @@ -28,9 +28,8 @@ import {isHoverEnabled} from './HoverState'; import PressabilityPerformanceEventEmitter from './PressabilityPerformanceEventEmitter.js'; import {type PressabilityTouchSignal as TouchSignal} from './PressabilityTypes.js'; import invariant from 'invariant'; -import * as React from 'react'; -export type PressabilityConfig = $ReadOnly<{| +export type PressabilityConfig = $ReadOnly<{ /** * Whether a press gesture can be interrupted by a parent gesture such as a * scroll event. Defaults to true. @@ -127,38 +126,38 @@ export type PressabilityConfig = $ReadOnly<{| /** * Called when a long press gesture has been triggered. */ - onLongPress?: ?(event: PressEvent) => mixed, + onLongPress?: ?(event: GestureResponderEvent) => mixed, /** * Called when a press gesture has been triggered. */ - onPress?: ?(event: PressEvent) => mixed, + onPress?: ?(event: GestureResponderEvent) => mixed, /** * Called when the press is activated to provide visual feedback. */ - onPressIn?: ?(event: PressEvent) => mixed, + onPressIn?: ?(event: GestureResponderEvent) => mixed, /** * Called when the press location moves. (This should rarely be used.) */ - onPressMove?: ?(event: PressEvent) => mixed, + onPressMove?: ?(event: GestureResponderEvent) => mixed, /** * Called when the press is deactivated to undo visual feedback. */ - onPressOut?: ?(event: PressEvent) => mixed, + onPressOut?: ?(event: GestureResponderEvent) => mixed, /** * Whether to prevent any other native components from becoming responder * while this pressable is responder. */ blockNativeResponder?: ?boolean, -|}>; +}>; -export type EventHandlers = $ReadOnly<{| +export type EventHandlers = $ReadOnly<{ onBlur: (event: BlurEvent) => void, - onClick: (event: PressEvent) => void, + onClick: (event: GestureResponderEvent) => void, onFocus: (event: FocusEvent) => void, onKeyDown: (event: KeyEvent) => void, onKeyUp: (event: KeyEvent) => void, @@ -166,13 +165,13 @@ export type EventHandlers = $ReadOnly<{| onMouseLeave?: (event: MouseEvent) => void, onPointerEnter?: (event: PointerEvent) => void, onPointerLeave?: (event: PointerEvent) => void, - onResponderGrant: (event: PressEvent) => void | boolean, - onResponderMove: (event: PressEvent) => void, - onResponderRelease: (event: PressEvent) => void, - onResponderTerminate: (event: PressEvent) => void, + onResponderGrant: (event: GestureResponderEvent) => void | boolean, + onResponderMove: (event: GestureResponderEvent) => void, + onResponderRelease: (event: GestureResponderEvent) => void, + onResponderTerminate: (event: GestureResponderEvent) => void, onResponderTerminationRequest: () => boolean, onStartShouldSetResponder: () => boolean, -|}>; +}>; type TouchState = | 'NOT_RESPONDER' @@ -398,17 +397,17 @@ export default class Pressability { _longPressDelayTimeout: ?TimeoutID = null; _pressDelayTimeout: ?TimeoutID = null; _pressOutDelayTimeout: ?TimeoutID = null; - _responderID: ?number | React.ElementRef> = null; - _responderRegion: ?$ReadOnly<{| + _responderID: ?number | HostInstance = null; + _responderRegion: ?$ReadOnly<{ bottom: number, left: number, right: number, top: number, - |}> = null; - _touchActivatePosition: ?$ReadOnly<{| + }> = null; + _touchActivatePosition: ?$ReadOnly<{ pageX: number, pageY: number, - |}>; + }>; _touchActivateTime: ?number; _touchState: TouchState = 'NOT_RESPONDER'; @@ -471,7 +470,7 @@ export default class Pressability { return !disabled ?? true; }, - onResponderGrant: (event: PressEvent): void | boolean => { + onResponderGrant: (event: GestureResponderEvent): void | boolean => { event.persist(); this._cancelPressOutDelayTimeout(); @@ -501,7 +500,7 @@ export default class Pressability { return this._config.blockNativeResponder === true; }, - onResponderMove: (event: PressEvent): void => { + onResponderMove: (event: GestureResponderEvent): void => { const {onPressMove} = this._config; if (onPressMove != null) { onPressMove(event); @@ -536,11 +535,11 @@ export default class Pressability { } }, - onResponderRelease: (event: PressEvent): void => { + onResponderRelease: (event: GestureResponderEvent): void => { this._receiveSignal('RESPONDER_RELEASE', event); }, - onResponderTerminate: (event: PressEvent): void => { + onResponderTerminate: (event: GestureResponderEvent): void => { this._receiveSignal('RESPONDER_TERMINATED', event); }, @@ -549,7 +548,7 @@ export default class Pressability { return cancelable ?? true; }, - onClick: (event: PressEvent): void => { + onClick: (event: GestureResponderEvent): void => { // If event has `pointerType`, it was emitted from a PointerEvent and // we should ignore it to avoid triggering `onPress` twice. if (event?.nativeEvent?.hasOwnProperty?.('pointerType')) { @@ -727,7 +726,7 @@ export default class Pressability { * Receives a state machine signal, performs side effects of the transition * and stores the new state. Validates the transition as well. */ - _receiveSignal(signal: TouchSignal, event: PressEvent): void { + _receiveSignal(signal: TouchSignal, event: GestureResponderEvent): void { // Especially on iOS, not all events have timestamps associated. // For telemetry purposes, this doesn't matter too much, as long as *some* do. // Since the native timestamp is integral for logging telemetry, just skip @@ -769,7 +768,7 @@ export default class Pressability { prevState: TouchState, nextState: TouchState, signal: TouchSignal, - event: PressEvent, + event: GestureResponderEvent, ): void { if (isTerminalSignal(signal)) { this._touchActivatePosition = null; @@ -825,7 +824,7 @@ export default class Pressability { this._cancelPressDelayTimeout(); } - _activate(event: PressEvent): void { + _activate(event: GestureResponderEvent): void { const {onPressIn} = this._config; const {pageX, pageY} = getTouchFromPressEvent(event); this._touchActivatePosition = {pageX, pageY}; @@ -835,7 +834,7 @@ export default class Pressability { } } - _deactivate(event: PressEvent): void { + _deactivate(event: GestureResponderEvent): void { const {onPressOut} = this._config; if (onPressOut != null) { const minPressDuration = normalizeDelay( @@ -892,13 +891,13 @@ export default class Pressability { }; _isTouchWithinResponderRegion( - touch: $PropertyType, - responderRegion: $ReadOnly<{| + touch: $PropertyType, + responderRegion: $ReadOnly<{ bottom: number, left: number, right: number, top: number, - |}>, + }>, ): boolean { const hitSlop = normalizeRect(this._config.hitSlop); const pressRectOffset = normalizeRect(this._config.pressRectOffset); @@ -937,7 +936,7 @@ export default class Pressability { ); } - _handleLongPress(event: PressEvent): void { + _handleLongPress(event: GestureResponderEvent): void { if ( this._touchState === 'RESPONDER_ACTIVE_PRESS_IN' || this._touchState === 'RESPONDER_ACTIVE_LONG_PRESS_IN' @@ -990,7 +989,7 @@ function normalizeDelay( return Math.max(min, delay ?? fallback); } -const getTouchFromPressEvent = (event: PressEvent) => { +const getTouchFromPressEvent = (event: GestureResponderEvent) => { const {changedTouches, touches} = event.nativeEvent; if (touches != null && touches.length > 0) { diff --git a/packages/react-native/Libraries/Pressability/usePressability.js b/packages/react-native/Libraries/Pressability/usePressability.js index 3f2aebb4461261..9a8f9666c6efc8 100644 --- a/packages/react-native/Libraries/Pressability/usePressability.js +++ b/packages/react-native/Libraries/Pressability/usePressability.js @@ -14,6 +14,9 @@ import Pressability, { } from './Pressability'; import {useEffect, useRef} from 'react'; +declare function usePressability(config: PressabilityConfig): EventHandlers; +declare function usePressability(config: null | void): null | EventHandlers; + /** * Creates a persistent instance of `Pressability` that automatically configures * itself and resets. Accepts null `config` to support lazy initialization. Once @@ -28,7 +31,7 @@ import {useEffect, useRef} from 'react'; */ export default function usePressability( config: ?PressabilityConfig, -): ?EventHandlers { +): null | EventHandlers { const pressabilityRef = useRef(null); if (config != null && pressabilityRef.current == null) { pressabilityRef.current = new Pressability(config); diff --git a/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js b/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js index f51e3db3b58ae9..93e591ec4169ab 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js +++ b/packages/react-native/Libraries/PushNotificationIOS/PushNotificationIOS.js @@ -569,4 +569,4 @@ class PushNotificationIOS { } } -module.exports = PushNotificationIOS; +export default PushNotificationIOS; diff --git a/packages/react-native/Libraries/ReactNative/AppContainer.js b/packages/react-native/Libraries/ReactNative/AppContainer.js index 91a51cd8f48a31..afea7ca8631d01 100644 --- a/packages/react-native/Libraries/ReactNative/AppContainer.js +++ b/packages/react-native/Libraries/ReactNative/AppContainer.js @@ -24,7 +24,7 @@ export type Props = $ReadOnly<{| internal_excludeInspector?: boolean, |}>; -const AppContainer: React.AbstractComponent = __DEV__ +const AppContainer: component(...Props) = __DEV__ ? require('./AppContainer-dev').default : require('./AppContainer-prod').default; diff --git a/packages/react-native/Libraries/ReactNative/AppRegistry.js b/packages/react-native/Libraries/ReactNative/AppRegistry.js index d073995be8c859..204972f1323a21 100644 --- a/packages/react-native/Libraries/ReactNative/AppRegistry.js +++ b/packages/react-native/Libraries/ReactNative/AppRegistry.js @@ -46,7 +46,6 @@ type AppParameters = { initialProps: $ReadOnly<{[string]: mixed, ...}>, rootTag: RootTag, fabric?: boolean, - concurrentRoot?: boolean, }; export type Runnable = ( appParameters: AppParameters, @@ -120,10 +119,6 @@ const AppRegistry = { ): string { const scopedPerformanceLogger = createPerformanceLogger(); runnables[appKey] = (appParameters, displayMode) => { - const concurrentRootEnabled = Boolean( - appParameters.initialProps?.concurrentRoot || - appParameters.concurrentRoot, - ); renderApplication( componentProviderInstrumentationHook( componentProvider, @@ -138,7 +133,6 @@ const AppRegistry = { appKey === 'LogBox', // is logbox appKey, displayMode, - concurrentRootEnabled, ); }; if (section) { diff --git a/packages/react-native/Libraries/ReactNative/DisplayMode.js b/packages/react-native/Libraries/ReactNative/DisplayMode.js index 2bf55dc6976ea5..4de3035833b69b 100644 --- a/packages/react-native/Libraries/ReactNative/DisplayMode.js +++ b/packages/react-native/Libraries/ReactNative/DisplayMode.js @@ -12,7 +12,7 @@ export opaque type DisplayModeType = number; /** DisplayMode should be in sync with the method displayModeToInt from * react/renderer/uimanager/primitives.h. */ -const DisplayMode: {[string]: DisplayModeType} = Object.freeze({ +const DisplayMode: {+[string]: DisplayModeType} = Object.freeze({ VISIBLE: 1, SUSPENDED: 2, HIDDEN: 3, diff --git a/packages/react-native/Libraries/ReactNative/PaperUIManager.js b/packages/react-native/Libraries/ReactNative/PaperUIManager.js index cf18e8e864e94f..b9c827fe206bba 100644 --- a/packages/react-native/Libraries/ReactNative/PaperUIManager.js +++ b/packages/react-native/Libraries/ReactNative/PaperUIManager.js @@ -14,10 +14,11 @@ import type {UIManagerJSInterface} from '../Types/UIManagerJSInterface'; import NativeUIManager from './NativeUIManager'; import nullthrows from 'nullthrows'; -const NativeModules = require('../BatchedBridge/NativeModules'); -const defineLazyObjectProperty = require('../Utilities/defineLazyObjectProperty'); -const Platform = require('../Utilities/Platform'); -const UIManagerProperties = require('./UIManagerProperties'); +const NativeModules = require('../BatchedBridge/NativeModules').default; +const defineLazyObjectProperty = + require('../Utilities/defineLazyObjectProperty').default; +const Platform = require('../Utilities/Platform').default; +const UIManagerProperties = require('./UIManagerProperties').default; const viewManagerConfigs: {[string]: any | null} = {}; @@ -188,4 +189,4 @@ if (!global.nativeCallSyncHook) { }); } -module.exports = UIManagerJS; +export default UIManagerJS; diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js index b7a06c89ed74a5..bfa7ae15392a6b 100644 --- a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricHostComponent.js @@ -9,7 +9,7 @@ */ import type { - HostComponent, + HostInstance, INativeMethods, InternalInstanceHandle, MeasureInWindowOnSuccessCallback, @@ -17,7 +17,6 @@ import type { MeasureOnSuccessCallback, ViewConfig, } from '../../Renderer/shims/ReactNativeTypes'; -import type {ElementRef} from 'react'; import TextInputState from '../../Components/TextInput/TextInputState'; import {getNodeFromInternalInstanceHandle} from '../../ReactNative/RendererProxy'; @@ -85,7 +84,7 @@ export default class ReactFabricHostComponent implements INativeMethods { } measureLayout( - relativeToNativeNode: number | ElementRef>, + relativeToNativeNode: number | HostInstance, onSuccess: MeasureLayoutOnSuccessCallback, onFail?: () => void /* currently unused */, ) { diff --git a/packages/react-native/Libraries/ReactNative/RendererImplementation.js b/packages/react-native/Libraries/ReactNative/RendererImplementation.js index 4c5ce0a6ecd3da..e2d25090794b94 100644 --- a/packages/react-native/Libraries/ReactNative/RendererImplementation.js +++ b/packages/react-native/Libraries/ReactNative/RendererImplementation.js @@ -10,6 +10,7 @@ import type { HostComponent, + HostInstance, InternalInstanceHandle, Node, } from '../Renderer/shims/ReactNativeTypes'; @@ -34,7 +35,7 @@ export function renderElement({ useConcurrentRoot: boolean, }): void { if (useFabric) { - require('../Renderer/shims/ReactFabric').render( + require('../Renderer/shims/ReactFabric').default.render( element, rootTag, null, @@ -46,7 +47,7 @@ export function renderElement({ }, ); } else { - require('../Renderer/shims/ReactNative').render( + require('../Renderer/shims/ReactNative').default.render( element, rootTag, undefined, @@ -61,8 +62,8 @@ export function renderElement({ export function findHostInstance_DEPRECATED( componentOrHandle: ?(ElementRef | number), -): ?ElementRef> { - return require('../Renderer/shims/ReactNative').findHostInstance_DEPRECATED( +): ?HostInstance { + return require('../Renderer/shims/ReactNative').default.findHostInstance_DEPRECATED( componentOrHandle, ); } @@ -70,26 +71,26 @@ export function findHostInstance_DEPRECATED( export function findNodeHandle( componentOrHandle: ?(ElementRef | number), ): ?number { - return require('../Renderer/shims/ReactNative').findNodeHandle( + return require('../Renderer/shims/ReactNative').default.findNodeHandle( componentOrHandle, ); } export function dispatchCommand( - handle: ElementRef>, + handle: HostInstance, command: string, args: Array, ): void { if (global.RN$Bridgeless === true) { // Note: this function has the same implementation in the legacy and new renderer. // However, evaluating the old renderer comes with some side effects. - return require('../Renderer/shims/ReactFabric').dispatchCommand( + return require('../Renderer/shims/ReactFabric').default.dispatchCommand( handle, command, args, ); } else { - return require('../Renderer/shims/ReactNative').dispatchCommand( + return require('../Renderer/shims/ReactNative').default.dispatchCommand( handle, command, args, @@ -98,10 +99,10 @@ export function dispatchCommand( } export function sendAccessibilityEvent( - handle: ElementRef>, + handle: HostInstance, eventType: string, ): void { - return require('../Renderer/shims/ReactNative').sendAccessibilityEvent( + return require('../Renderer/shims/ReactNative').default.sendAccessibilityEvent( handle, eventType, ); @@ -114,7 +115,7 @@ export function sendAccessibilityEvent( export function unmountComponentAtNodeAndRemoveContainer(rootTag: RootTag) { // $FlowExpectedError[incompatible-type] rootTag is an opaque type so we can't really cast it as is. const rootTagAsNumber: number = rootTag; - require('../Renderer/shims/ReactNative').unmountComponentAtNodeAndRemoveContainer( + require('../Renderer/shims/ReactNative').default.unmountComponentAtNodeAndRemoveContainer( rootTagAsNumber, ); } @@ -124,7 +125,7 @@ export function unstable_batchedUpdates( bookkeeping: T, ): void { // This doesn't actually do anything when batching updates for a Fabric root. - return require('../Renderer/shims/ReactNative').unstable_batchedUpdates( + return require('../Renderer/shims/ReactNative').default.unstable_batchedUpdates( fn, bookkeeping, ); @@ -135,10 +136,10 @@ export function isProfilingRenderer(): boolean { } export function isChildPublicInstance( - parentInstance: ReactFabricHostComponent | HostComponent, - childInstance: ReactFabricHostComponent | HostComponent, + parentInstance: ReactFabricHostComponent | HostComponent, + childInstance: ReactFabricHostComponent | HostComponent, ): boolean { - return require('../Renderer/shims/ReactNative').isChildPublicInstance( + return require('../Renderer/shims/ReactNative').default.isChildPublicInstance( parentInstance, childInstance, ); @@ -148,7 +149,7 @@ export function getNodeFromInternalInstanceHandle( internalInstanceHandle: InternalInstanceHandle, ): ?Node { // This is only available in Fabric - return require('../Renderer/shims/ReactFabric').getNodeFromInternalInstanceHandle( + return require('../Renderer/shims/ReactFabric').default.getNodeFromInternalInstanceHandle( internalInstanceHandle, ); } @@ -157,7 +158,7 @@ export function getPublicInstanceFromInternalInstanceHandle( internalInstanceHandle: InternalInstanceHandle, ): mixed /*PublicInstance | PublicTextInstance | null*/ { // This is only available in Fabric - return require('../Renderer/shims/ReactFabric').getPublicInstanceFromInternalInstanceHandle( + return require('../Renderer/shims/ReactFabric').default.getPublicInstanceFromInternalInstanceHandle( internalInstanceHandle, ); } diff --git a/packages/react-native/Libraries/ReactNative/getCachedComponentWithDebugName.js b/packages/react-native/Libraries/ReactNative/getCachedComponentWithDebugName.js index 34aaea169261c0..6b581185d1149f 100644 --- a/packages/react-native/Libraries/ReactNative/getCachedComponentWithDebugName.js +++ b/packages/react-native/Libraries/ReactNative/getCachedComponentWithDebugName.js @@ -8,11 +8,9 @@ * @format */ -import type {AbstractComponent} from 'react'; - import * as React from 'react'; -type NoopComponent = AbstractComponent<{children: React.Node}>; +type NoopComponent = component(children: React.Node); const cache: Map< string, // displayName diff --git a/packages/react-native/Libraries/ReactNative/renderApplication.js b/packages/react-native/Libraries/ReactNative/renderApplication.js index c483082f919d4c..de0f46108e35c7 100644 --- a/packages/react-native/Libraries/ReactNative/renderApplication.js +++ b/packages/react-native/Libraries/ReactNative/renderApplication.js @@ -23,10 +23,12 @@ import * as React from 'react'; // require BackHandler so it sets the default handler that exits the app if no listeners respond import '../Utilities/BackHandler'; -type ActivityType = React.AbstractComponent<{ - mode: 'visible' | 'hidden', - children: React.Node, -}>; +type ActivityType = component( + ...{ + mode: 'visible' | 'hidden', + children: React.Node, + } +); export default function renderApplication( RootComponent: React.ComponentType, @@ -39,7 +41,6 @@ export default function renderApplication( isLogBox?: boolean, debugName?: string, displayMode?: ?DisplayModeType, - useConcurrentRoot?: boolean, useOffscreen?: boolean, ) { invariant(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag); @@ -85,12 +86,12 @@ export default function renderApplication( } // We want to have concurrentRoot always enabled when you're on Fabric. - const useConcurrentRootOverride = fabric; + const useConcurrentRoot = Boolean(fabric); performanceLogger.startTimespan('renderApplication_React_render'); performanceLogger.setExtra( 'usedReactConcurrentRoot', - useConcurrentRootOverride ? '1' : '0', + useConcurrentRoot ? '1' : '0', ); performanceLogger.setExtra('usedReactFabric', fabric ? '1' : '0'); performanceLogger.setExtra( @@ -101,7 +102,7 @@ export default function renderApplication( element: renderable, rootTag, useFabric: Boolean(fabric), - useConcurrentRoot: Boolean(useConcurrentRootOverride), + useConcurrentRoot, }); performanceLogger.stopTimespan('renderApplication_React_render'); } diff --git a/packages/react-native/Libraries/ReactNative/requireNativeComponent.js b/packages/react-native/Libraries/ReactNative/requireNativeComponent.js index e9be731df0a181..28f0a76acce7b5 100644 --- a/packages/react-native/Libraries/ReactNative/requireNativeComponent.js +++ b/packages/react-native/Libraries/ReactNative/requireNativeComponent.js @@ -12,7 +12,8 @@ import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; -const createReactNativeComponentClass = require('../Renderer/shims/createReactNativeComponentClass'); +const createReactNativeComponentClass = + require('../Renderer/shims/createReactNativeComponentClass').default; const getNativeComponentAttributes = require('./getNativeComponentAttributes'); /** @@ -24,7 +25,9 @@ const getNativeComponentAttributes = require('./getNativeComponentAttributes'); * */ -const requireNativeComponent = (uiViewClassName: string): HostComponent => +const requireNativeComponent = ( + uiViewClassName: string, +): HostComponent => ((createReactNativeComponentClass(uiViewClassName, () => getNativeComponentAttributes(uiViewClassName), ): any): HostComponent); diff --git a/packages/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js b/packages/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js index 206b826f14ed0d..ca8c814590e8fa 100644 --- a/packages/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js +++ b/packages/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js @@ -77,7 +77,7 @@ module.exports = { return require('../Core/ReactFiberErrorDialog').default; }, get legacySendAccessibilityEvent(): legacySendAccessibilityEvent { - return require('../Components/AccessibilityInfo/legacySendAccessibilityEvent'); + return require('../Components/AccessibilityInfo/legacySendAccessibilityEvent').default; }, get RawEventEmitter(): RawEventEmitter { return require('../Core/RawEventEmitter').default; diff --git a/packages/react-native/Libraries/Settings/Settings.ios.js b/packages/react-native/Libraries/Settings/Settings.ios.js index 994e92201817a4..c4aaa483a4559c 100644 --- a/packages/react-native/Libraries/Settings/Settings.ios.js +++ b/packages/react-native/Libraries/Settings/Settings.ios.js @@ -78,4 +78,4 @@ RCTDeviceEventEmitter.addListener( Settings._sendObservations.bind(Settings), ); -module.exports = Settings; +export default Settings; diff --git a/packages/react-native/Libraries/Settings/Settings.macos.js b/packages/react-native/Libraries/Settings/Settings.macos.js index 3a97e107580975..994e92201817a4 100644 --- a/packages/react-native/Libraries/Settings/Settings.macos.js +++ b/packages/react-native/Libraries/Settings/Settings.macos.js @@ -1,5 +1,5 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * 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. @@ -8,8 +8,74 @@ * @flow */ -// [macOS] +import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter'; +import NativeSettingsManager from './NativeSettingsManager'; +import invariant from 'invariant'; + +const subscriptions: Array<{ + keys: Array, + callback: ?Function, + ... +}> = []; + +const Settings = { + _settings: (NativeSettingsManager && + NativeSettingsManager.getConstants().settings: any), + + get(key: string): mixed { + // $FlowFixMe[object-this-reference] + return this._settings[key]; + }, + + set(settings: Object) { + // $FlowFixMe[object-this-reference] + this._settings = Object.assign(this._settings, settings); + NativeSettingsManager.setValues(settings); + }, + + watchKeys(keys: string | Array, callback: Function): number { + if (typeof keys === 'string') { + keys = [keys]; + } + + invariant( + Array.isArray(keys), + 'keys should be a string or array of strings', + ); + + const sid = subscriptions.length; + subscriptions.push({keys: keys, callback: callback}); + return sid; + }, + + clearWatch(watchId: number) { + if (watchId < subscriptions.length) { + subscriptions[watchId] = {keys: [], callback: null}; + } + }, + + _sendObservations(body: Object) { + Object.keys(body).forEach(key => { + const newValue = body[key]; + // $FlowFixMe[object-this-reference] + const didChange = this._settings[key] !== newValue; + // $FlowFixMe[object-this-reference] + this._settings[key] = newValue; + + if (didChange) { + subscriptions.forEach(sub => { + if (sub.keys.indexOf(key) !== -1 && sub.callback) { + sub.callback(); + } + }); + } + }); + }, +}; + +RCTDeviceEventEmitter.addListener( + 'settingsUpdated', + Settings._sendObservations.bind(Settings), +); -/* $FlowFixMe allow macOS to share iOS file */ -const Settings = require('./Settings.ios'); module.exports = Settings; diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.macos.js b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.macos.js index 29ae835410baf6..df23765985f5fd 100644 --- a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.macos.js +++ b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypes.macos.js @@ -82,7 +82,7 @@ const _normalizeColorObject = ( // a macOS semantic color return color; } else if ('dynamic' in color && color.dynamic !== undefined) { - const normalizeColor = require('./normalizeColor'); + const normalizeColor = require('./normalizeColor').default; // a dynamic, appearance aware color const dynamic = color.dynamic; @@ -103,7 +103,7 @@ const _normalizeColorObject = ( 'colorWithSystemEffect' in color && color.colorWithSystemEffect != null ) { - const normalizeColor = require('./normalizeColor'); + const normalizeColor = require('./normalizeColor').default; const colorWithSystemEffect = color.colorWithSystemEffect; const colorObject: LocalNativeColorValue = { colorWithSystemEffect: { diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.js b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.js index 9b9506e8a45f2f..11fed9fa63fcf1 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) 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. diff --git a/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.macos.js b/packages/react-native/Libraries/StyleSheet/PlatformColorValueTypesMacOS.macos.js index d0890a335d4903..11fed9fa63fcf1 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) 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. @@ -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/StyleSheet/StyleSheet.js b/packages/react-native/Libraries/StyleSheet/StyleSheet.js index b588fae9007265..c8531f9950dc3a 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheet.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheet.js @@ -170,7 +170,13 @@ if (hairlineWidth === 0) { hairlineWidth = 1 / PixelRatio.get(); } -const absoluteFill = { +const absoluteFill: { + +bottom: 0, + +left: 0, + +position: 'absolute', + +right: 0, + +top: 0, +} = { position: 'absolute', left: 0, right: 0, diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index 33c6506b0d62b1..209d8a888a6b03 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -11,6 +11,7 @@ 'use strict'; import type AnimatedNode from '../Animated/nodes/AnimatedNode'; +import type {ImageResizeMode} from './../Image/ImageResizeMode'; import type { ____DangerouslyImpreciseStyle_InternalOverrides, ____ImageStyle_InternalOverrides, @@ -97,7 +98,7 @@ type ____LayoutStyle_Internal = $ReadOnly<{ * It works similarly to `display` in CSS, but only support 'flex' and 'none'. * 'flex' is the default. */ - display?: 'none' | 'flex', + display?: 'none' | 'flex' | 'contents', /** `width` sets the width of this component. * @@ -651,6 +652,19 @@ type ____LayoutStyle_Internal = $ReadOnly<{ */ aspectRatio?: number | string, + /** + * Box sizing controls whether certain size properties apply to the node's + * content box or border box. The size properties in question include `width`, + * `height`, `minWidth`, `minHeight`, `maxWidth`, `maxHeight`, and `flexBasis`. + * + * e.g: Say a node has 10px of padding and 10px of borders on all + * sides and a defined `width` and `height` of 100px and 50px. Then the total + * size of the node (content area + padding + border) would be 100px by 50px + * under `boxSizing: border-box` and 120px by 70px under + * `boxSizing: content-box`. + */ + boxSizing?: 'border-box' | 'content-box', + /** `zIndex` controls which components display on top of others. * Normally, you don't use `zIndex`. Components render according to * their order in the document tree, so later components draw over @@ -964,8 +978,8 @@ export type ____TextStyle_Internal = $ReadOnly<{ export type ____ImageStyle_InternalCore = $ReadOnly<{ ...$Exact<____ViewStyle_Internal>, - resizeMode?: 'contain' | 'cover' | 'stretch' | 'center' | 'repeat', - objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down', + resizeMode?: ImageResizeMode, + objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | 'none', tintColor?: ____ColorValue_Internal, overlayColor?: string, }>; @@ -977,8 +991,8 @@ export type ____ImageStyle_Internal = $ReadOnly<{ export type ____DangerouslyImpreciseStyle_InternalCore = $ReadOnly<{ ...$Exact<____TextStyle_Internal>, - resizeMode?: 'contain' | 'cover' | 'stretch' | 'center' | 'repeat', - objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down', + resizeMode?: ImageResizeMode, + objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | 'none', tintColor?: ____ColorValue_Internal, overlayColor?: string, }>; diff --git a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js index ccfad8a10707a0..5457579ea3714d 100644 --- a/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js +++ b/packages/react-native/Libraries/StyleSheet/processBackgroundImage.js @@ -14,25 +14,28 @@ import type {ProcessedColorValue} from './processColor'; import type {GradientValue} from './StyleSheetTypes'; const processColor = require('./processColor').default; -const DIRECTION_REGEX = - /^to\s+(?:top|bottom|left|right)(?:\s+(?:top|bottom|left|right))?/; +const DIRECTION_KEYWORD_REGEX = + /^to\s+(?:top|bottom|left|right)(?:\s+(?:top|bottom|left|right))?/i; const ANGLE_UNIT_REGEX = /^([+-]?\d*\.?\d+)(deg|grad|rad|turn)$/i; -const TO_BOTTOM_START_END_POINTS = { - start: {x: 0.5, y: 0}, - end: {x: 0.5, y: 1}, -}; +type LinearGradientDirection = + | {type: 'angle', value: number} + | {type: 'keyword', value: string}; type ParsedGradientValue = { type: 'linearGradient', - start: {x: number, y: number}, - end: {x: number, y: number}, + direction: LinearGradientDirection, colorStops: $ReadOnlyArray<{ color: ProcessedColorValue, position: number, }>, }; +const DEFAULT_DIRECTION: LinearGradientDirection = { + type: 'angle', + value: 180, +}; + export default function processBackgroundImage( backgroundImage: ?($ReadOnlyArray | string), ): $ReadOnlyArray { @@ -76,37 +79,43 @@ export default function processBackgroundImage( } } - let points: { - start: ParsedGradientValue['start'], - end: ParsedGradientValue['end'], - } | null = null; - - if (typeof bgImage.direction === 'undefined') { - points = TO_BOTTOM_START_END_POINTS; - } else if (ANGLE_UNIT_REGEX.test(bgImage.direction)) { - const angle = parseAngle(bgImage.direction); - if (angle != null) { - points = calculateStartEndPointsFromAngle(angle); - } - } else if (DIRECTION_REGEX.test(bgImage.direction)) { - const processedPoints = calculateStartEndPointsFromDirection( - bgImage.direction, - ); - if (processedPoints != null) { - points = processedPoints; + let direction: LinearGradientDirection = DEFAULT_DIRECTION; + const bgDirection = + bgImage.direction != null ? bgImage.direction.toLowerCase() : null; + + if (bgDirection != null) { + if (ANGLE_UNIT_REGEX.test(bgDirection)) { + const parsedAngle = getAngleInDegrees(bgDirection); + if (parsedAngle != null) { + direction = { + type: 'angle', + value: parsedAngle, + }; + } else { + // If an angle is invalid, return an empty array and do not apply any gradient. Same as web. + return []; + } + } else if (DIRECTION_KEYWORD_REGEX.test(bgDirection)) { + const parsedDirection = getDirectionForKeyword(bgDirection); + if (parsedDirection != null) { + direction = parsedDirection; + } else { + // If a direction is invalid, return an empty array and do not apply any gradient. Same as web. + return []; + } + } else { + // If a direction is invalid, return an empty array and do not apply any gradient. Same as web. + return []; } } const fixedColorStops = getFixedColorStops(processedColorStops); - if (points != null) { - result = result.concat({ - type: 'linearGradient', - start: points.start, - end: points.end, - colorStops: fixedColorStops, - }); - } + result = result.concat({ + type: 'linearGradient', + direction, + colorStops: fixedColorStops, + }); } } @@ -118,30 +127,39 @@ function parseCSSLinearGradient( ): $ReadOnlyArray { const gradients = []; let match; + + // matches one or more linear-gradient functions in CSS const linearGradientRegex = /linear-gradient\s*\(((?:\([^)]*\)|[^())])*)\)/gi; while ((match = linearGradientRegex.exec(cssString))) { const gradientContent = match[1]; const parts = gradientContent.split(','); - let points = TO_BOTTOM_START_END_POINTS; + let direction: LinearGradientDirection = DEFAULT_DIRECTION; const trimmedDirection = parts[0].trim().toLowerCase(); + + // matches individual color stops in a gradient function + // supports various color formats: named colors, hex colors, rgb(a), and hsl(a) + // e.g. "red 20%", "blue 50%", "rgba(0, 0, 0, 0.5) 30% 50%" + // TODO: does not support color hint syntax yet. It is WIP. const colorStopRegex = /\s*((?:(?:rgba?|hsla?)\s*\([^)]+\))|#[0-9a-fA-F]+|[a-zA-Z]+)(?:\s+(-?[0-9.]+%?)(?:\s+(-?[0-9.]+%?))?)?\s*/gi; if (ANGLE_UNIT_REGEX.test(trimmedDirection)) { - const angle = parseAngle(trimmedDirection); - if (angle != null) { - points = calculateStartEndPointsFromAngle(angle); + const parsedAngle = getAngleInDegrees(trimmedDirection); + if (parsedAngle != null) { + direction = { + type: 'angle', + value: parsedAngle, + }; parts.shift(); } else { // If an angle is invalid, return an empty array and do not apply any gradient. Same as web. return []; } - } else if (DIRECTION_REGEX.test(trimmedDirection)) { - const parsedPoints = - calculateStartEndPointsFromDirection(trimmedDirection); - if (parsedPoints != null) { - points = parsedPoints; + } else if (DIRECTION_KEYWORD_REGEX.test(trimmedDirection)) { + const parsedDirection = getDirectionForKeyword(trimmedDirection); + if (parsedDirection != null) { + direction = parsedDirection; parts.shift(); } else { // If a direction is invalid, return an empty array and do not apply any gradient. Same as web. @@ -198,8 +216,7 @@ function parseCSSLinearGradient( gradients.push({ type: 'linearGradient', - start: points.start, - end: points.end, + direction, colorStops: fixedColorStops, }); } @@ -207,83 +224,43 @@ function parseCSSLinearGradient( return gradients; } -function calculateStartEndPointsFromDirection(direction: string): ?{ - start: {x: number, y: number}, - end: {x: number, y: number}, -} { +function getDirectionForKeyword(direction?: string): ?LinearGradientDirection { + if (direction == null) { + return null; + } // Remove extra whitespace - const normalizedDirection = direction.replace(/\s+/g, ' '); + const normalized = direction.replace(/\s+/g, ' ').toLowerCase(); - switch (normalizedDirection) { + switch (normalized) { + case 'to top': + return {type: 'angle', value: 0}; case 'to right': - return { - start: {x: 0, y: 0.5}, - end: {x: 1, y: 0.5}, - }; - case 'to left': - return { - start: {x: 1, y: 0.5}, - end: {x: 0, y: 0.5}, - }; + return {type: 'angle', value: 90}; case 'to bottom': - return TO_BOTTOM_START_END_POINTS; - case 'to top': - return { - start: {x: 0.5, y: 1}, - end: {x: 0.5, y: 0}, - }; + return {type: 'angle', value: 180}; + case 'to left': + return {type: 'angle', value: 270}; + case 'to top right': + case 'to right top': + return {type: 'keyword', value: 'to top right'}; case 'to bottom right': case 'to right bottom': - return { - start: {x: 0, y: 0}, - end: {x: 1, y: 1}, - }; + return {type: 'keyword', value: 'to bottom right'}; case 'to top left': case 'to left top': - return { - start: {x: 1, y: 1}, - end: {x: 0, y: 0}, - }; + return {type: 'keyword', value: 'to top left'}; case 'to bottom left': case 'to left bottom': - return { - start: {x: 1, y: 0}, - end: {x: 0, y: 1}, - }; - case 'to top right': - case 'to right top': - return { - start: {x: 0, y: 1}, - end: {x: 1, y: 0}, - }; + return {type: 'keyword', value: 'to bottom left'}; default: return null; } } -function calculateStartEndPointsFromAngle(angleRadians: number): { - start: {x: number, y: number}, - end: {x: number, y: number}, -} { - // Normalize angle to be between 0 and 2π - let angleRadiansNormalized = angleRadians % (2 * Math.PI); - if (angleRadiansNormalized < 0) { - angleRadiansNormalized += 2 * Math.PI; +function getAngleInDegrees(angle?: string): ?number { + if (angle == null) { + return null; } - - const endX = 0.5 + 0.5 * Math.sin(angleRadiansNormalized); - const endY = 0.5 - 0.5 * Math.cos(angleRadiansNormalized); - - const startX = 1 - endX; - const startY = 1 - endY; - - return { - start: {x: startX, y: startY}, - end: {x: endX, y: endY}, - }; -} - -function parseAngle(angle: string): ?number { const match = angle.match(ANGLE_UNIT_REGEX); if (!match) { return null; @@ -294,13 +271,13 @@ function parseAngle(angle: string): ?number { const numericValue = parseFloat(value); switch (unit) { case 'deg': - return (numericValue * Math.PI) / 180; + return numericValue; case 'grad': - return (numericValue * Math.PI) / 200; + return numericValue * 0.9; // 1 grad = 0.9 degrees case 'rad': - return numericValue; + return (numericValue * 180) / Math.PI; case 'turn': - return numericValue * 2 * Math.PI; + return numericValue * 360; // 1 turn = 360 degrees default: return null; } diff --git a/packages/react-native/Libraries/StyleSheet/processTransform.js b/packages/react-native/Libraries/StyleSheet/processTransform.js index 6310a4b5c77550..8338e4f06d2b77 100644 --- a/packages/react-native/Libraries/StyleSheet/processTransform.js +++ b/packages/react-native/Libraries/StyleSheet/processTransform.js @@ -26,7 +26,7 @@ function processTransform( ): Array | Array { if (typeof transform === 'string') { const regex = new RegExp(/(\w+)\(([^)]+)\)/g); - let transformArray: Array = []; + const transformArray: Array = []; let matches; while ((matches = regex.exec(transform))) { @@ -50,23 +50,7 @@ function processTransform( } const _getKeyAndValueFromCSSTransform: ( - key: - | string - | $TEMPORARY$string<'matrix'> - | $TEMPORARY$string<'perspective'> - | $TEMPORARY$string<'rotate'> - | $TEMPORARY$string<'rotateX'> - | $TEMPORARY$string<'rotateY'> - | $TEMPORARY$string<'rotateZ'> - | $TEMPORARY$string<'scale'> - | $TEMPORARY$string<'scaleX'> - | $TEMPORARY$string<'scaleY'> - | $TEMPORARY$string<'skewX'> - | $TEMPORARY$string<'skewY'> - | $TEMPORARY$string<'translate'> - | $TEMPORARY$string<'translate3d'> - | $TEMPORARY$string<'translateX'> - | $TEMPORARY$string<'translateY'>, + key: string, args: string, ) => {key: string, value?: Array | number | string} = ( key, @@ -164,27 +148,18 @@ function _validateTransforms(transform: Array): void { ); const key = keys[0]; const value = transformation[key]; + if (key === 'matrix' && transform.length > 1) { + console.error( + 'When using a matrix transform, you must specify exactly one transform object. Passed transform: ' + + stringifySafe(transform), + ); + } _validateTransform(key, value, transformation); }); } function _validateTransform( - key: - | string - | $TEMPORARY$string<'matrix'> - | $TEMPORARY$string<'perspective'> - | $TEMPORARY$string<'rotate'> - | $TEMPORARY$string<'rotateX'> - | $TEMPORARY$string<'rotateY'> - | $TEMPORARY$string<'rotateZ'> - | $TEMPORARY$string<'scale'> - | $TEMPORARY$string<'scaleX'> - | $TEMPORARY$string<'scaleY'> - | $TEMPORARY$string<'skewX'> - | $TEMPORARY$string<'skewY'> - | $TEMPORARY$string<'translate'> - | $TEMPORARY$string<'translateX'> - | $TEMPORARY$string<'translateY'>, + key: string, value: any | number | string, transformation: any, ) { 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 0a083f4a2ed120..f94c98d49b9675 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.h +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.h @@ -62,6 +62,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 342ac386ffac59..94daba3f05a7a5 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.js b/packages/react-native/Libraries/Text/Text.js index 01fe0d57a20e28..67f274f33d4f21 100644 --- a/packages/react-native/Libraries/Text/Text.js +++ b/packages/react-native/Libraries/Text/Text.js @@ -33,231 +33,163 @@ type TextForwardRef = React.ElementRef< * * @see https://reactnative.dev/docs/text */ -const Text: React.AbstractComponent = - React.forwardRef( - ( - { - accessible, - accessibilityLabel, - accessibilityState, - allowFontScaling, - 'aria-busy': ariaBusy, - 'aria-checked': ariaChecked, - 'aria-disabled': ariaDisabled, - 'aria-expanded': ariaExpanded, - 'aria-label': ariaLabel, - 'aria-selected': ariaSelected, - children, - ellipsizeMode, - disabled, - id, - nativeID, - numberOfLines, - onLongPress, - onPress, - onPressIn, - onPressOut, - onResponderGrant, - onResponderMove, - onResponderRelease, - onResponderTerminate, - onResponderTerminationRequest, - onStartShouldSetResponder, - pressRetentionOffset, - selectable, - selectionColor, - suppressHighlighting, - style, - ...restProps - }: TextProps, - forwardedRef, - ) => { - const _accessibilityLabel = ariaLabel ?? accessibilityLabel; - - let _accessibilityState: ?TextProps['accessibilityState'] = - accessibilityState; - if ( - ariaBusy != null || - ariaChecked != null || - ariaDisabled != null || - ariaExpanded != null || - ariaSelected != null - ) { - if (_accessibilityState != null) { - _accessibilityState = { - busy: ariaBusy ?? _accessibilityState.busy, - checked: ariaChecked ?? _accessibilityState.checked, - disabled: ariaDisabled ?? _accessibilityState.disabled, - expanded: ariaExpanded ?? _accessibilityState.expanded, - selected: ariaSelected ?? _accessibilityState.selected, - }; - } else { - _accessibilityState = { - busy: ariaBusy, - checked: ariaChecked, - disabled: ariaDisabled, - expanded: ariaExpanded, - selected: ariaSelected, - }; - } +const Text: component( + ref: React.RefSetter, + ...props: TextProps +) = React.forwardRef( + ( + { + accessible, + accessibilityLabel, + accessibilityState, + allowFontScaling, + 'aria-busy': ariaBusy, + 'aria-checked': ariaChecked, + 'aria-disabled': ariaDisabled, + 'aria-expanded': ariaExpanded, + 'aria-label': ariaLabel, + 'aria-selected': ariaSelected, + children, + ellipsizeMode, + href, + disabled, + id, + nativeID, + numberOfLines, + onLongPress, + onPress, + onPressIn, + onPressOut, + onResponderGrant, + onResponderMove, + onResponderRelease, + onResponderTerminate, + onResponderTerminationRequest, + onStartShouldSetResponder, + pressRetentionOffset, + selectable, + selectionColor, + suppressHighlighting, + style, + ...restProps + }: TextProps, + forwardedRef, + ) => { + const _accessibilityLabel = ariaLabel ?? accessibilityLabel; + + let _accessibilityState: ?TextProps['accessibilityState'] = + accessibilityState; + if ( + ariaBusy != null || + ariaChecked != null || + ariaDisabled != null || + ariaExpanded != null || + ariaSelected != null + ) { + if (_accessibilityState != null) { + _accessibilityState = { + busy: ariaBusy ?? _accessibilityState.busy, + checked: ariaChecked ?? _accessibilityState.checked, + disabled: ariaDisabled ?? _accessibilityState.disabled, + expanded: ariaExpanded ?? _accessibilityState.expanded, + selected: ariaSelected ?? _accessibilityState.selected, + }; + } else { + _accessibilityState = { + busy: ariaBusy, + checked: ariaChecked, + disabled: ariaDisabled, + expanded: ariaExpanded, + selected: ariaSelected, + }; } + } - const _accessibilityStateDisabled = _accessibilityState?.disabled; - const _disabled = disabled ?? _accessibilityStateDisabled; - - const isPressable = - (onPress != null || - onLongPress != null || - onStartShouldSetResponder != null) && - _disabled !== true; + const _accessibilityStateDisabled = _accessibilityState?.disabled; + const _disabled = disabled ?? _accessibilityStateDisabled; - // TODO: Move this processing to the view configuration. - const _selectionColor = - selectionColor == null ? null : processColor(selectionColor); + const isPressable = + (href != null || + onPress != null || + onLongPress != null || + onStartShouldSetResponder != null) && + _disabled !== true; - let _style = style; - if (__DEV__) { - if (PressabilityDebug.isEnabled() && onPress != null) { - _style = [style, {color: 'magenta'}]; - } - } + // TODO: Move this processing to the view configuration. + const _selectionColor = + selectionColor != null ? processColor(selectionColor) : undefined; - let _numberOfLines = numberOfLines; - if (_numberOfLines != null && !(_numberOfLines >= 0)) { - if (__DEV__) { - console.error( - `'numberOfLines' in must be a non-negative number, received: ${_numberOfLines}. The value will be set to 0.`, - ); - } - _numberOfLines = 0; + let _style = style; + if (__DEV__) { + if (PressabilityDebug.isEnabled() && onPress != null) { + _style = [style, {color: 'magenta'}]; } + } - let _selectable = selectable; - - let processedStyle = flattenStyle(_style); - if (processedStyle != null) { - let overrides: ?{...TextStyleInternal} = null; - if (typeof processedStyle.fontWeight === 'number') { - overrides = overrides || ({}: {...TextStyleInternal}); - overrides.fontWeight = - // $FlowFixMe[incompatible-cast] - (processedStyle.fontWeight.toString(): TextStyleInternal['fontWeight']); - } + if (href != null) { + _style = [_style, {cursor: 'pointer'}]; + } - if (processedStyle.userSelect != null) { - _selectable = userSelectToSelectableMap[processedStyle.userSelect]; - overrides = overrides || ({}: {...TextStyleInternal}); - overrides.userSelect = undefined; - } + let _numberOfLines = numberOfLines; + if (_numberOfLines != null && !(_numberOfLines >= 0)) { + if (__DEV__) { + console.error( + `'numberOfLines' in must be a non-negative number, received: ${_numberOfLines}. The value will be set to 0.`, + ); + } + _numberOfLines = 0; + } - if (processedStyle.verticalAlign != null) { - overrides = overrides || ({}: {...TextStyleInternal}); - overrides.textAlignVertical = - verticalAlignToTextAlignVerticalMap[processedStyle.verticalAlign]; - overrides.verticalAlign = undefined; - } + let _selectable = selectable; - if (overrides != null) { - // $FlowFixMe[incompatible-type] - _style = [_style, overrides]; - } + let processedStyle = flattenStyle(_style); + if (processedStyle != null) { + let overrides: ?{...TextStyleInternal} = null; + if (typeof processedStyle.fontWeight === 'number') { + overrides = overrides || ({}: {...TextStyleInternal}); + overrides.fontWeight = + // $FlowFixMe[incompatible-cast] + (processedStyle.fontWeight.toString(): TextStyleInternal['fontWeight']); } - const _nativeID = id ?? nativeID; - - const hasTextAncestor = useContext(TextAncestor); - if (hasTextAncestor) { - if (isPressable) { - return ( - - ); - } + if (processedStyle.userSelect != null) { + _selectable = userSelectToSelectableMap[processedStyle.userSelect]; + overrides = overrides || ({}: {...TextStyleInternal}); + overrides.userSelect = undefined; + } - return ( - - {children} - - ); + if (processedStyle.verticalAlign != null) { + overrides = overrides || ({}: {...TextStyleInternal}); + overrides.textAlignVertical = + verticalAlignToTextAlignVerticalMap[processedStyle.verticalAlign]; + overrides.verticalAlign = undefined; } - // If the disabled prop and accessibilityState.disabled are out of sync but not both in - // falsy states we need to update the accessibilityState object to use the disabled prop. - if ( - _disabled !== _accessibilityStateDisabled && - ((_disabled != null && _disabled !== false) || - (_accessibilityStateDisabled != null && - _accessibilityStateDisabled !== false)) - ) { - _accessibilityState = {..._accessibilityState, disabled: _disabled}; + if (overrides != null) { + // $FlowFixMe[incompatible-type] + _style = [_style, overrides]; } + } - const _accessible = Platform.select({ - ios: accessible !== false, - android: - accessible == null - ? onPress != null || onLongPress != null - : accessible, - default: accessible, - }); + const _nativeID = id ?? nativeID; - let nativeText = null; + const hasTextAncestor = useContext(TextAncestor); + if (hasTextAncestor) { if (isPressable) { - nativeText = ( - = }} /> ); - } else { - nativeText = ( - - {children} - - ); } - if (children == null) { - return nativeText; - } + return ( + + {children} + + ); + } - // If the children do not contain a JSX element it would not be possible to have a - // nested `Text` component so we can skip adding the `TextAncestor` context wrapper - // which has a performance overhead. Since we do this for performance reasons we need - // to keep the check simple to avoid regressing overall perf. For this reason the - // `children.length` constant is set to `3`, this should be a reasonable tradeoff - // to capture the majority of `Text` uses but also not make this check too expensive. - if (Array.isArray(children) && children.length <= 3) { - let hasNonTextChild = false; - for (let child of children) { - if (child != null && typeof child === 'object') { - hasNonTextChild = true; - break; - } - } - if (!hasNonTextChild) { - return nativeText; + // If the disabled prop and accessibilityState.disabled are out of sync but not both in + // falsy states we need to update the accessibilityState object to use the disabled prop. + if ( + _disabled !== _accessibilityStateDisabled && + ((_disabled != null && _disabled !== false) || + (_accessibilityStateDisabled != null && + _accessibilityStateDisabled !== false)) + ) { + _accessibilityState = {..._accessibilityState, disabled: _disabled}; + } + + const _accessible = Platform.select({ + ios: accessible !== false, + android: + accessible == null + ? onPress != null || onLongPress != null + : accessible, + default: accessible, + }); + + let nativeText = null; + if (isPressable) { + nativeText = ( + + ); + } else { + nativeText = ( + + {children} + + ); + } + + if (children == null) { + return nativeText; + } + + // If the children do not contain a JSX element it would not be possible to have a + // nested `Text` component so we can skip adding the `TextAncestor` context wrapper + // which has a performance overhead. Since we do this for performance reasons we need + // to keep the check simple to avoid regressing overall perf. For this reason the + // `children.length` constant is set to `3`, this should be a reasonable tradeoff + // to capture the majority of `Text` uses but also not make this check too expensive. + if (Array.isArray(children) && children.length <= 3) { + let hasNonTextChild = false; + for (let child of children) { + if (child != null && typeof child === 'object') { + hasNonTextChild = true; + break; } - } else if (typeof children !== 'object') { + } + if (!hasNonTextChild) { return nativeText; } + } else if (typeof children !== 'object') { + return nativeText; + } - return ( - {nativeText} - ); - }, - ); + return ( + {nativeText} + ); + }, +); Text.displayName = 'Text'; @@ -476,10 +485,10 @@ type NativePressableTextProps = $ReadOnly<{ * This logic is split out from the main Text component to enable the more * expensive pressability logic to be only initialized when needed. */ -const NativePressableVirtualText: React.AbstractComponent< - NativePressableTextProps, - TextForwardRef, -> = React.forwardRef(({textProps, textPressabilityProps}, forwardedRef) => { +const NativePressableVirtualText: component( + ref: React.RefSetter, + ...props: NativePressableTextProps +) = React.forwardRef(({textProps, textPressabilityProps}, forwardedRef) => { const [isHighlighted, eventHandlersForText] = useTextPressability( textPressabilityProps, ); @@ -501,10 +510,10 @@ const NativePressableVirtualText: React.AbstractComponent< * This logic is split out from the main Text component to enable the more * expensive pressability logic to be only initialized when needed. */ -const NativePressableText: React.AbstractComponent< - NativePressableTextProps, - TextForwardRef, -> = React.forwardRef(({textProps, textPressabilityProps}, forwardedRef) => { +const NativePressableText: component( + ref: React.RefSetter, + ...props: NativePressableTextProps +) = React.forwardRef(({textProps, textPressabilityProps}, forwardedRef) => { const [isHighlighted, eventHandlersForText] = useTextPressability( textPressabilityProps, ); diff --git a/packages/react-native/Libraries/Text/Text/RCTTextView.h b/packages/react-native/Libraries/Text/Text/RCTTextView.h index b66c68399e9390..a950b62c9aaf68 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.h +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.h @@ -13,7 +13,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 4cf18946bdfc54..5ccb58197c3994 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextView.mm +++ b/packages/react-native/Libraries/Text/Text/RCTTextView.mm @@ -23,6 +23,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. @@ -36,6 +38,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] @@ -67,6 +79,10 @@ @implementation RCTTextView { CGRect _contentFrame; } +#if TARGET_OS_OSX // [macOS +@synthesize additionalMenuItems = _additionalMenuItems; +#endif // macOS] + // [macOS - (instancetype)initWithEventDispatcher:(id)eventDispatcher { @@ -85,12 +101,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 = @{}; @@ -138,9 +158,6 @@ - (void)setSelectable:(BOOL)selectable } #else // [macOS _textView.selectable = _selectable; - if (_selectable) { - [self setFocusable:YES]; - } #endif // macOS] } @@ -273,8 +290,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] @@ -438,17 +455,38 @@ - (BOOL)hasMouseHoverEvent return indexOfChildWithMouseHoverEvent != NSNotFound; } +- (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; } @@ -510,7 +548,7 @@ - (void)updateHoveredSubviewWithEvent:(NSEvent *)event if (_currentHoveredSubview == hoveredView) { return; - } + } // self will always be an ancestor of any views we pass in here, so it serves as a good default option. // Also, if we do set from/to nil, we have to call the relevant events on the entire subtree. @@ -622,21 +660,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]) { @@ -649,16 +672,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/RCTMultilineTextInputViewManager.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputViewManager.mm index 477f4545e0107e..b0b27e62845ba9 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputViewManager.mm +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTMultilineTextInputViewManager.mm @@ -33,6 +33,19 @@ - (RCTUIView *)view // [macOS] [view setReadablePasteBoardTypes: types]; } } +RCT_CUSTOM_VIEW_PROPERTY(disableWritingTools, BOOL, RCTMultilineTextInputView) +{ + if (@available(macOS 15.0, *)) { + NSTextView *textView = (NSTextView*)[view backedTextInputView]; + NSWritingToolsBehavior currentWritingToolsBehavior = [textView writingToolsBehavior]; + BOOL disable = json ? [RCTConvert BOOL:json] : NO; + if (disable && currentWritingToolsBehavior != NSWritingToolsBehaviorNone) { + [textView setWritingToolsBehavior:NSWritingToolsBehaviorNone]; + } else if (currentWritingToolsBehavior == NSWritingToolsBehaviorNone) { + [textView setWritingToolsBehavior:NSWritingToolsBehaviorDefault]; + } + } +} #endif // macOS] @end diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h index 4774333941aeb8..4b24938e817d39 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h @@ -6,8 +6,7 @@ */ #import // [macOS] - -#import "RCTTextUIKit.h" // [macOS] +#import // [macOS] #import #import diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm index 679d8c423eb831..07c6d9b8d53dc3 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; @@ -122,6 +126,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 @@ -226,12 +249,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]; @@ -365,7 +384,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; @@ -399,19 +418,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]; } @@ -603,7 +622,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.h b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.h new file mode 100644 index 00000000000000..8b1c4d5aa5deaa --- /dev/null +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTWrappedTextView.h @@ -0,0 +1,24 @@ +/* + * 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 "../../RCTTextUIKit.h" + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTWrappedTextView : RCTPlatformView + +@property (nonatomic, weak) id textInputDelegate; +@property (assign) BOOL hideVerticalScrollIndicator; + +@end + +NS_ASSUME_NONNULL_END 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 7fe50d5b24d67a..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; } @@ -398,17 +427,17 @@ - (void)textViewDidChangeSelection:(__unused UITextView *)textView [self textViewProbablyDidChangeSelection]; } +#endif // [macOS] + #pragma mark - UIScrollViewDelegate -- (void)scrollViewDidScroll:(UIScrollView *)scrollView +- (void)scrollViewDidScroll:(RCTUIScrollView *)scrollView // [macOS] { if ([_backedTextInputView.textInputDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) { [_backedTextInputView.textInputDelegate scrollViewDidScroll:scrollView]; } } -#endif // [macOS] - #if TARGET_OS_OSX // [macOS #pragma mark - NSTextViewDelegate @@ -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/RCTBackedTextInputViewProtocol.h b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h index d34ab300e65865..583ecc16835ee3 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h +++ b/packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h @@ -38,6 +38,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign, readonly) BOOL textWasPasted; #else // [macOS @property (nonatomic, assign) BOOL textWasPasted; +@property (nonatomic, readonly) NSResponder *responder; +@property (nonatomic, assign) BOOL enableFocusRing; #endif // macOS] @property (nonatomic, assign, readonly) BOOL dictationRecognizing; @property (nonatomic, assign) UIEdgeInsets textContainerInset; diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm index dd0e88178c7fe4..07d0503ab83b71 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm @@ -212,6 +212,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; @@ -220,6 +227,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) { @@ -229,6 +238,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText [self.backedTextInputView.undoManager registerUndoWithTarget:self handler:^(RCTBaseTextInputView *strongSelf) { strongSelf.attributedText = oldAttributedTextWithoutGhostText; [strongSelf textInputDidChange]; + [strongSelf setSelectionStart:oldSelectionStart selectionEnd:oldSelectionEnd]; // [macOS] }]; } @@ -306,7 +316,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]; @@ -332,7 +342,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] } @@ -549,7 +559,7 @@ - (void)submitOnKeyDownIfNeeded:(NSEvent *)event } } } - + if (shouldSubmit) { if (_onSubmitEditing) { _onSubmitEditing(@{}); @@ -650,7 +660,7 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range [backedTextInputView setSelectedTextRange:NSMakeRange(range.location + allowedLength, 0) notifyDelegate:YES]; #endif // macOS] - + [self textInputDidChange]; } @@ -705,17 +715,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), + }, + }); }); } @@ -778,7 +791,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 69f89842558dc8..c896b4f058d3d7 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm @@ -158,6 +158,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 ae55ec1c8b0683..f444a3cf2b93e6 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 2c58023ee31611..ba6b22ede0fbfb 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 @@ -60,12 +59,12 @@ - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { if (self.drawsBackground) { if (self.backgroundColor && self.backgroundColor.alphaComponent > 0) { - + [self.backgroundColor set]; NSRectFill(cellFrame); } } - + [super drawInteriorWithFrame:[self titleRectForBounds:cellFrame] inView:controlView]; } @@ -105,7 +104,7 @@ @implementation RCTUITextField { - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_textDidChange) name:UITextFieldTextDidChangeNotification @@ -157,7 +156,7 @@ - (BOOL)hasMarkedText } #endif // macOS] - + #pragma mark - Accessibility #if !TARGET_OS_OSX // [macOS] @@ -202,6 +201,11 @@ - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset #if TARGET_OS_OSX // [macOS +- (NSResponder *)responder +{ + return self; +} + + (Class)cellClass { return RCTUITextFieldCell.class; @@ -276,7 +280,7 @@ - (RCTUIColor*)selectionColor { return ((RCTUITextFieldCell*)self.cell).selectionColor; } - + - (void)setCursorColor:(NSColor *)cursorColor { ((RCTUITextFieldCell*)self.cell).insertionPointColor = cursorColor; @@ -484,9 +488,9 @@ - (CGRect)editingRectForBounds:(CGRect)bounds { return [self textRectForBounds:bounds]; } - + #else // [macOS - + #pragma mark - NSTextFieldDelegate methods - (void)textDidChange:(NSNotification *)notification @@ -513,7 +517,7 @@ - (void)textDidEndEditing:(NSNotification *)notification [delegate textFieldEndEditing:self]; } } - + - (void)textViewDidChangeSelection:(NSNotification *)notification { id delegate = self.delegate; @@ -530,13 +534,18 @@ - (BOOL)textView:(NSTextView *)aTextView shouldChangeTextInRange:(NSRange)aRange } return NO; } - + - (NSMenu *)textView:(NSTextView *)view menu:(NSMenu *)menu forEvent:(NSEvent *)event atIndex:(NSUInteger)charIndex { if (menu) { [[RCTTouchHandler touchHandlerForView:self] willShowMenuWithEvent:event]; } + RCTHideMenuItemsWithFilterPredicate(menu, ^bool(NSMenuItem *item) { + // hide font menu option + return RCTMenuItemHasSubmenuItemWithAction(item, @selector(orderFrontFontPanel:)); + }); + return menu; } @@ -590,7 +599,7 @@ - (BOOL)performKeyEquivalent:(NSEvent *)event return [super performKeyEquivalent:event]; } #endif // macOS] - + #if !TARGET_OS_OSX // [macOS] - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate { @@ -621,7 +630,7 @@ - (void)setSelectedTextRange:(NSRange)selectedTextRange notifyDelegate:(BOOL)not // so the adapter must not generate a notification for it. [_textInputDelegateAdapter skipNextTextInputDidChangeSelectionEventWithTextRange:selectedTextRange]; } - + [[self currentEditor] setSelectedRange:selectedTextRange]; } diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.h b/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.h index c0586c7be5dbfd..792c6703c41d58 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.h +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.h @@ -7,9 +7,6 @@ // [macOS] -#if TARGET_OS_OSX #define RCT_SUBCLASS_SECURETEXTFIELD 1 -#endif #include - diff --git a/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.m b/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.m index 41d4f4c9b7b3b3..c8bdcc2788bfd3 100644 --- a/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.m +++ b/packages/react-native/Libraries/Text/TextInput/Singleline/macOS/RCTUISecureTextField.m @@ -5,10 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -// [macOS] - -#if TARGET_OS_OSX #define RCT_SUBCLASS_SECURETEXTFIELD 1 -#endif #include "../RCTUITextField.mm" diff --git a/packages/react-native/Libraries/Text/TextNativeComponent.js b/packages/react-native/Libraries/Text/TextNativeComponent.js index 5ce6c0c8c07aac..f502c085126953 100644 --- a/packages/react-native/Libraries/Text/TextNativeComponent.js +++ b/packages/react-native/Libraries/Text/TextNativeComponent.js @@ -8,21 +8,20 @@ * @format */ -import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; +import type {HostComponent} from '../../src/private/types/HostComponent'; import type {ProcessedColorValue} from '../StyleSheet/processColor'; -import type {PressEvent} from '../Types/CoreEventTypes'; +import type {GestureResponderEvent} from '../Types/CoreEventTypes'; import type {TextProps} from './TextProps'; import {createViewConfig} from '../NativeComponent/ViewConfig'; import UIManager from '../ReactNative/UIManager'; import createReactNativeComponentClass from '../Renderer/shims/createReactNativeComponentClass'; -import Platform from '../Utilities/Platform'; export type NativeTextProps = $ReadOnly<{ ...TextProps, isHighlighted?: ?boolean, selectionColor?: ?ProcessedColorValue, - onClick?: ?(event: PressEvent) => mixed, + onClick?: ?(event: GestureResponderEvent) => mixed, // This is only needed for platforms that optimize text hit testing, e.g., // react-native-windows. It can be used to only hit test virtual text spans // that have pressable events attached to them. @@ -49,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/Text/TextProps.js b/packages/react-native/Libraries/Text/TextProps.js index 21ab11c0664031..2e9caa706a99fa 100644 --- a/packages/react-native/Libraries/Text/TextProps.js +++ b/packages/react-native/Libraries/Text/TextProps.js @@ -17,11 +17,11 @@ import type { AccessibilityState, Role, } from '../Components/View/ViewAccessibility'; -import type {TextStyleProp} from '../StyleSheet/StyleSheet'; +import type {ColorValue, TextStyleProp} from '../StyleSheet/StyleSheet'; import type { - LayoutEvent, + LayoutChangeEvent, PointerEvent, - PressEvent, + GestureResponderEvent, TextLayoutEvent, } from '../Types/CoreEventTypes'; import type {Node} from 'react'; @@ -142,27 +142,27 @@ export type TextProps = $ReadOnly<{ * * See https://reactnative.dev/docs/text#onlayout */ - onLayout?: ?(event: LayoutEvent) => mixed, + onLayout?: ?(event: LayoutChangeEvent) => mixed, /** * This function is called on long press. * * See https://reactnative.dev/docs/text#onlongpress */ - onLongPress?: ?(event: PressEvent) => mixed, + onLongPress?: ?(event: GestureResponderEvent) => mixed, /** * This function is called on press. * * See https://reactnative.dev/docs/text#onpress */ - onPress?: ?(event: PressEvent) => mixed, - onPressIn?: ?(event: PressEvent) => mixed, - onPressOut?: ?(event: PressEvent) => mixed, - onResponderGrant?: ?(event: PressEvent) => void, - onResponderMove?: ?(event: PressEvent) => void, - onResponderRelease?: ?(event: PressEvent) => void, - onResponderTerminate?: ?(event: PressEvent) => void, + onPress?: ?(event: GestureResponderEvent) => mixed, + onPressIn?: ?(event: GestureResponderEvent) => mixed, + onPressOut?: ?(event: GestureResponderEvent) => mixed, + onResponderGrant?: ?(event: GestureResponderEvent) => void, + onResponderMove?: ?(event: GestureResponderEvent) => void, + onResponderRelease?: ?(event: GestureResponderEvent) => void, + onResponderTerminate?: ?(event: GestureResponderEvent) => void, onResponderTerminationRequest?: ?() => boolean, onStartShouldSetResponder?: ?() => boolean, onMoveShouldSetResponder?: ?() => boolean, @@ -212,7 +212,7 @@ export type TextProps = $ReadOnly<{ * * See https://reactnative.dev/docs/text#selectioncolor */ - selectionColor?: ?string, + selectionColor?: ?ColorValue, dataDetectorType?: ?('phoneNumber' | 'link' | 'email' | 'none' | 'all'), diff --git a/packages/react-native/Libraries/TurboModule/TurboModuleRegistry.js b/packages/react-native/Libraries/TurboModule/TurboModuleRegistry.js index 9f2a3735861642..135be5bca89d7a 100644 --- a/packages/react-native/Libraries/TurboModule/TurboModuleRegistry.js +++ b/packages/react-native/Libraries/TurboModule/TurboModuleRegistry.js @@ -16,9 +16,6 @@ const NativeModules = require('../BatchedBridge/NativeModules'); const turboModuleProxy = global.__turboModuleProxy; -const useLegacyNativeModuleInterop = - global.RN$Bridgeless !== true || global.RN$TurboInterop === true; - function requireModule(name: string): ?T { if (turboModuleProxy != null) { const module: ?T = turboModuleProxy(name); @@ -27,8 +24,11 @@ function requireModule(name: string): ?T { } } - if (useLegacyNativeModuleInterop) { - // Backward compatibility layer during migration. + if ( + global.RN$Bridgeless !== true || + global.RN$TurboInterop === true || + global.RN$UnifiedNativeModuleProxy === true + ) { const legacyModule: ?T = NativeModules[name]; if (legacyModule != null) { return legacyModule; diff --git a/packages/react-native/Libraries/Types/CoreEventTypes.js b/packages/react-native/Libraries/Types/CoreEventTypes.js index f506cb4ac7c3c3..59e82c255b7316 100644 --- a/packages/react-native/Libraries/Types/CoreEventTypes.js +++ b/packages/react-native/Libraries/Types/CoreEventTypes.js @@ -8,18 +8,16 @@ * @format */ -import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; +import type {HostInstance} from '../../src/private/types/HostInstance'; -import * as React from 'react'; - -export type SyntheticEvent<+T> = $ReadOnly<{| +export type NativeSyntheticEvent<+T> = $ReadOnly<{ bubbles: ?boolean, cancelable: ?boolean, - currentTarget: number | React.ElementRef>, + currentTarget: number | HostInstance, defaultPrevented: ?boolean, - dispatchConfig: $ReadOnly<{| + dispatchConfig: $ReadOnly<{ registrationName: string, - |}>, + }>, eventPhase: ?number, preventDefault: () => void, isDefaultPrevented: () => boolean, @@ -28,19 +26,19 @@ export type SyntheticEvent<+T> = $ReadOnly<{| isTrusted: ?boolean, nativeEvent: T, persist: () => void, - target: ?number | React.ElementRef>, + target: ?number | HostInstance, timeStamp: number, type: ?string, -|}>; +}>; -export type ResponderSyntheticEvent = $ReadOnly<{| - ...SyntheticEvent, - touchHistory: $ReadOnly<{| +export type ResponderSyntheticEvent = $ReadOnly<{ + ...NativeSyntheticEvent, + touchHistory: $ReadOnly<{ indexOfSingleActiveTouch: number, mostRecentTimeStamp: number, numberActiveTouches: number, touchBank: $ReadOnlyArray< - $ReadOnly<{| + $ReadOnly<{ touchActive: boolean, startPageX: number, startPageY: number, @@ -51,38 +49,38 @@ export type ResponderSyntheticEvent = $ReadOnly<{| previousPageX: number, previousPageY: number, previousTimeStamp: number, - |}>, + }>, >, - |}>, -|}>; + }>, +}>; -export type Layout = $ReadOnly<{| +export type LayoutRectangle = $ReadOnly<{ x: number, y: number, width: number, height: number, -|}>; +}>; -export type TextLayout = $ReadOnly<{| - ...Layout, +export type TextLayoutLine = $ReadOnly<{ + ...LayoutRectangle, ascender: number, capHeight: number, descender: number, text: string, xHeight: number, -|}>; +}>; -export type LayoutEvent = SyntheticEvent< - $ReadOnly<{| - layout: Layout, - |}>, +export type LayoutChangeEvent = NativeSyntheticEvent< + $ReadOnly<{ + layout: LayoutRectangle, + }>, >; -export type TextLayoutEvent = SyntheticEvent< - $ReadOnly<{| - lines: Array, - |}>, ->; +export type TextLayoutEventData = $ReadOnly<{ + lines: Array, +}>; + +export type TextLayoutEvent = NativeSyntheticEvent; /** * https://developer.mozilla.org/en-US/docs/Web/API/UIEvent @@ -157,7 +155,7 @@ export interface NativeMouseEvent extends NativeUIEvent { /** * The secondary target for the event, if there is one. */ - +relatedTarget: null | number | React.ElementRef>; + +relatedTarget: null | number | HostInstance; // offset is proposed: https://drafts.csswg.org/cssom-view/#extensions-to-the-mouseevent-interface /** * The X coordinate of the mouse pointer between that event and the padding edge of the target node @@ -220,13 +218,15 @@ export interface NativePointerEvent extends NativeMouseEvent { +isPrimary: boolean; } -export type PointerEvent = SyntheticEvent; +export type PointerEvent = NativeSyntheticEvent; -export type PressEvent = ResponderSyntheticEvent< - $ReadOnly<{| +export type GestureResponderEvent = ResponderSyntheticEvent< + $ReadOnly<{ altKey?: ?boolean, // [macOS] button?: ?number, // [macOS] - changedTouches: $ReadOnlyArray<$PropertyType>, + changedTouches: $ReadOnlyArray< + $PropertyType, + >, ctrlKey?: ?boolean, // [macOS] force?: number, identifier: number, @@ -238,59 +238,61 @@ export type PressEvent = ResponderSyntheticEvent< shiftKey?: ?boolean, // [macOS] target: ?number, timestamp: number, - touches: $ReadOnlyArray<$PropertyType>, - |}>, + touches: $ReadOnlyArray< + $PropertyType, + >, + }>, >; -export type ScrollEvent = SyntheticEvent< - $ReadOnly<{| - contentInset: $ReadOnly<{| +export type ScrollEvent = NativeSyntheticEvent< + $ReadOnly<{ + contentInset: $ReadOnly<{ bottom: number, left: number, right: number, top: number, - |}>, - contentOffset: $ReadOnly<{| + }>, + contentOffset: $ReadOnly<{ y: number, x: number, - |}>, - contentSize: $ReadOnly<{| + }>, + contentSize: $ReadOnly<{ height: number, width: number, - |}>, - layoutMeasurement: $ReadOnly<{| + }>, + layoutMeasurement: $ReadOnly<{ height: number, width: number, - |}>, - targetContentOffset?: $ReadOnly<{| + }>, + targetContentOffset?: $ReadOnly<{ y: number, x: number, - |}>, - velocity?: $ReadOnly<{| + }>, + velocity?: $ReadOnly<{ y: number, x: number, - |}>, + }>, zoomScale?: number, responderIgnoreScroll?: boolean, preferredScrollerStyle?: string, // [macOS] - |}>, + }>, >; -export type BlurEvent = SyntheticEvent< - $ReadOnly<{| +export type BlurEvent = NativeSyntheticEvent< + $ReadOnly<{ target: number, - |}>, + }>, >; -export type FocusEvent = SyntheticEvent< - $ReadOnly<{| +export type FocusEvent = NativeSyntheticEvent< + $ReadOnly<{ target: number, - |}>, + }>, >; // [macOS -export type KeyEvent = SyntheticEvent< - $ReadOnly<{| +export type KeyEvent = NativeSyntheticEvent< + $ReadOnly<{ // Modifier keys capsLockKey: boolean, shiftKey: boolean, @@ -306,7 +308,7 @@ export type KeyEvent = SyntheticEvent< ArrowUp: boolean, ArrowDown: boolean, key: string, - |}>, + }>, >; /** @@ -320,22 +322,22 @@ export type KeyEvent = SyntheticEvent< * * @platform macos */ -export type HandledKeyEvent = $ReadOnly<{| +export type HandledKeyEvent = $ReadOnly<{ altKey?: ?boolean, ctrlKey?: ?boolean, metaKey?: ?boolean, shiftKey?: ?boolean, key: string, -|}>; +}>; // macOS] -export type MouseEvent = SyntheticEvent< - $ReadOnly<{| +export type MouseEvent = NativeSyntheticEvent< + $ReadOnly<{ clientX: number, clientY: number, pageX: number, pageY: number, timestamp: number, - |}>, + }>, >; diff --git a/packages/react-native/Libraries/Utilities/Appearance.js b/packages/react-native/Libraries/Utilities/Appearance.js index 3cd23a65fe2245..6c13f4d6d530c5 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.js +++ b/packages/react-native/Libraries/Utilities/Appearance.js @@ -14,7 +14,6 @@ import typeof INativeAppearance from './NativeAppearance'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import EventEmitter from '../vendor/emitter/EventEmitter'; -import {isAsyncDebugging} from './DebugEnvironment'; import invariant from 'invariant'; type Appearance = { @@ -74,13 +73,6 @@ function getState(): $NonMaybeType { * the `useColorScheme` hook. */ export function getColorScheme(): ?ColorSchemeName { - if (__DEV__) { - if (isAsyncDebugging) { - // Hard code light theme when using the async debugger as - // sync calls aren't supported - return 'light'; - } - } let colorScheme = null; const state = getState(); const {NativeAppearance} = state; @@ -105,7 +97,9 @@ export function setColorScheme(colorScheme: ?ColorSchemeName): void { const {NativeAppearance} = state; if (NativeAppearance != null) { NativeAppearance.setColorScheme(colorScheme ?? 'unspecified'); - state.appearance = {colorScheme}; + state.appearance = { + colorScheme: toColorScheme(NativeAppearance.getColorScheme()), + }; } } diff --git a/packages/react-native/Libraries/Utilities/BackHandler.android.js b/packages/react-native/Libraries/Utilities/BackHandler.android.js index 187136a7f61338..63d768ad519a4d 100644 --- a/packages/react-native/Libraries/Utilities/BackHandler.android.js +++ b/packages/react-native/Libraries/Utilities/BackHandler.android.js @@ -61,10 +61,6 @@ type TBackHandler = {| eventName: BackPressEventName, handler: () => ?boolean, ) => {remove: () => void, ...}, - +removeEventListener: ( - eventName: BackPressEventName, - handler: () => ?boolean, - ) => void, |}; const BackHandler: TBackHandler = { exitApp: function (): void { @@ -88,22 +84,14 @@ const BackHandler: TBackHandler = { _backPressSubscriptions.push(handler); } return { - remove: (): void => BackHandler.removeEventListener(eventName, handler), + remove: (): void => { + const index = _backPressSubscriptions.indexOf(handler); + if (index !== -1) { + _backPressSubscriptions.splice(index, 1); + } + }, }; }, - - /** - * Removes the event handler. - */ - removeEventListener: function ( - eventName: BackPressEventName, - handler: () => ?boolean, - ): void { - const index = _backPressSubscriptions.indexOf(handler); - if (index !== -1) { - _backPressSubscriptions.splice(index, 1); - } - }, }; module.exports = BackHandler; diff --git a/packages/react-native/Libraries/Utilities/BackHandler.ios.js b/packages/react-native/Libraries/Utilities/BackHandler.ios.js index 3cd03706467aa3..e6e201a136e23c 100644 --- a/packages/react-native/Libraries/Utilities/BackHandler.ios.js +++ b/packages/react-native/Libraries/Utilities/BackHandler.ios.js @@ -10,32 +10,25 @@ 'use strict'; -module.exports = require('../Components/UnimplementedViews/UnimplementedView'); - type BackPressEventName = 'backPress' | 'hardwareBackPress'; function emptyFunction(): void {} -type TBackHandler = {| +type TBackHandler = { +exitApp: () => void, +addEventListener: ( eventName: BackPressEventName, handler: () => ?boolean, ) => {remove: () => void, ...}, - +removeEventListener: ( - eventName: BackPressEventName, - handler: () => ?boolean, - ) => void, -|}; +}; -let BackHandler: TBackHandler = { +const BackHandler: TBackHandler = { exitApp: emptyFunction, addEventListener(_eventName: BackPressEventName, _handler: Function) { return { remove: emptyFunction, }; }, - removeEventListener(_eventName: BackPressEventName, _handler: Function) {}, }; -module.exports = BackHandler; +export default BackHandler; diff --git a/packages/react-native/Libraries/Utilities/BackHandler.js.flow b/packages/react-native/Libraries/Utilities/BackHandler.js.flow index a966ffd6183694..aec78e1697004b 100644 --- a/packages/react-native/Libraries/Utilities/BackHandler.js.flow +++ b/packages/react-native/Libraries/Utilities/BackHandler.js.flow @@ -18,10 +18,6 @@ type TBackHandler = {| eventName: BackPressEventName, handler: () => ?boolean, ) => {remove: () => void, ...}, - +removeEventListener: ( - eventName: BackPressEventName, - handler: () => ?boolean, - ) => void, |}; declare module.exports: TBackHandler; diff --git a/packages/react-native/Libraries/Utilities/BackHandler.macos.js b/packages/react-native/Libraries/Utilities/BackHandler.macos.js index 02d7142731af0e..3cd03706467aa3 100644 --- a/packages/react-native/Libraries/Utilities/BackHandler.macos.js +++ b/packages/react-native/Libraries/Utilities/BackHandler.macos.js @@ -1,18 +1,41 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * 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. * - * On Apple TV, this implements back navigation using the TV remote's menu button. - * On iOS, this just implements a stub. - * - * @flow * @format + * @flow */ -// [macOS] +'use strict'; + +module.exports = require('../Components/UnimplementedViews/UnimplementedView'); + +type BackPressEventName = 'backPress' | 'hardwareBackPress'; + +function emptyFunction(): void {} + +type TBackHandler = {| + +exitApp: () => void, + +addEventListener: ( + eventName: BackPressEventName, + handler: () => ?boolean, + ) => {remove: () => void, ...}, + +removeEventListener: ( + eventName: BackPressEventName, + handler: () => ?boolean, + ) => void, +|}; + +let BackHandler: TBackHandler = { + exitApp: emptyFunction, + addEventListener(_eventName: BackPressEventName, _handler: Function) { + return { + remove: emptyFunction, + }; + }, + removeEventListener(_eventName: BackPressEventName, _handler: Function) {}, +}; -/* $FlowFixMe allow macOS to share iOS file */ -const BackHandler = require('./BackHandler.ios'); module.exports = BackHandler; diff --git a/packages/react-native/Libraries/Utilities/DevSettings.js b/packages/react-native/Libraries/Utilities/DevSettings.js index 9f7b4baddc368c..c3779588820ec4 100644 --- a/packages/react-native/Libraries/Utilities/DevSettings.js +++ b/packages/react-native/Libraries/Utilities/DevSettings.js @@ -69,4 +69,4 @@ if (__DEV__) { }; } -module.exports = DevSettings; +export default DevSettings; diff --git a/packages/react-native/Libraries/Utilities/HMRClient.js b/packages/react-native/Libraries/Utilities/HMRClient.js index 25baa6759022a2..857e8793f5ec9a 100644 --- a/packages/react-native/Libraries/Utilities/HMRClient.js +++ b/packages/react-native/Libraries/Utilities/HMRClient.js @@ -14,8 +14,8 @@ import getDevServer from '../Core/Devtools/getDevServer'; import LogBox from '../LogBox/LogBox'; import NativeRedBox from '../NativeModules/specs/NativeRedBox'; -const DevSettings = require('./DevSettings'); -const Platform = require('./Platform'); +const DevSettings = require('./DevSettings').default; +const Platform = require('./Platform').default; const invariant = require('invariant'); const MetroHMRClient = require('metro-runtime/src/modules/HMRClient'); const prettyFormat = require('pretty-format'); @@ -39,7 +39,7 @@ type LogLevel = | 'groupEnd' | 'debug'; -export type HMRClientNativeInterface = {| +export type HMRClientNativeInterface = { enable(): void, disable(): void, registerBundle(requestUrl: string): void, @@ -53,7 +53,7 @@ export type HMRClientNativeInterface = {| scheme?: string, ): void, unstable_notifyFuseboxConsoleEnabled(): void, -|}; +}; /** * HMR Client that receives from the server HMR updates and propagates them @@ -70,7 +70,7 @@ const HMRClient: HMRClientNativeInterface = { } invariant(hmrClient, 'Expected HMRClient.setup() call at startup.'); - const DevLoadingView = require('./DevLoadingView'); + const DevLoadingView = require('./DevLoadingView').default; // We use this for internal logging only. // It doesn't affect the logic. @@ -181,7 +181,7 @@ const HMRClient: HMRClientNativeInterface = { invariant(!hmrClient, 'Cannot initialize hmrClient twice'); // Moving to top gives errors due to NativeModules not being initialized - const DevLoadingView = require('./DevLoadingView'); + const DevLoadingView = require('./DevLoadingView').default; const serverHost = port !== null && port !== '' ? `${host}:${port}` : host; @@ -387,4 +387,4 @@ function showCompileError() { throw error; } -module.exports = HMRClient; +export default HMRClient; diff --git a/packages/react-native/Libraries/Utilities/Platform.flow.js b/packages/react-native/Libraries/Utilities/Platform.flow.js index b8a12d86524dd4..674d94388db1f2 100644 --- a/packages/react-native/Libraries/Utilities/Platform.flow.js +++ b/packages/react-native/Libraries/Utilities/Platform.flow.js @@ -18,7 +18,7 @@ export type PlatformSelectSpec = { type IOSPlatform = { __constants: null, - OS: $TEMPORARY$string<'ios'>, + OS: 'ios', // $FlowFixMe[unsafe-getters-setters] get Version(): string, // $FlowFixMe[unsafe-getters-setters] @@ -54,7 +54,7 @@ type IOSPlatform = { type AndroidPlatform = { __constants: null, - OS: $TEMPORARY$string<'android'>, + OS: 'android', // $FlowFixMe[unsafe-getters-setters] get Version(): number, // $FlowFixMe[unsafe-getters-setters] diff --git a/packages/react-native/Libraries/Utilities/Platform.macos.js b/packages/react-native/Libraries/Utilities/Platform.macos.js index fd739e03aed9ac..4a8e53f13093ab 100644 --- a/packages/react-native/Libraries/Utilities/Platform.macos.js +++ b/packages/react-native/Libraries/Utilities/Platform.macos.js @@ -28,17 +28,17 @@ const Platform = { return this.constants.osVersion; }, // $FlowFixMe[unsafe-getters-setters] - get constants(): {| + get constants(): { isTesting: boolean, osVersion: string, - reactNativeVersion: {| + reactNativeVersion: { major: number, minor: number, patch: number, prerelease: ?number, - |}, + }, systemName: string, - |} { + } { // $FlowFixMe[object-this-reference] if (this.__constants == null) { // $FlowFixMe[object-this-reference] @@ -75,4 +75,4 @@ const Platform = { spec.default, }; -module.exports = Platform; +export default Platform; diff --git a/packages/react-native/Libraries/Utilities/ReactNativeTestTools.js b/packages/react-native/Libraries/Utilities/ReactNativeTestTools.js index 685f5e746188cd..87ae5b4115e34c 100644 --- a/packages/react-native/Libraries/Utilities/ReactNativeTestTools.js +++ b/packages/react-native/Libraries/Utilities/ReactNativeTestTools.js @@ -18,7 +18,7 @@ const Switch = require('../Components/Switch/Switch').default; const TextInput = require('../Components/TextInput/TextInput'); const View = require('../Components/View/View'); const Text = require('../Text/Text'); -const {VirtualizedList} = require('@react-native-mac/virtualized-lists'); // [macOS] +const {VirtualizedList} = require('@react-native/virtualized-lists'); // [macOS] const React = require('react'); const ReactTestRenderer = require('react-test-renderer'); diff --git a/packages/react-native/Libraries/Utilities/codegenNativeComponent.js b/packages/react-native/Libraries/Utilities/codegenNativeComponent.js index 3f9187f8fe173a..cd2d9ee5998fa2 100644 --- a/packages/react-native/Libraries/Utilities/codegenNativeComponent.js +++ b/packages/react-native/Libraries/Utilities/codegenNativeComponent.js @@ -31,7 +31,7 @@ export type NativeComponentType = HostComponent; // `requireNativeComponent` is not available in Bridgeless mode. // e.g. This function runs at runtime if `codegenNativeComponent` was not called // from a file suffixed with NativeComponent.js. -function codegenNativeComponent( +function codegenNativeComponent( componentName: string, options?: Options, ): NativeComponentType { diff --git a/packages/react-native/Libraries/Utilities/useMergeRefs.js b/packages/react-native/Libraries/Utilities/useMergeRefs.js index 1499e4eab3afdb..3c7439a60360f2 100644 --- a/packages/react-native/Libraries/Utilities/useMergeRefs.js +++ b/packages/react-native/Libraries/Utilities/useMergeRefs.js @@ -8,6 +8,7 @@ * @format */ +import useRefEffect from './useRefEffect'; import * as React from 'react'; import {useCallback} from 'react'; @@ -22,19 +23,37 @@ import {useCallback} from 'react'; */ export default function useMergeRefs( ...refs: $ReadOnlyArray> -): (Instance | null) => void { - return useCallback( - (current: Instance | null) => { - for (const ref of refs) { - if (ref != null) { +): React.RefSetter { + const refEffect = useCallback( + (current: Instance) => { + const cleanups: $ReadOnlyArray void)> = refs.map(ref => { + if (ref == null) { + return undefined; + } else { if (typeof ref === 'function') { - ref(current); + // $FlowIssue[incompatible-type] - Flow does not understand ref cleanup. + const cleanup: void | (() => void) = ref(current); + return typeof cleanup === 'function' + ? cleanup + : () => { + ref(null); + }; } else { ref.current = current; + return () => { + ref.current = null; + }; } } - } + }); + + return () => { + for (const cleanup of cleanups) { + cleanup?.(); + } + }; }, [...refs], // eslint-disable-line react-hooks/exhaustive-deps ); + return useRefEffect(refEffect); } diff --git a/packages/react-native/Libraries/WebSocket/WebSocket.js b/packages/react-native/Libraries/WebSocket/WebSocket.js index 7fe28e813eedc9..839e251fabce95 100644 --- a/packages/react-native/Libraries/WebSocket/WebSocket.js +++ b/packages/react-native/Libraries/WebSocket/WebSocket.js @@ -296,4 +296,4 @@ class WebSocket extends (EventTarget(...WEBSOCKET_EVENTS): typeof EventTarget) { } } -module.exports = WebSocket; +export default WebSocket; diff --git a/packages/react-native/Libraries/WebSocket/WebSocketEvent.js b/packages/react-native/Libraries/WebSocket/WebSocketEvent.js index d70147ef646232..ae1f72e9eac7e8 100644 --- a/packages/react-native/Libraries/WebSocket/WebSocketEvent.js +++ b/packages/react-native/Libraries/WebSocket/WebSocketEvent.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @format + * @flow strict-local */ 'use strict'; @@ -18,7 +19,9 @@ * In case of "message", the `data` property contains the incoming data. */ class WebSocketEvent { - constructor(type, eventInitDict) { + type: string; + + constructor(type: string, eventInitDict: $FlowFixMe) { this.type = type.toString(); Object.assign(this, eventInitDict); } diff --git a/packages/react-native/Libraries/WebSocket/WebSocketInterceptor.js b/packages/react-native/Libraries/WebSocket/WebSocketInterceptor.js index b3618d58f61c58..4db6c7ec42ae5f 100644 --- a/packages/react-native/Libraries/WebSocket/WebSocketInterceptor.js +++ b/packages/react-native/Libraries/WebSocket/WebSocketInterceptor.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @format + * @flow strict-local */ import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; @@ -40,53 +41,53 @@ const WebSocketInterceptor = { /** * Invoked when RCTWebSocketModule.close(...) is called. */ - setCloseCallback(callback) { + setCloseCallback(callback: $FlowFixMe) { closeCallback = callback; }, /** * Invoked when RCTWebSocketModule.send(...) or sendBinary(...) is called. */ - setSendCallback(callback) { + setSendCallback(callback: $FlowFixMe) { sendCallback = callback; }, /** * Invoked when RCTWebSocketModule.connect(...) is called. */ - setConnectCallback(callback) { + setConnectCallback(callback: $FlowFixMe) { connectCallback = callback; }, /** * Invoked when event "websocketOpen" happens. */ - setOnOpenCallback(callback) { + setOnOpenCallback(callback: $FlowFixMe) { onOpenCallback = callback; }, /** * Invoked when event "websocketMessage" happens. */ - setOnMessageCallback(callback) { + setOnMessageCallback(callback: $FlowFixMe) { onMessageCallback = callback; }, /** * Invoked when event "websocketFailed" happens. */ - setOnErrorCallback(callback) { + setOnErrorCallback(callback: $FlowFixMe) { onErrorCallback = callback; }, /** * Invoked when event "websocketClosed" happens. */ - setOnCloseCallback(callback) { + setOnCloseCallback(callback: $FlowFixMe) { onCloseCallback = callback; }, - isInterceptorEnabled() { + isInterceptorEnabled(): boolean { return isInterceptorEnabled; }, @@ -100,6 +101,7 @@ const WebSocketInterceptor = { */ _registerEvents() { subscriptions = [ + // $FlowFixMe[incompatible-type] eventEmitter.addListener('websocketMessage', ev => { if (onMessageCallback) { onMessageCallback( @@ -110,16 +112,19 @@ const WebSocketInterceptor = { ); } }), + // $FlowFixMe[incompatible-type] eventEmitter.addListener('websocketOpen', ev => { if (onOpenCallback) { onOpenCallback(ev.id); } }), + // $FlowFixMe[incompatible-type] eventEmitter.addListener('websocketClosed', ev => { if (onCloseCallback) { onCloseCallback(ev.id, {code: ev.code, reason: ev.reason}); } }), + // $FlowFixMe[incompatible-type] eventEmitter.addListener('websocketFailed', ev => { if (onErrorCallback) { onErrorCallback(ev.id, {message: ev.message}); @@ -132,6 +137,7 @@ const WebSocketInterceptor = { if (isInterceptorEnabled) { return; } + // $FlowFixMe[underconstrained-implicit-instantiation] eventEmitter = new NativeEventEmitter( // T88715063: NativeEventEmitter only used this parameter on iOS. Now it uses it on all platforms, so this code was modified automatically to preserve its behavior // If you want to use the native module on other platforms, please remove this condition and test its behavior @@ -144,11 +150,13 @@ const WebSocketInterceptor = { // Override `connect` method for all RCTWebSocketModule requests // to intercept the request url, protocols, options and socketId, // then pass them through the `connectCallback`. + // $FlowFixMe[cannot-write] + // $FlowFixMe[missing-this-annot] NativeWebSocketModule.connect = function ( - url, - protocols, - options, - socketId, + url: string, + protocols: Array | null, + options: $FlowFixMe, + socketId: number, ) { if (connectCallback) { connectCallback(url, protocols, options, socketId); @@ -158,6 +166,8 @@ const WebSocketInterceptor = { // Override `send` method for all RCTWebSocketModule requests to intercept // the data sent, then pass them through the `sendCallback`. + // $FlowFixMe[cannot-write] + // $FlowFixMe[missing-this-annot] NativeWebSocketModule.send = function (data, socketId) { if (sendCallback) { sendCallback(data, socketId); @@ -167,6 +177,8 @@ const WebSocketInterceptor = { // Override `sendBinary` method for all RCTWebSocketModule requests to // intercept the data sent, then pass them through the `sendCallback`. + // $FlowFixMe[cannot-write] + // $FlowFixMe[missing-this-annot] NativeWebSocketModule.sendBinary = function (data, socketId) { if (sendCallback) { sendCallback(WebSocketInterceptor._arrayBufferToString(data), socketId); @@ -176,6 +188,8 @@ const WebSocketInterceptor = { // Override `close` method for all RCTWebSocketModule requests to intercept // the close information, then pass them through the `closeCallback`. + // $FlowFixMe[cannot-write] + // $FlowFixMe[missing-this-annot] NativeWebSocketModule.close = function () { if (closeCallback) { if (arguments.length === 3) { @@ -190,7 +204,7 @@ const WebSocketInterceptor = { isInterceptorEnabled = true; }, - _arrayBufferToString(data) { + _arrayBufferToString(data: string): ArrayBuffer | string { const value = base64.toByteArray(data).buffer; if (value === undefined || value === null) { return '(no value)'; @@ -211,9 +225,13 @@ const WebSocketInterceptor = { return; } isInterceptorEnabled = false; + // $FlowFixMe[cannot-write] NativeWebSocketModule.send = originalRCTWebSocketSend; + // $FlowFixMe[cannot-write] NativeWebSocketModule.sendBinary = originalRCTWebSocketSendBinary; + // $FlowFixMe[cannot-write] NativeWebSocketModule.close = originalRCTWebSocketClose; + // $FlowFixMe[cannot-write] NativeWebSocketModule.connect = originalRCTWebSocketConnect; connectCallback = null; @@ -228,4 +246,4 @@ const WebSocketInterceptor = { }, }; -module.exports = WebSocketInterceptor; +export default WebSocketInterceptor; diff --git a/packages/react-native/Libraries/promiseRejectionTrackingOptions.js b/packages/react-native/Libraries/promiseRejectionTrackingOptions.js index 5a12d2d300b3e4..d8fdbd617843fb 100644 --- a/packages/react-native/Libraries/promiseRejectionTrackingOptions.js +++ b/packages/react-native/Libraries/promiseRejectionTrackingOptions.js @@ -36,7 +36,7 @@ let rejectionTrackingOptions: $NonMaybeType[0]> = { } // It could although this object is not a standard error, it still has stack information to unwind // $FlowFixMe ignore types just check if stack is there - if (rejection.stack && typeof rejection.stack === 'string') { + if (rejection?.stack && typeof rejection.stack === 'string') { stack = rejection.stack; } } diff --git a/packages/react-native/React/Base/RCTAssert.h b/packages/react-native/React/Base/RCTAssert.h index e088fa9582b1f0..47d4409f6e2577 100644 --- a/packages/react-native/React/Base/RCTAssert.h +++ b/packages/react-native/React/Base/RCTAssert.h @@ -157,7 +157,7 @@ RCT_EXTERN NSString *RCTFormatStackTrace(NSArray *> /** * Convenience macro to assert which thread is currently running (DEBUG mode only) */ -#if DEBUG +#ifdef DEBUG #define RCTAssertThread(thread, ...) \ _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") RCTAssert( \ diff --git a/packages/react-native/React/Base/RCTBridge.mm b/packages/react-native/React/Base/RCTBridge.mm index e640776bd18b41..f576a6be6ace04 100644 --- a/packages/react-native/React/Base/RCTBridge.mm +++ b/packages/react-native/React/Base/RCTBridge.mm @@ -8,7 +8,6 @@ #import "RCTBridge.h" #import "RCTBridge+Inspector.h" #import "RCTBridge+Private.h" -#import "RCTDevSettings.h" // [macOS] #import diff --git a/packages/react-native/React/Base/RCTDefines.h b/packages/react-native/React/Base/RCTDefines.h index ed038680a4146c..0f3793bec299c8 100644 --- a/packages/react-native/React/Base/RCTDefines.h +++ b/packages/react-native/React/Base/RCTDefines.h @@ -27,7 +27,7 @@ * from release builds to improve performance and reduce binary size. */ #ifndef RCT_DEBUG -#if DEBUG +#ifdef DEBUG #define RCT_DEBUG 1 #else #define RCT_DEBUG 0 @@ -39,7 +39,7 @@ * such as the debug executors, dev menu, red box, etc. */ #ifndef RCT_DEV -#if DEBUG +#ifdef DEBUG #define RCT_DEV 1 #else #define RCT_DEV 0 @@ -48,12 +48,16 @@ /** * RCT_REMOTE_PROFILE: RCT_PROFILE + RCT_ENABLE_INSPECTOR + enable the - * connectivity functionality to control the profiler remotely, such as via Chrome DevTools or - * Flipper. + * connectivity functionality to control the profiler remotely, such as via Chrome DevTools. + * If Fusebox is enabled for release builds, enable the remote profile mode, fall back to RCT_DEV by default. */ #ifndef RCT_REMOTE_PROFILE +#ifdef REACT_NATIVE_ENABLE_FUSEBOX_RELEASE +#define RCT_REMOTE_PROFILE REACT_NATIVE_ENABLE_FUSEBOX_RELEASE +#else #define RCT_REMOTE_PROFILE RCT_DEV #endif +#endif /** * Enable the code to support making calls to the underlying sampling profiler mechanism. 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 3f45d2f7a6bcd8..caadbe62545421 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) { @@ -134,22 +223,8 @@ - (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; if ([((RCTUIView*)targetView) respondsToSelector:@selector(isUserInteractionEnabled)]) { // [macOS] @@ -211,6 +286,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]; @@ -269,7 +349,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; @@ -307,11 +387,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] @@ -456,10 +536,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 { @@ -486,6 +566,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, @@ -495,56 +585,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 @@ -556,7 +654,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 @@ -606,13 +704,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 8cd17e29b3ca41..e28beac0ca610f 100644 --- a/packages/react-native/React/Base/RCTUIKit.h +++ b/packages/react-native/React/Base/RCTUIKit.h @@ -218,6 +218,7 @@ typedef NS_ENUM(NSInteger, UIViewContentMode) { UIViewContentModeScaleAspectFit = NSViewLayerContentsPlacementScaleProportionallyToFit, UIViewContentModeScaleToFill = NSViewLayerContentsPlacementScaleAxesIndependently, UIViewContentModeCenter = NSViewLayerContentsPlacementCenter, + UIViewContentModeTopLeft = NSViewLayerContentsPlacementTopLeft, }; // UIInterface.h/NSUserInterfaceLayout.h @@ -269,6 +270,9 @@ extern "C" { // UIGraphics.h CGContextRef UIGraphicsGetCurrentContext(void); +void UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale); +NSImage *UIGraphicsGetImageFromCurrentImageContext(void); +void UIGraphicsEndImageContext(void); CGImageRef UIImageGetCGImageRef(NSImage *image); #ifdef __cplusplus @@ -661,8 +665,18 @@ typedef void (^RCTUIGraphicsImageDrawingActions)(RCTUIGraphicsImageRendererConte @interface RCTUIGraphicsImageRenderer : NSObject - (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 77c833d60caca2..41e342676fba49 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/RCTUtils.m b/packages/react-native/React/Base/RCTUtils.m index d91af712dd0663..e95d34cdb9685d 100644 --- a/packages/react-native/React/Base/RCTUtils.m +++ b/packages/react-native/React/Base/RCTUtils.m @@ -443,7 +443,7 @@ IMP RCTSwapClassMethods(Class cls, SEL original, SEL replacement) // [macOS] } else { method_exchangeImplementations(originalMethod, replacementMethod); } - + return originalImplementation; // [macOS] } @@ -462,7 +462,7 @@ IMP RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement) // [macOS] } else { method_exchangeImplementations(originalMethod, replacementMethod); } - + return originalImplementation; // [macOS] } @@ -1225,3 +1225,27 @@ BOOL RCTIsAppActive(void) return [RCTSharedApplication() isActive]; #endif // macOS] } + +#if TARGET_OS_OSX // [macOS +void RCTHideMenuItemsWithFilterPredicate(NSMenu *menu, RCTMenuItemFilterPredicate shouldFilter) +{ + for (NSMenuItem *item in menu.itemArray) { + if (shouldFilter(item)) { + item.hidden = YES; + } + } +} + +BOOL RCTMenuItemHasSubmenuItemWithAction(NSMenuItem *item, SEL action) +{ + if (!item.hasSubmenu) { + return NO; + } + for (NSMenuItem *submenuItem in item.submenu.itemArray) { + if (submenuItem.action == action) { + return YES; + } + } + return NO; +} +#endif // macOS] 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 e40b7277fc3a24..a0194cefc7aad7 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)initWithSurface:(id)surface NS_DESIGNATED_INITIALIZER; diff --git a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.mm b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.mm index 1b07c5943af4f5..4e9e16f7c081dd 100644 --- a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.mm +++ b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingProxyRootView.mm @@ -18,6 +18,9 @@ #import "RCTRootContentView.h" #import "RCTRootViewDelegate.h" #import "RCTSurface.h" +#import "RCTSurfaceRootShadowView.h" +#import "RCTUIManager.h" +#import "RCTUIManagerUtils.h" #import "UIView+React.h" static RCTSurfaceSizeMeasureMode convertToSurfaceSizeMeasureMode(RCTRootViewSizeFlexibility sizeFlexibility) diff --git a/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm b/packages/react-native/React/Base/Surface/SurfaceHostingView/RCTSurfaceHostingView.mm index 409db62215784f..39c3ce790e7011 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/RCTPlatform.m b/packages/react-native/React/Base/macOS/RCTPlatform.m index 5b8cdac554bbec..d56c31bf911a80 100644 --- a/packages/react-native/React/Base/macOS/RCTPlatform.m +++ b/packages/react-native/React/Base/macOS/RCTPlatform.m @@ -5,10 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -// [macOS] -#if TARGET_OS_OSX - -#import "RCTPlatform.h" +#import #import @@ -39,4 +36,3 @@ Class RCTPlatformCls(void) { } @end -#endif \ No newline at end of file diff --git a/packages/react-native/React/Base/macOS/RCTPlatformDisplayLink.m b/packages/react-native/React/Base/macOS/RCTPlatformDisplayLink.m index 6ee09fde67a600..caf1f648a65eca 100644 --- a/packages/react-native/React/Base/macOS/RCTPlatformDisplayLink.m +++ b/packages/react-native/React/Base/macOS/RCTPlatformDisplayLink.m @@ -6,7 +6,6 @@ */ // [macOS] -#if TARGET_OS_OSX #import "RCTPlatformDisplayLink.h" @@ -162,4 +161,3 @@ - (void)tick #pragma clang diagnostic pop @end -#endif \ No newline at end of file diff --git a/packages/react-native/React/Base/macOS/RCTUIKit.m b/packages/react-native/React/Base/macOS/RCTUIKit.m index 48f2ec5f8eff5c..566be6c224f23e 100644 --- a/packages/react-native/React/Base/macOS/RCTUIKit.m +++ b/packages/react-native/React/Base/macOS/RCTUIKit.m @@ -5,10 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -// [macOS] - -#if TARGET_OS_OSX - #import #import @@ -32,6 +28,31 @@ CGContextRef UIGraphicsGetCurrentContext(void) return [[NSGraphicsContext currentContext] CGContext]; } +void UIGraphicsBeginImageContextWithOptions(CGSize size, __unused BOOL opaque, CGFloat scale) +{ + if (scale == 0.0) + { + // TODO: Assert. We can't assume a display scale on macOS + scale = 1.0; + } + size_t width = ceilf(size.width * scale); + size_t height = ceilf(size.height * scale); + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef ctx = CGBitmapContextCreate(NULL, width, height, 8/*bitsPerComponent*/, width * 4/*bytesPerRow*/, colorSpace, kCGImageAlphaPremultipliedFirst); + CGColorSpaceRelease(colorSpace); + if (ctx != NULL) + { + // flip the context (top left at 0, 0) and scale it + CGContextTranslateCTM(ctx, 0.0, height); + CGContextScaleCTM(ctx, scale, -scale); + NSGraphicsContext *graphicsContext = [NSGraphicsContext graphicsContextWithCGContext:ctx flipped:YES]; + objc_setAssociatedObject(graphicsContext, &RCTGraphicsContextSizeKey, [NSValue valueWithSize:size], OBJC_ASSOCIATION_COPY_NONATOMIC); + [NSGraphicsContext saveGraphicsState]; + [NSGraphicsContext setCurrentContext:graphicsContext]; + CFRelease(ctx); + } +} + NSImage *UIGraphicsGetImageFromCurrentImageContext(void) { NSImage *image = nil; @@ -52,6 +73,12 @@ CGContextRef UIGraphicsGetCurrentContext(void) return image; } +void UIGraphicsEndImageContext(void) +{ + RCTAssert(objc_getAssociatedObject([NSGraphicsContext currentContext], &RCTGraphicsContextSizeKey), @"The current graphics context is not a React image context!"); + [NSGraphicsContext restoreGraphicsState]; +} + // // functionally equivalent types // @@ -63,7 +90,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; @@ -119,7 +146,7 @@ void UIBezierPathAppendPath(UIBezierPath *path, UIBezierPath *appendPath) CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *bezierPath) { CGPathRef immutablePath = NULL; - + // Draw the path elements. NSInteger numElements = [bezierPath elementCount]; if (numElements > 0) @@ -127,7 +154,7 @@ CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *bezierPath) CGMutablePathRef path = CGPathCreateMutable(); NSPoint points[3]; BOOL didClosePath = YES; - + for (NSInteger i = 0; i < numElements; i++) { switch ([bezierPath elementAtIndex:i associatedPoints:points]) @@ -135,34 +162,34 @@ CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *bezierPath) case NSMoveToBezierPathElement: CGPathMoveToPoint(path, NULL, points[0].x, points[0].y); break; - + case NSLineToBezierPathElement: CGPathAddLineToPoint(path, NULL, points[0].x, points[0].y); didClosePath = NO; break; - + case NSCurveToBezierPathElement: CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y); didClosePath = NO; break; - + case NSClosePathBezierPathElement: CGPathCloseSubpath(path); didClosePath = YES; break; } } - + // Be sure the path is closed or Quartz may not do valid hit detection. if (!didClosePath) CGPathCloseSubpath(path); - + immutablePath = CGPathCreateCopy(path); CGPathRelease(path); } - + return immutablePath; } @@ -182,6 +209,7 @@ @implementation RCTUIView BOOL _userInteractionEnabled; NSTrackingArea *_trackingArea; BOOL _mouseDownCanMoveWindow; + BOOL _respondsToDisplayLayer; } + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key @@ -211,6 +239,7 @@ @implementation RCTUIView self->_userInteractionEnabled = YES; self->_enableFocusRing = YES; self->_mouseDownCanMoveWindow = YES; + self->_respondsToDisplayLayer = [self respondsToSelector:@selector(displayLayer:)]; } return self; } @@ -471,14 +500,23 @@ - (void)updateLayer // so it has to be reset from the view's NSColor ivar. [layer setBackgroundColor:[_backgroundColor CGColor]]; } - [(id)self displayLayer:layer]; + + // In Fabric, wantsUpdateLayer is always enabled and doesn't guarantee that + // the instance has a displayLayer method. + if (_respondsToDisplayLayer) { + [(id)self displayLayer:layer]; + } } - (void)drawRect:(CGRect)rect { 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]; } @@ -501,6 +539,21 @@ - (BOOL)becomeFirstResponder return [[self window] makeFirstResponder:self]; } +- (BOOL)canBecomeKeyView +{ + return self.focusable; +} + +- (NSRect)focusRingMaskBounds { + return [self bounds]; +} + +- (void)drawFocusRingMask { + if ([self enableFocusRing]) { + NSRectFill(self.bounds); + } +} + @synthesize userInteractionEnabled = _userInteractionEnabled; - (NSView *)hitTest:(CGPoint)point withEvent:(__unused UIEvent *)event @@ -582,7 +635,7 @@ - (instancetype)initWithFrame:(CGRect)frame self.scrollEnabled = YES; self.drawsBackground = NO; } - + return self; } @@ -737,7 +790,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect self.constrainScrolling = NO; self.drawsBackground = NO; } - + return self; } @@ -746,7 +799,7 @@ - (NSRect)constrainBoundsRect:(NSRect)proposedBounds if (self.constrainScrolling) { return NSMakeRect(0, 0, 0, 0); } - + return [super constrainBoundsRect:proposedBounds]; } @@ -777,7 +830,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect [self setSelectable:NO]; [self setWantsLayer:YES]; } - + return self; } @@ -846,7 +899,7 @@ - (void)stopAnimation:(id)sender - (void)setActivityIndicatorViewStyle:(UIActivityIndicatorViewStyle)activityIndicatorViewStyle { _activityIndicatorViewStyle = activityIndicatorViewStyle; - + switch (activityIndicatorViewStyle) { case UIActivityIndicatorViewStyleLarge: self.controlSize = NSControlSizeLarge; @@ -876,14 +929,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]; @@ -914,6 +967,7 @@ - (void)setHidden:(BOOL)hidden // RCTUIImageView @implementation RCTUIImageView { + UIImage *_image; CALayer *_tintingLayer; } @@ -923,7 +977,7 @@ - (instancetype)initWithFrame:(CGRect)frame [self setLayer:[[CALayer alloc] init]]; [self setWantsLayer:YES]; } - + return self; } @@ -937,65 +991,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]; } } @@ -1026,12 +1094,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); @@ -1043,5 +1111,3 @@ - (nonnull NSImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActi } @end - -#endif diff --git a/packages/react-native/React/CoreModules/RCTActionSheetManager.mm b/packages/react-native/React/CoreModules/RCTActionSheetManager.mm index dffdcaf758ff4a..340d56398ec286 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); @@ -166,8 +168,8 @@ - (void)presentSharingServicePicker:(NSSharingServicePicker *)picker UIColor *tintColor = [RCTConvert UIColor:options.tintColor() ? @(*options.tintColor()) : nil]; UIColor *cancelButtonTintColor = [RCTConvert UIColor:options.cancelButtonTintColor() ? @(*options.cancelButtonTintColor()) : nil]; -#endif // [macOS] NSString *userInterfaceStyle = [RCTConvert NSString:options.userInterfaceStyle()]; +#endif // [macOS] dispatch_async(dispatch_get_main_queue(), ^{ #if !TARGET_OS_OSX // [macOS] @@ -330,9 +332,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) { @@ -444,7 +450,7 @@ - (void)sharingServicePicker:(NSSharingServicePicker *)sharingServicePicker didC service.subject = _sharingSubject; } } - + - (void)sharingService:(NSSharingService *)sharingService didFailToShareItems:(NSArray *)items error:(NSError *)error { _failureCallback(@[RCTJSErrorFromNSError(error)]); @@ -462,7 +468,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) { @@ -470,7 +476,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 cee66a985f01ad..bdcfc0a0bcbfdb 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 @@ -178,7 +199,24 @@ - (void)registerHotkeys [(RCTDevSettings *)[weakSelf.moduleRegistry moduleForName:"DevSettings"] setIsDebuggingRemotely:NO]; }]; -#endif +#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 @@ -189,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 @@ -249,12 +289,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 { @@ -279,7 +323,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" @@ -624,6 +670,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 19c0a38ec8a499..6fe05c7ebdb1e9 100644 --- a/packages/react-native/React/CoreModules/RCTDeviceInfo.mm +++ b/packages/react-native/React/CoreModules/RCTDeviceInfo.mm @@ -16,7 +16,7 @@ #import #import #import -#import "UIView+React.h" // [macOS] +#import // [macOS] #import "CoreModulesPlugins.h" @@ -108,11 +108,12 @@ - (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]; @@ -128,9 +129,10 @@ - (void)_cleanupObservers static BOOL RCTIsIPhoneNotched() { static BOOL isIPhoneNotched = NO; - static dispatch_once_t onceToken; #if TARGET_OS_IOS + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ RCTAssertMainQueue(); @@ -166,17 +168,17 @@ 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; #endif // macOS] - + return RCTExportedDimensions(fontScale); } diff --git a/packages/react-native/React/CoreModules/RCTEventDispatcher.mm b/packages/react-native/React/CoreModules/RCTEventDispatcher.mm index d8b9639d5631af..b685edacb55631 100644 --- a/packages/react-native/React/CoreModules/RCTEventDispatcher.mm +++ b/packages/react-native/React/CoreModules/RCTEventDispatcher.mm @@ -107,6 +107,7 @@ - (void)sendTextEventWithType:(RCTTextEventType)type break; case '\n': key = @"Enter"; + break; default: break; } diff --git a/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm b/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm index 8031e9741979ce..58997abacbf4c6 100644 --- a/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm +++ b/packages/react-native/React/CoreModules/RCTKeyboardObserver.mm @@ -83,7 +83,7 @@ -(void)EVENT : (NSNotification *)notification } @end - +#if !TARGET_OS_OSX // [macOS] NS_INLINE NSDictionary *RCTRectDictionaryValue(CGRect rect) { return @{ @@ -93,7 +93,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/CxxBridge/JSCExecutorFactory.mm b/packages/react-native/React/CxxBridge/JSCExecutorFactory.mm index fda4a64a0e74e8..6173ae5ccffac4 100644 --- a/packages/react-native/React/CxxBridge/JSCExecutorFactory.mm +++ b/packages/react-native/React/CxxBridge/JSCExecutorFactory.mm @@ -13,26 +13,12 @@ namespace facebook::react { -// [macOS -void JSCExecutorFactory::setEnableDebugger(bool enableDebugger) { - enableDebugger_ = enableDebugger; -} - -void JSCExecutorFactory::setDebuggerName(const std::string &debuggerName) { - debuggerName_ = debuggerName; -} -// macOS] - std::unique_ptr JSCExecutorFactory::createJSExecutor( std::shared_ptr delegate, std::shared_ptr __unused jsQueue) { - // [macOS - facebook::jsc::RuntimeConfig rc = { - .enableDebugger = enableDebugger_, - .debuggerName = debuggerName_, - }; - return std::make_unique(facebook::jsc::makeJSCRuntime(std::move(rc)), delegate, JSIExecutor::defaultTimeoutInvoker, runtimeInstaller_); - // macOS] + return std::make_unique( + facebook::jsc::makeJSCRuntime(), delegate, JSIExecutor::defaultTimeoutInvoker, runtimeInstaller_); } + } // namespace facebook::react diff --git a/packages/react-native/React/CxxModule/RCTCxxMethod.mm b/packages/react-native/React/CxxModule/RCTCxxMethod.mm index 4fc4a2abbc39c9..55db91d2965e8f 100644 --- a/packages/react-native/React/CxxModule/RCTCxxMethod.mm +++ b/packages/react-native/React/CxxModule/RCTCxxMethod.mm @@ -98,14 +98,22 @@ - (id)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray NSNumber *id2 = arguments[arguments.count - 1]; second = ^(std::vector args) { - [bridge enqueueCallback:id2 args:convertFollyDynamicToId(folly::dynamic(args.begin(), args.end()))]; + folly::dynamic obj = folly::dynamic::array; + for (auto &arg : args) { + obj.push_back(std::move(arg)); + } + [bridge enqueueCallback:id2 args:convertFollyDynamicToId(std::move(obj))]; }; } else { id1 = arguments[arguments.count - 1]; } first = ^(std::vector args) { - [bridge enqueueCallback:id1 args:convertFollyDynamicToId(folly::dynamic(args.begin(), args.end()))]; + folly::dynamic obj = folly::dynamic::array; + for (auto &arg : args) { + obj.push_back(std::move(arg)); + } + [bridge enqueueCallback:id1 args:convertFollyDynamicToId(std::move(obj))]; }; } diff --git a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm index c43cc33a8cf990..be62b7a5c8847d 100644 --- a/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm +++ b/packages/react-native/React/DevSupport/RCTInspectorDevServerHelper.mm @@ -101,6 +101,9 @@ static NSURL *getInspectorDeviceUrl(NSURL *bundleURL) { + auto &inspectorFlags = facebook::react::jsinspector_modern::InspectorFlags::getInstance(); + BOOL isProfilingBuild = inspectorFlags.getIsProfilingBuild(); + #if !TARGET_OS_OSX // [macOS] NSString *escapedDeviceName = [[[UIDevice currentDevice] name] stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; @@ -113,11 +116,12 @@ NSString *escapedInspectorDeviceId = [getInspectorDeviceId() stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; - return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/inspector/device?name=%@&app=%@&device=%@", + return [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/inspector/device?name=%@&app=%@&device=%@&profiling=%@", getServerHost(bundleURL), escapedDeviceName, escapedAppName, - escapedInspectorDeviceId]]; + escapedInspectorDeviceId, + isProfilingBuild ? @"true" : @"false"]]; } @implementation RCTInspectorDevServerHelper @@ -149,15 +153,11 @@ + (BOOL)isPackagerDisconnected + (void)openDebugger:(NSURL *)bundleURL withErrorMessage:(NSString *)errorMessage { - NSString *appId = [[[NSBundle mainBundle] bundleIdentifier] - stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; - NSString *escapedInspectorDeviceId = [getInspectorDeviceId() stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; - NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/open-debugger?appId=%@&device=%@", + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/open-debugger?device=%@", getServerHost(bundleURL), - appId, escapedInspectorDeviceId]]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; [request setHTTPMethod:@"POST"]; @@ -203,7 +203,7 @@ + (void)disableDebugger // [macOS Add a lock around access to connection id connection; - [connectionsLock lock]; + [connectionsLock lock]; connection = socketConnections[key]; // macOS] if (!connection || !connection.isConnected) { diff --git a/packages/react-native/React/Fabric/AppleEventBeat.cpp b/packages/react-native/React/Fabric/AppleEventBeat.cpp new file mode 100644 index 00000000000000..4a3d533a0cd94a --- /dev/null +++ b/packages/react-native/React/Fabric/AppleEventBeat.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "AppleEventBeat.h" + +#include + +namespace facebook::react { + +AppleEventBeat::AppleEventBeat( + std::shared_ptr ownerBox, + std::unique_ptr uiRunLoopObserver, + RuntimeScheduler& runtimeScheduler) + : EventBeat(std::move(ownerBox), runtimeScheduler), + uiRunLoopObserver_(std::move(uiRunLoopObserver)) { + uiRunLoopObserver_->setDelegate(this); + uiRunLoopObserver_->enable(); +} + +void AppleEventBeat::activityDidChange( + const RunLoopObserver::Delegate* delegate, + RunLoopObserver::Activity /*activity*/) const noexcept { + react_native_assert(delegate == this); + induce(); +} + +} // namespace facebook::react diff --git a/packages/react-native/React/Fabric/AppleEventBeat.h b/packages/react-native/React/Fabric/AppleEventBeat.h new file mode 100644 index 00000000000000..f2f9ef4316a3f4 --- /dev/null +++ b/packages/react-native/React/Fabric/AppleEventBeat.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +class RuntimeScheduler; + +/* + * Event beat associated with JavaScript runtime. + * The beat is called on `RuntimeExecutor`'s thread induced by the UI thread + * event loop. + */ +class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate { + public: + AppleEventBeat( + std::shared_ptr ownerBox, + std::unique_ptr uiRunLoopObserver, + RuntimeScheduler& RuntimeScheduler); + +#pragma mark - RunLoopObserver::Delegate + + void activityDidChange( + const RunLoopObserver::Delegate* delegate, + RunLoopObserver::Activity activity) const noexcept override; + + private: + std::unique_ptr uiRunLoopObserver_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm index 963aa5a8b0f4c5..b1697574c0df18 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm @@ -16,7 +16,6 @@ #import #import #import -#import using namespace facebook::react; @@ -101,13 +100,6 @@ - (void)_setStateAndResubscribeImageResponseObserver:(const ImageShadowNode::Con const auto &imageRequest = _state->getData().getImageRequest(); auto &observerCoordinator = imageRequest.getObserverCoordinator(); observerCoordinator.removeObserver(_imageResponseObserverProxy); - // Cancelling image request because we are no longer observing it. - // This is not 100% correct place to do this because we may want to - // re-create RCTImageComponentView with the same image and if it - // was cancelled before downloaded, download is not resumed. - // This will only become issue if we decouple life cycle of a - // ShadowNode from ComponentView, which is not something we do now. - imageRequest.cancel(); } _state = state; 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 a1e320c1d03bc1..3364d32c1eb721 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm @@ -58,12 +58,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) { @@ -88,7 +131,11 @@ - (void)setContentOffset:(CGPoint)contentOffset } 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; } @@ -97,11 +144,43 @@ - (void)setContentOffset:(CGPoint)contentOffset } } +#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]; + } +#endif // macOS] +} + + +#if TARGET_OS_OSX // [macOS +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated +{ + if (animated) { + [NSAnimationContext beginGrouping]; + [[NSAnimationContext currentContext] setDuration:0.3]; + [[self.contentView animator] setBoundsOrigin:contentOffset]; + [NSAnimationContext endGrouping]; + } else { + self.contentOffset = contentOffset; + } +} + +- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated +{ + [self magnifyToFitRect:rect]; } +- (void)flashScrollIndicators +{ + [self flashScrollers]; +} +#endif // macOS] + #if !TARGET_OS_OSX // [macOS] - (BOOL)touchesShouldCancelInContentView:(RCTPlatformView *)view // [macOS] { 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 008ded02ade8e3..6cf408cf42f385 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h @@ -53,6 +53,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 75650be2f347fb..a12ebee628b51f 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] @@ -276,6 +276,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 { @@ -407,7 +439,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; @@ -451,7 +487,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); @@ -618,6 +654,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] @@ -643,33 +695,18 @@ - (BOOL)touchesShouldCancelInContentView:(__unused RCTPlatformView *)view // [ma - (void)scrollViewDidScroll:(RCTUIScrollView *)scrollView // [macOS] { - const auto &props = static_cast(*_props); auto scrollMetrics = [self _scrollViewMetrics]; - if (props.enableSyncOnScroll) { + [self _updateStateWithContentOffset]; + + NSTimeInterval now = CACurrentMediaTime(); + if ((_lastScrollEventDispatchTime == 0) || (now - _lastScrollEventDispatchTime > _scrollEventThrottle)) { + _lastScrollEventDispatchTime = now; if (_eventEmitter) { - const auto &eventEmitter = static_cast(*_eventEmitter); - // TODO: temporary API to unblock testing of synchronous rendering. - eventEmitter.experimental_flushSync([&eventEmitter, &scrollMetrics, &self]() { - [self _updateStateWithContentOffset]; - // TODO: temporary API to unblock testing of synchronous rendering. - eventEmitter.experimental_onDiscreteScroll(scrollMetrics); - }); - } - } else { - if (!_isUserTriggeredScrolling || CoreFeatures::enableGranularScrollViewStateUpdatesIOS) { - [self _updateStateWithContentOffset]; + static_cast(*_eventEmitter).onScroll(scrollMetrics); } - NSTimeInterval now = CACurrentMediaTime(); - if ((_lastScrollEventDispatchTime == 0) || (now - _lastScrollEventDispatchTime > _scrollEventThrottle)) { - _lastScrollEventDispatchTime = now; - if (_eventEmitter) { - static_cast(*_eventEmitter).onScroll(scrollMetrics); - } - - RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag, kOnScrollEvent); - } + RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag, kOnScrollEvent); } [self _remountChildrenIfNeeded]; @@ -817,9 +854,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 @@ -918,9 +953,7 @@ - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated [self _forceDispatchNextScrollEvent]; -#if !TARGET_OS_OSX // [macOS] - [_scrollView setContentOffset:offset animated:animated]; -#endif // [macOS] + [(RCTEnhancedScrollView *)_scrollView setContentOffset:offset animated:animated]; // [macOS] if (!animated) { // When not animated, the expected workflow in ``scrollViewDidEndScrollingAnimation`` after scrolling is not going @@ -931,9 +964,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/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index b5e5f3c5c590ab..1db8bf119fca55 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -10,7 +10,9 @@ #if !TARGET_OS_OSX // [macOS] #import -#endif // [macOS] +#else // [macOS +#import +#endif // macOS] #import #import @@ -50,13 +52,37 @@ - (void)setNeedsDisplay; @end -#if !TARGET_OS_OSX // [macOS] @interface RCTParagraphComponentView () @property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0)); @end -#endif // [macOS] +#else // [macOS +@interface RCTParagraphComponentUnfocusableTextView : NSTextView +@end + +@implementation RCTParagraphComponentUnfocusableTextView + +- (BOOL)canBecomeKeyView +{ + return NO; +} + +- (BOOL)resignFirstResponder +{ + // Don't relinquish first responder while selecting text. + if (self.selectable && NSRunLoop.currentRunLoop.currentMode == NSEventTrackingRunLoopMode) { + return NO; + } + + return [super resignFirstResponder]; +} + +@end + +@interface RCTParagraphComponentView () +@end +#endif // macOS] @implementation RCTParagraphComponentView { ParagraphShadowNode::ConcreteState::Shared _state; @@ -144,7 +170,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & const auto &newParagraphProps = static_cast(*props); _paragraphAttributes = newParagraphProps.paragraphAttributes; +#if !TARGET_OS_OSX // [macOS] _textView.paragraphAttributes = _paragraphAttributes; +#endif // [macOS] if (newParagraphProps.isSelectable != oldParagraphProps.isSelectable) { #if !TARGET_OS_OSX // [macOS] @@ -164,8 +192,12 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState { _state = std::static_pointer_cast(state); +#if !TARGET_OS_OSX // [macOS] _textView.state = _state; [_textView setNeedsDisplay]; +#else // [macOS + [self _updateTextView]; +#endif // macOS] [self setNeedsLayout]; } @@ -175,16 +207,64 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics // Using stored `_layoutMetrics` as `oldLayoutMetrics` here to avoid // re-applying individual sub-values which weren't changed. [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics]; +#if !TARGET_OS_OSX // [macOS] _textView.layoutMetrics = _layoutMetrics; [_textView setNeedsDisplay]; +#else // [macOS + [self _updateTextView]; +#endif // macOS] [self setNeedsLayout]; } +#if TARGET_OS_OSX // [macOS +- (void)_updateTextView +{ + if (!_state) { + return; + } + + auto textLayoutManager = _state->getData().layoutManager.lock(); + + if (!textLayoutManager) { + return; + } + + RCTTextLayoutManager *nativeTextLayoutManager = + (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); + + CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); + + NSTextStorage *textStorage = [nativeTextLayoutManager getTextStorageForAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes frame:frame]; + + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; + + [_textView replaceTextContainer:textContainer]; + + NSArray *managers = [[textStorage layoutManagers] copy]; + for (NSLayoutManager *manager in managers) { + [textStorage removeLayoutManager:manager]; + } + + _textView.minSize = frame.size; + _textView.maxSize = frame.size; + _textView.frame = frame; + _textView.textStorage.attributedString = textStorage; + + [self setNeedsDisplay]; +} +#endif // macOS] + - (void)prepareForRecycle { [super prepareForRecycle]; _state.reset(); _accessibilityProvider = nil; + +#if TARGET_OS_OSX // [macOS + // Clear the text view to avoid displaying the previous text on recycle with undefined text content. + _textView.string = @""; +#endif // macOS] } - (void)layoutSubviews @@ -205,6 +285,7 @@ - (NSString *)accessibilityLabel return self.attributedText.string; } +#if !TARGET_OS_OSX // [macOS] - (BOOL)isAccessibilityElement { // All accessibility functionality of the component is implemented in `accessibilityElements` method below. @@ -213,7 +294,6 @@ - (BOOL)isAccessibilityElement return NO; } -#if !TARGET_OS_OSX // [macOS] - (NSArray *)accessibilityElements { const auto ¶graphProps = static_cast(*_props); @@ -282,9 +362,9 @@ - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point return std::static_pointer_cast(eventEmitter); } +#if !TARGET_OS_OSX // [macOS] #pragma mark - Context Menu -#if !TARGET_OS_OSX // [macOS] - (void)enableContextMenu { _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self @@ -325,14 +405,124 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture [menuController showMenuFromView:self rect:self.bounds]; } } -#endif // [macOS] +#else // [macOS +- (NSView *)hitTest:(CGPoint)point withEvent:(NSEvent *)event +{ + // We will forward mouse click events to the NSTextView ourselves to prevent NSTextView from swallowing events that may be handled in JS (e.g. long press). + NSView *hitView = [super hitTest:point withEvent:event]; + + NSEventType eventType = NSApp.currentEvent.type; + BOOL isMouseClickEvent = NSEvent.pressedMouseButtons > 0; + BOOL isMouseMoveEventType = eventType == NSEventTypeMouseMoved || eventType == NSEventTypeMouseEntered || eventType == NSEventTypeMouseExited || eventType == NSEventTypeCursorUpdate; + BOOL isMouseMoveEvent = !isMouseClickEvent && isMouseMoveEventType; + BOOL isTextViewClick = (hitView && hitView == _textView) && !isMouseMoveEvent; + + return isTextViewClick ? self : hitView; +} + +- (NSView *)hitTest:(NSPoint)point +{ + return [self hitTest:point withEvent:NSApp.currentEvent]; +} + +- (void)mouseDown:(NSEvent *)event +{ + if (!_textView.selectable) { + [super mouseDown:event]; + return; + } + + // Double/triple-clicks should be forwarded to the NSTextView. + BOOL shouldForward = event.clickCount > 1; + + if (!shouldForward) { + // Peek at next event to know if a selection should begin. + NSEvent *nextEvent = [self.window nextEventMatchingMask:NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged + untilDate:[NSDate distantFuture] + inMode:NSEventTrackingRunLoopMode + dequeue:NO]; + shouldForward = nextEvent.type == NSEventTypeLeftMouseDragged; + } + + if (shouldForward) { + NSView *contentView = self.window.contentView; + // -[NSView hitTest:] takes coordinates in a view's superview coordinate system. + NSPoint point = [contentView.superview convertPoint:event.locationInWindow fromView:nil]; + + // Start selection if we're still selectable and hit-testable. + if (_textView.selectable && [contentView hitTest:point] == self) { + [[RCTSurfaceTouchHandler surfaceTouchHandlerForView:self] cancelTouchWithEvent:event]; + [self.window makeFirstResponder:_textView]; + [_textView mouseDown:event]; + } + } else { + // Clear selection for single clicks. + _textView.selectedRange = NSMakeRange(NSNotFound, 0); + } +} + +- (void)rightMouseDown:(NSEvent *)event +{ + auto const &props = *std::static_pointer_cast(_props); + if (!props.isSelectable) { + [super rightMouseDown:event]; + return; + } + + [_textView rightMouseDown:event]; +} + +#pragma mark - NSTextViewDelegate + +- (NSMenu *)textView:(NSTextView *)view menu:(NSMenu *)menu forEvent:(NSEvent *)event atIndex:(NSUInteger)charIndex +{ + 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; +} + +#pragma mark - Selection + +- (void)textDidEndEditing:(NSNotification *)notification +{ + _textView.selectedRange = NSMakeRange(NSNotFound, 0); +} +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] - (BOOL)canBecomeFirstResponder { const auto ¶graphProps = static_cast(*_props); return paragraphProps.isSelectable; } +#else +- (BOOL)becomeFirstResponder +{ + if (![super becomeFirstResponder]) { + return NO; + } + + return YES; +} +- (BOOL)canBecomeFirstResponder +{ + return self.focusable; +} +#endif // macOS] + +#if !TARGET_OS_OSX // [macOS] - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { const auto ¶graphProps = static_cast(*_props); @@ -347,6 +537,7 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return NO; #endif // macOS] } +#endif // [macOS] - (void)copy:(id)sender { @@ -390,6 +581,9 @@ @implementation RCTParagraphTextView { - (void)drawRect:(CGRect)rect { +#if TARGET_OS_OSX // [macOS + return; +#else // [macOS] if (!_state) { return; } 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 a9b58ed96267b5..eb46761cf83170 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -13,9 +13,18 @@ #import #import +#if !TARGET_OS_OSX // [macOS] #import +#else // [macOS +#include +#include +#endif // macOS] #import #import +#if TARGET_OS_OSX // [macOS +#import +#import +#endif // macOS] #import "RCTConversions.h" #import "RCTTextInputNativeCommands.h" @@ -101,6 +110,11 @@ - (void)didMoveToWindow if (props.autoFocus) { #if !TARGET_OS_OSX // [macOS] [_backedTextInputView becomeFirstResponder]; +#else // [macOS + NSWindow *window = _backedTextInputView.window; + if (window) { + [window makeFirstResponder:_backedTextInputView.responder]; + } #endif // [macOS] } _didMoveToWindow = YES; @@ -236,11 +250,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _backedTextInputView.scrollEnabled = newTextInputProps.traits.scrollEnabled; } -#if !TARGET_OS_OSX // [macOS] if (newTextInputProps.traits.secureTextEntry != oldTextInputProps.traits.secureTextEntry) { +#if !TARGET_OS_OSX // [macOS] _backedTextInputView.secureTextEntry = newTextInputProps.traits.secureTextEntry; +#else // [macOS + [self _setSecureTextEntry:newTextInputProps.traits.secureTextEntry]; +#endif // macOS] } +#if !TARGET_OS_OSX // [macOS] if (newTextInputProps.traits.keyboardType != oldTextInputProps.traits.keyboardType) { _backedTextInputView.keyboardType = RCTUIKeyboardTypeFromKeyboardType(newTextInputProps.traits.keyboardType); } @@ -547,8 +565,28 @@ - (BOOL)textInputShouldHandlePaste:(nonnull id)s - (void)scrollViewDidScroll:(RCTUIScrollView *)scrollView // [macOS] { if (_eventEmitter) { - static_cast(*_eventEmitter).onScroll([self _textInputMetrics]); - } + + TextInputEventEmitter::Metrics metrics = [self _textInputMetrics]; // [macOS] + +#if TARGET_OS_OSX // [macOS + CGPoint contentOffset = scrollView.contentOffset; + metrics.contentOffset = {contentOffset.x, contentOffset.y}; + + UIEdgeInsets contentInset = scrollView.contentInset; + metrics.contentInset = {contentInset.left, contentInset.top, contentInset.right, contentInset.bottom}; + + CGSize contentSize = scrollView.contentSize; + metrics.contentSize = {contentSize.width, contentSize.height}; + + CGSize layoutMeasurement = scrollView.bounds.size; + metrics.layoutMeasurement = {layoutMeasurement.width, layoutMeasurement.height}; + + CGFloat zoomScale = scrollView.zoomScale ?: 1; + metrics.zoomScale = zoomScale; +#endif // macOS] + + std::static_pointer_cast(_eventEmitter)->onScroll(metrics); // [macOS] +} } #pragma mark - Native Commands @@ -618,6 +656,10 @@ - (void)setTextAndSelection:(NSInteger)eventCount UITextRange *range = [_backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition]; [_backedTextInputView setSelectedTextRange:range notifyDelegate:NO]; } +#else // [macOS + NSInteger startPosition = MIN(start, end); + NSInteger endPosition = MAX(start, end); + [_backedTextInputView setSelectedTextRange:NSMakeRange(startPosition, endPosition - startPosition) notifyDelegate:YES]; #endif // [macOS] _comingFromJS = NO; } @@ -767,8 +809,8 @@ - (void)_updateState toPosition:selectedTextRange.end]; return AttributedString::Range{(int)start, (int)(end - start)}; #else // [macOS - // [Fabric] Placeholder till we implement selection in Fabric - return AttributedString::Range({0, 1}); + NSRange selectedTextRange = [_backedTextInputView selectedTextRange]; + return AttributedString::Range{(int)selectedTextRange.location, (int)selectedTextRange.length}; #endif // macOS] } @@ -789,13 +831,25 @@ - (void)_restoreTextSelection - (void)_setAttributedString:(NSAttributedString *)attributedString { +#if TARGET_OS_OSX // [macOS + // When the text view displays temporary content (e.g. completions, accents), do not update the attributed string. + if (_backedTextInputView.hasMarkedText) { + return; + } +#endif // macOS] + if ([self _textOf:attributedString equals:_backedTextInputView.attributedText]) { return; } #if !TARGET_OS_OSX // [macOS] UITextRange *selectedRange = _backedTextInputView.selectedTextRange; +#else + NSRange selection = [_backedTextInputView selectedTextRange]; +#endif // macOS] + NSAttributedString *oldAttributedText = [_backedTextInputView.attributedText copy]; NSInteger oldTextLength = _backedTextInputView.attributedText.string.length; _backedTextInputView.attributedText = attributedString; +#if !TARGET_OS_OSX // [macOS] // Updating the UITextView attributedText, for example changing the lineHeight, the color or adding // a new paragraph with \n, causes the cursor to move to the end of the Text and scroll. // This is fixed by restoring the cursor position and scrolling to that position (iOS issue 652653). @@ -822,7 +876,7 @@ - (void)_setMultiline:(BOOL)multiline #if !TARGET_OS_OSX // [macOS] RCTUIView *backedTextInputView = multiline ? [RCTUITextView new] : [RCTUITextField new]; #else // [macOS - RCTUITextView *backedTextInputView = [RCTUITextView new]; + RCTPlatformView *backedTextInputView = multiline ? [RCTWrappedTextView new] : [RCTUITextField new]; #endif // macOS] backedTextInputView.frame = _backedTextInputView.frame; RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView); @@ -849,6 +903,27 @@ - (void)_setShowSoftInputOnFocus:(BOOL)showSoftInputOnFocus } #endif // macOS] +#if TARGET_OS_OSX // [macOS +- (void)_setSecureTextEntry:(BOOL)secureTextEntry +{ + [_backedTextInputView removeFromSuperview]; + RCTPlatformView *backedTextInputView = secureTextEntry ? [RCTUISecureTextField new] : [RCTUITextField new]; + backedTextInputView.frame = _backedTextInputView.frame; + RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView); + + // Copy the text field specific properties if we came from a single line input before the switch + if ([_backedTextInputView isKindOfClass:[RCTUITextField class]]) { + RCTUITextField *previousTextField = (RCTUITextField *)_backedTextInputView; + RCTUITextField *newTextField = (RCTUITextField *)backedTextInputView; + newTextField.textAlignment = previousTextField.textAlignment; + newTextField.text = previousTextField.text; + } + + _backedTextInputView = backedTextInputView; + [self addSubview:_backedTextInputView]; +} +#endif // macOS] + - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText { // When the dictation is running we can't update the attributed text on the backed up text view diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h index 399384b852a51e..11368be4f2818e 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.h @@ -22,8 +22,8 @@ void RCTCopyBackedTextInput( RCTUIView *fromTextInput, RCTUIView *toTextInput #else // [macOS - RCTUITextView *fromTextInput, - RCTUITextView *toTextInput + RCTPlatformView *fromTextInput, + RCTPlatformView *toTextInput #endif // macOS] ); diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm index 610ba216c69276..bc328927347b03 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm @@ -23,8 +23,8 @@ void RCTCopyBackedTextInput( RCTUIView *fromTextInput, RCTUIView *toTextInput #else // [macOS - RCTUITextView *fromTextInput, - RCTUITextView *toTextInput + RCTPlatformView *fromTextInput, + RCTPlatformView *toTextInput #endif // macOS] ) { @@ -32,6 +32,15 @@ void RCTCopyBackedTextInput( toTextInput.placeholder = fromTextInput.placeholder; toTextInput.placeholderColor = fromTextInput.placeholderColor; toTextInput.textContainerInset = fromTextInput.textContainerInset; + +#if TARGET_OS_OSX // [macOS + toTextInput.accessibilityElement = fromTextInput.accessibilityElement; + toTextInput.accessibilityHelp = fromTextInput.accessibilityHelp; + toTextInput.accessibilityIdentifier = fromTextInput.accessibilityIdentifier; + toTextInput.accessibilityLabel = fromTextInput.accessibilityLabel; + toTextInput.accessibilityRole = fromTextInput.accessibilityRole; + toTextInput.autoresizingMask = fromTextInput.autoresizingMask; +#endif // macOS] #if TARGET_OS_IOS // [macOS] [visionOS] toTextInput.inputAccessoryView = fromTextInput.inputAccessoryView; #endif // [macOS] [visionOS] diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h index 7a903401112fa8..ed0746f3f14307 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -75,6 +75,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask NS_REQUIRES_SUPER; - (void)prepareForRecycle NS_REQUIRES_SUPER; +#if TARGET_OS_OSX // [macOS +- (BOOL)handleKeyboardEvent:(NSEvent *)event; +- (void)buildDataTransferItems:(std::vector &)dataTransferItems forPasteboard:(NSPasteboard *)pasteboard; +#endif // macOS] + /* * This is a fragment of temporary workaround that we need only temporary and will get rid of soon. */ diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 2f3b7049f8d55a..d50b21515be3a5 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -10,13 +10,19 @@ #import #import #import +#import #import #import #import #import +#import #import // [macOS] #import +#if TARGET_OS_OSX // [macOS +#import // [macOS] +#import // [macOS] +#endif // macOS] #import #import #import @@ -38,10 +44,13 @@ @implementation RCTViewComponentView { __weak CALayer *_borderLayer; CALayer *_boxShadowLayer; CALayer *_filterLayer; - NSMutableArray *_gradientLayers; + NSMutableArray *_backgroundImageLayers; BOOL _needsInvalidateLayer; BOOL _isJSResponder; BOOL _removeClippedSubviews; + BOOL _hasMouseOver; // [macOS] + BOOL _hasClipViewBoundsObserver; // [macOS] + NSTrackingArea *_trackingArea; // [macOS] NSMutableArray *_reactSubviews; // [macOS] NSSet *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN; RCTPlatformView *_containerView; // [macOS] @@ -62,6 +71,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; } @@ -139,7 +151,7 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection - (void)viewDidChangeEffectiveAppearance { [super viewDidChangeEffectiveAppearance]; - + [self invalidateLayer]; } #endif // macOS] @@ -300,7 +312,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; @@ -548,6 +560,49 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & needsInvalidateLayer = YES; } +#if TARGET_OS_OSX // [macOS + // `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 new]; + for (const auto &draggedType : newViewProps.draggedTypes) { + if (draggedType == "fileUrl") { + [pasteboardTypes addObject:NSFilenamesPboardType]; + } else if (draggedType == "image") { + [pasteboardTypes addObject:NSPasteboardTypePNG]; + [pasteboardTypes addObject:NSPasteboardTypeTIFF]; + } else if (draggedType == "string") { + [pasteboardTypes addObject:NSPasteboardTypeString]; + } + } + [self registerForDraggedTypes:pasteboardTypes]; + } + } + + // `tooltip` + if (oldViewProps.tooltip != newViewProps.tooltip) { + if (newViewProps.tooltip.has_value()) { + self.toolTip = RCTNSStringFromStringNilIfEmpty(newViewProps.tooltip.value()); + } else { + self.toolTip = nil; + } + } +#endif // macOS] + _needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer; _props = std::static_pointer_cast(props); @@ -604,6 +659,10 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask { [super finalizeUpdates:updateMask]; _useCustomContainerView = [self styleWouldClipOverflowInk]; + + [self updateTrackingAreas]; + [self updateClipViewBoundsObserverIfNeeded]; + if (!_needsInvalidateLayer) { return; } @@ -817,8 +876,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: @@ -873,6 +930,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; @@ -905,7 +973,7 @@ - (void)invalidateLayer } else { layer.shadowPath = nil; } - + #if !TARGET_OS_OSX // [visionOS] // Stage 1.5. Cursor / Hover Effects if (@available(iOS 17.0, *)) { @@ -926,7 +994,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]; @@ -1017,7 +1085,17 @@ - (void)invalidateLayer layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left; RCTUIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left); 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); @@ -1117,50 +1195,35 @@ - (void)invalidateLayer } // background image - [self clearExistingGradientLayers]; + [self clearExistingBackgroundImageLayers]; if (!_props->backgroundImage.empty()) { - for (const auto &gradient : _props->backgroundImage) { - CAGradientLayer *gradientLayer = [CAGradientLayer layer]; - NSMutableArray *colors = [NSMutableArray array]; - NSMutableArray *locations = [NSMutableArray array]; - for (const auto &colorStop : gradient.colorStops) { - if (colorStop.position.has_value()) { - auto location = @(colorStop.position.value()); - RCTUIColor *color = RCTUIColorFromSharedColor(colorStop.color); // [macOS] - [colors addObject:(id)color.CGColor]; - [locations addObject:location]; + // iterate in reverse to match CSS specification + for (const auto &backgroundImage : std::ranges::reverse_view(_props->backgroundImage)) { + if (std::holds_alternative(backgroundImage)) { + const auto &linearGradient = std::get(backgroundImage); + CALayer *backgroundImageLayer = [RCTLinearGradient gradientLayerWithSize:self.layer.bounds.size + gradient:linearGradient]; + 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; } - } - gradientLayer.startPoint = CGPointMake(gradient.startX, gradient.startY); - gradientLayer.endPoint = CGPointMake(gradient.endX, gradient.endY); - - if (locations.count > 0) { - gradientLayer.locations = locations; - } - gradientLayer.colors = colors; - gradientLayer.frame = layer.bounds; - - // border styling to work with gradient layers - if (useCoreAnimationBorderRendering) { - gradientLayer.borderWidth = layer.borderWidth; - gradientLayer.borderColor = layer.borderColor; - gradientLayer.cornerRadius = layer.cornerRadius; - gradientLayer.cornerCurve = layer.cornerCurve; - } else { - CAShapeLayer *maskLayer = [CAShapeLayer layer]; - CGPathRef path = RCTPathCreateWithRoundedRect( - self.bounds, - RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero), - nil); - maskLayer.path = path; - CGPathRelease(path); - gradientLayer.mask = maskLayer; - } - gradientLayer.zPosition = BACKGROUND_COLOR_ZPOSITION; + backgroundImageLayer.zPosition = BACKGROUND_COLOR_ZPOSITION; - [self.layer addSublayer:gradientLayer]; - [_gradientLayers addObject:gradientLayer]; + [self.layer addSublayer:backgroundImageLayer]; + [_backgroundImageLayers addObject:backgroundImageLayer]; + } } } @@ -1232,17 +1295,44 @@ - (CAShapeLayer *)createMaskLayer:(CGRect)bounds cornerInsets:(RCTCornerInsets)c return maskLayer; } -- (void)clearExistingGradientLayers +- (void)clearExistingBackgroundImageLayers { - if (_gradientLayers == nil) { - _gradientLayers = [NSMutableArray new]; + if (_backgroundImageLayers == nil) { + _backgroundImageLayers = [NSMutableArray new]; return; } - for (CAGradientLayer *gradientLayer in _gradientLayers) { - [gradientLayer removeFromSuperlayer]; + for (CALayer *backgroundImageLayer in _backgroundImageLayers) { + [backgroundImageLayer removeFromSuperlayer]; + } + [_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]; } - [_gradientLayers removeAllObjects]; } +#endif // macOS] + #pragma mark - Accessibility @@ -1427,6 +1517,419 @@ - (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)acti } } + +#if TARGET_OS_OSX // [macOS + +#pragma mark - Focus Events + +- (BOOL)becomeFirstResponder +{ + if (![super becomeFirstResponder]) { + return NO; + } + + if (_eventEmitter) { + _eventEmitter->onFocus(); + } + + return YES; +} + +- (BOOL)resignFirstResponder +{ + if (![super resignFirstResponder]) { + return NO; + } + + if (_eventEmitter) { + _eventEmitter->onBlur(); + } + + return YES; +} + + +#pragma mark - Keyboard Events + +- (BOOL)handleKeyboardEvent:(NSEvent *)event { + BOOL keyDown = event.type == NSEventTypeKeyDown; + BOOL hasHandler = keyDown ? _props->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 = " " } }; + } + + // If there are no valid keys defined, no key event handling is required. + if (!validKeys.has_value()) { + return NO; + } + + // Convert the event to a KeyEvent + NSEventModifierFlags modifierFlags = event.modifierFlags; + KeyEvent keyEvent = { + .key = [[RCTViewKeyboardEvent keyFromEvent:event] UTF8String], + .altKey = static_cast(modifierFlags & NSEventModifierFlagOption), + .ctrlKey = static_cast(modifierFlags & NSEventModifierFlagControl), + .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), + .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), + .capsLockKey = static_cast(modifierFlags & NSEventModifierFlagCapsLock), + .numericPadKey = static_cast(modifierFlags & NSEventModifierFlagNumericPad), + .helpKey = static_cast(modifierFlags & NSEventModifierFlagHelp), + .functionKey = static_cast(modifierFlags & NSEventModifierFlagFunction), + }; + + BOOL shouldBlock = NO; + for (auto const &validKey : *validKeys) { + if (keyEvent == validKey) { + shouldBlock = YES; + break; + } + } + + if (_eventEmitter && shouldBlock) { + if (keyDown) { + _eventEmitter->onKeyDown(keyEvent); + } else { + _eventEmitter->onKeyUp(keyEvent); + } + return YES; + } + } + + return NO; +} + +- (void)keyDown:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyDown:event]; + } +} + +- (void)keyUp:(NSEvent *)event { + if (![self handleKeyboardEvent:event]) { + [super keyUp:event]; + } +} + + +#pragma mark - Drag and Drop Events + +enum DragEventType { + DragEnter, + DragLeave, + Drop, +}; + +- (void)buildDataTransferItems:(std::vector &)dataTransferItems forPasteboard:(NSPasteboard *)pasteboard { + NSArray *fileNames = [pasteboard propertyListForType:NSFilenamesPboardType] ?: @[]; + for (NSString *file in fileNames) { + NSURL *fileURL = [NSURL fileURLWithPath:file]; + BOOL isDir = NO; + BOOL isValid = (![[NSFileManager defaultManager] fileExistsAtPath:fileURL.path isDirectory:&isDir] || isDir) ? NO : YES; + if (isValid) { + + NSString *MIMETypeString = nil; + if (fileURL.pathExtension) { + CFStringRef fileExtension = (__bridge CFStringRef)fileURL.pathExtension; + CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, NULL); + if (UTI != NULL) { + CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType); + CFRelease(UTI); + MIMETypeString = (__bridge_transfer NSString *)MIMEType; + } + } + + NSNumber *fileSizeValue = nil; + NSError *fileSizeError = nil; + BOOL success = [fileURL getResourceValue:&fileSizeValue + forKey:NSURLFileSizeKey + error:&fileSizeError]; + + NSNumber *width = nil; + NSNumber *height = nil; + if ([MIMETypeString hasPrefix:@"image/"]) { + NSImage *image = [[NSImage alloc] initWithContentsOfURL:fileURL]; + width = @(image.size.width); + height = @(image.size.height); + } + + DataTransferItem transferItem = { + .name = fileURL.lastPathComponent.UTF8String, + .kind = "file", + .type = MIMETypeString.UTF8String, + .uri = fileURL.path.UTF8String, + }; + + if (success) { + transferItem.size = fileSizeValue.intValue; + } + + if (width != nil) { + transferItem.width = width.intValue; + } + + if (height != nil) { + transferItem.height = height.intValue; + } + + dataTransferItems.push_back(transferItem); + } + } + + NSPasteboardType imageType = [pasteboard availableTypeFromArray:@[NSPasteboardTypePNG, NSPasteboardTypeTIFF]]; + if (imageType && fileNames.count == 0) { + NSString *MIMETypeString = imageType == NSPasteboardTypePNG ? @"image/png" : @"image/tiff"; + NSData *imageData = [pasteboard dataForType:imageType]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + DataTransferItem transferItem = { + .kind = "image", + .type = MIMETypeString.UTF8String, + .uri = RCTDataURL(MIMETypeString, imageData).absoluteString.UTF8String, + .size = imageData.length, + .width = image.size.width, + .height = image.size.height, + }; + + dataTransferItems.push_back(transferItem); + } +} + +- (void)sendDragEvent:(DragEventType)eventType withLocation:(NSPoint)locationInWindow pasteboard:(NSPasteboard *)pasteboard { + if (!_eventEmitter) { + return; + } + + std::vector dataTransferItems{}; + [self buildDataTransferItems:dataTransferItems forPasteboard:pasteboard]; + + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + NSEventModifierFlags modifierFlags = self.window.currentEvent.modifierFlags; + + DragEvent dragEvent = { + { + .clientX = locationInView.x, + .clientY = locationInView.y, + .screenX = locationInWindow.x, + .screenY = locationInWindow.y, + .altKey = static_cast(modifierFlags & NSEventModifierFlagOption), + .ctrlKey = static_cast(modifierFlags & NSEventModifierFlagControl), + .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), + .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), + }, + .dataTransferItems = dataTransferItems, + }; + + switch (eventType) { + case DragEnter: + _eventEmitter->onDragEnter(dragEvent); + break; + + case DragLeave: + _eventEmitter->onDragLeave(dragEvent); + break; + + case Drop: + _eventEmitter->onDrop(dragEvent); + break; + } +} + +- (NSDragOperation)draggingEntered:(id )sender +{ + NSPasteboard *pboard = sender.draggingPasteboard; + NSDragOperation sourceDragMask = sender.draggingSourceOperationMask; + + [self sendDragEvent:DragEnter withLocation:sender.draggingLocation pasteboard:pboard]; + + if ([pboard availableTypeFromArray:self.registeredDraggedTypes]) { + if (sourceDragMask & NSDragOperationLink) { + return NSDragOperationLink; + } else if (sourceDragMask & NSDragOperationCopy) { + return NSDragOperationCopy; + } + } + return NSDragOperationNone; +} + +- (void)draggingExited:(id)sender +{ + [self sendDragEvent:DragLeave withLocation:sender.draggingLocation pasteboard:sender.draggingPasteboard]; +} + +- (BOOL)performDragOperation:(id )sender +{ + [self sendDragEvent:Drop withLocation:sender.draggingLocation pasteboard:sender.draggingPasteboard]; + return YES; +} + + +#pragma mark - Mouse Events + +enum MouseEventType { + MouseEnter, + MouseLeave, + DoubleClick, +}; + +- (void)sendMouseEvent:(MouseEventType)eventType { + if (!_eventEmitter) { + return; + } + + NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream; + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + + NSEventModifierFlags modifierFlags = self.window.currentEvent.modifierFlags; + + MouseEvent mouseEvent = { + .clientX = locationInView.x, + .clientY = locationInView.y, + .screenX = locationInWindow.x, + .screenY = locationInWindow.y, + .altKey = static_cast(modifierFlags & NSEventModifierFlagOption), + .ctrlKey = static_cast(modifierFlags & NSEventModifierFlagControl), + .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), + .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), + }; + + switch (eventType) { + case MouseEnter: + _eventEmitter->onMouseEnter(mouseEvent); + break; + case MouseLeave: + _eventEmitter->onMouseLeave(mouseEvent); + break; + case DoubleClick: + _eventEmitter->onDoubleClick(mouseEvent); + break; + } +} + +- (void)updateMouseOverIfNeeded +{ + // When an enclosing scrollview is scrolled using the scrollWheel or trackpad, + // the mouseExited: event does not get called on the view where mouseEntered: was previously called. + // This creates an unnatural pairing of mouse enter and exit events and can cause problems. + // We therefore explicitly check for this here and handle them by calling the appropriate callbacks. + + BOOL hasMouseOver = _hasMouseOver; + NSPoint locationInWindow = self.window.mouseLocationOutsideOfEventStream; + NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil]; + BOOL insideBounds = NSPointInRect(locationInView, self.visibleRect); + + // On macOS 14.0 visibleRect can be larger than the view bounds + insideBounds &= NSPointInRect(locationInView, self.bounds); + + if (hasMouseOver && !insideBounds) { + hasMouseOver = NO; + } else if (!hasMouseOver && insideBounds) { + // The window's frame view must be used for hit testing against `locationInWindow` + NSView *hitView = [self.window.contentView.superview hitTest:locationInWindow]; + hasMouseOver = [hitView isDescendantOf:self]; + } + + if (hasMouseOver != _hasMouseOver) { + _hasMouseOver = hasMouseOver; + [self sendMouseEvent:hasMouseOver ? MouseEnter : MouseLeave]; + } +} + +- (void)updateClipViewBoundsObserverIfNeeded +{ + // Subscribe to view bounds changed notification so that the view can be notified when a + // scroll event occurs either due to trackpad/gesture based scrolling or a scrollwheel event + // both of which would not cause the mouseExited to be invoked. + + NSClipView *clipView = self.window ? self.enclosingScrollView.contentView : nil; + + BOOL hasMouseEventHandler = _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseEnter] || + _props->hostPlatformEvents[HostPlatformViewEvents::Offset::MouseLeave]; + + if (_hasClipViewBoundsObserver && (!clipView || !hasMouseEventHandler)) { + _hasClipViewBoundsObserver = NO; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSViewBoundsDidChangeNotification + object:nil]; + } else if (!_hasClipViewBoundsObserver && clipView && hasMouseEventHandler) { + _hasClipViewBoundsObserver = YES; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateMouseOverIfNeeded) + name:NSViewBoundsDidChangeNotification + object:clipView]; + [self updateMouseOverIfNeeded]; + } +} + +- (void)viewDidMoveToWindow +{ + [self updateClipViewBoundsObserverIfNeeded]; + [super viewDidMoveToWindow]; +} + +- (void)updateTrackingAreas +{ + if (_trackingArea) { + [self removeTrackingArea:_trackingArea]; + } + + if ( + _props->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) { + return; + } + + // The window's frame view must be used for hit testing against `locationInWindow` + NSView *hitView = [self.window.contentView.superview hitTest:event.locationInWindow]; + if (![hitView isDescendantOf:self]) { + return; + } + + _hasMouseOver = YES; + [self sendMouseEvent:MouseEnter]; +} + +- (void)mouseExited:(NSEvent *)event +{ + if (!_hasMouseOver) { + return; + } + + _hasMouseOver = NO; + [self sendMouseEvent:MouseLeave]; +} +#endif // macOS] + - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point { return _eventEmitter; @@ -1437,6 +1940,11 @@ - (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN return RCTNSStringFromString([[self class] componentDescriptorProvider].name); } +- (BOOL)wantsUpdateLayer +{ + return YES; +} + @end #ifdef __cplusplus diff --git a/packages/react-native/React/Fabric/Mounting/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/Mounting/RCTMountingManager.h b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.h index 22c0585a9cf832..40025b7ccd1358 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.h +++ b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.h @@ -48,7 +48,7 @@ NS_ASSUME_NONNULL_BEGIN * Schedule a mounting transaction to be performed on the main thread. * Can be called from any thread. */ -- (void)scheduleTransaction:(facebook::react::MountingCoordinator::Shared)mountingCoordinator; +- (void)scheduleTransaction:(std::shared_ptr)mountingCoordinator; /** * Dispatch a command to be performed on the main thread. diff --git a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm index ded9d1d45cedad..454d040046be13 100644 --- a/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm +++ b/packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm @@ -15,12 +15,10 @@ #import #import #import -#import #import #import #import #import -#import #import #import @@ -74,9 +72,8 @@ static void RCTPerformMountInstructions( case ShadowViewMutation::Insert: { auto &newChildShadowView = mutation.newChildShadowView; - auto &parentShadowView = mutation.parentShadowView; auto &newChildViewDescriptor = [registry componentViewDescriptorWithTag:newChildShadowView.tag]; - auto &parentViewDescriptor = [registry componentViewDescriptorWithTag:parentShadowView.tag]; + auto &parentViewDescriptor = [registry componentViewDescriptorWithTag:mutation.parentTag]; RCTUIView *newChildComponentView = newChildViewDescriptor.view; // [macOS] @@ -95,9 +92,8 @@ static void RCTPerformMountInstructions( case ShadowViewMutation::Remove: { auto &oldChildShadowView = mutation.oldChildShadowView; - auto &parentShadowView = mutation.parentShadowView; auto &oldChildViewDescriptor = [registry componentViewDescriptorWithTag:oldChildShadowView.tag]; - auto &parentViewDescriptor = [registry componentViewDescriptorWithTag:parentShadowView.tag]; + auto &parentViewDescriptor = [registry componentViewDescriptorWithTag:mutation.parentTag]; [parentViewDescriptor.view unmountChildComponentView:oldChildViewDescriptor.view index:mutation.index]; break; } @@ -187,7 +183,7 @@ - (void)detachSurfaceFromView:(RCTUIView *)view surfaceId:(SurfaceId)surfaceId / componentViewDescriptor:rootViewDescriptor]; } -- (void)scheduleTransaction:(MountingCoordinator::Shared)mountingCoordinator +- (void)scheduleTransaction:(std::shared_ptr)mountingCoordinator { if (RCTIsMainQueue()) { // Already on the proper thread, so: diff --git a/packages/react-native/React/Fabric/RCTScheduler.h b/packages/react-native/React/Fabric/RCTScheduler.h index 123961b8d86e2b..ffeb090ecc072e 100644 --- a/packages/react-native/React/Fabric/RCTScheduler.h +++ b/packages/react-native/React/Fabric/RCTScheduler.h @@ -26,9 +26,9 @@ NS_ASSUME_NONNULL_BEGIN */ @protocol RCTSchedulerDelegate -- (void)schedulerDidFinishTransaction:(facebook::react::MountingCoordinator::Shared)mountingCoordinator; +- (void)schedulerDidFinishTransaction:(std::shared_ptr)mountingCoordinator; -- (void)schedulerShouldRenderTransactions:(facebook::react::MountingCoordinator::Shared)mountingCoordinator; +- (void)schedulerShouldRenderTransactions:(std::shared_ptr)mountingCoordinator; - (void)schedulerDidDispatchCommand:(const facebook::react::ShadowView &)shadowView commandName:(const std::string &)commandName diff --git a/packages/react-native/React/Fabric/RCTScheduler.mm b/packages/react-native/React/Fabric/RCTScheduler.mm index 29ea406343dde1..95340e0ed9eda9 100644 --- a/packages/react-native/React/Fabric/RCTScheduler.mm +++ b/packages/react-native/React/Fabric/RCTScheduler.mm @@ -26,13 +26,13 @@ public: SchedulerDelegateProxy(void *scheduler) : scheduler_(scheduler) {} - void schedulerDidFinishTransaction(const MountingCoordinator::Shared &mountingCoordinator) override + void schedulerDidFinishTransaction(const std::shared_ptr &mountingCoordinator) override { RCTScheduler *scheduler = (__bridge RCTScheduler *)scheduler_; [scheduler.delegate schedulerDidFinishTransaction:mountingCoordinator]; } - void schedulerShouldRenderTransactions(const MountingCoordinator::Shared &mountingCoordinator) override + void schedulerShouldRenderTransactions(const std::shared_ptr &mountingCoordinator) override { RCTScheduler *scheduler = (__bridge RCTScheduler *)scheduler_; [scheduler.delegate schedulerShouldRenderTransactions:mountingCoordinator]; @@ -108,15 +108,12 @@ @implementation RCTScheduler { std::shared_ptr _animationDriver; std::shared_ptr _delegateProxy; std::shared_ptr _layoutAnimationDelegateProxy; - RunLoopObserver::Unique _uiRunLoopObserver; + std::unique_ptr _uiRunLoopObserver; } - (instancetype)initWithToolbox:(SchedulerToolbox)toolbox { if (self = [super init]) { - auto reactNativeConfig = - toolbox.contextContainer->at>("ReactNativeConfig"); - _delegateProxy = std::make_shared((__bridge void *)self); if (ReactNativeFeatureFlags::enableLayoutAnimationsOnIOS()) { diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm index 6f4f15c09a8c46..f510d8b8d1e7e6 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm @@ -26,16 +26,14 @@ #import #import -#import #import #import #import #import -#import #import #import -#import #import +#import "AppleEventBeat.h" #import "PlatformRunLoopObserver.h" #import "RCTConversions.h" @@ -229,16 +227,6 @@ - (BOOL)resume - (RCTScheduler *)_createScheduler { - auto reactNativeConfig = _contextContainer->at>("ReactNativeConfig"); - - if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:enable_cpp_props_iterator_setter_ios")) { - CoreFeatures::enablePropIteratorSetter = true; - } - - if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:enable_granular_scroll_view_state_updates_ios")) { - CoreFeatures::enableGranularScrollViewStateUpdatesIOS = true; - } - auto componentRegistryFactory = [factory = wrapManagedObject(_mountingManager.componentViewRegistry.componentViewFactory)]( const EventDispatcher::Weak &eventDispatcher, const ContextContainer::Shared &contextContainer) { @@ -263,11 +251,11 @@ - (RCTScheduler *)_createScheduler toolbox.runtimeExecutor = runtimeExecutor; toolbox.bridgelessBindingsExecutor = _bridgelessBindingsExecutor; - toolbox.asynchronousEventBeatFactory = - [runtimeExecutor](const EventBeat::SharedOwnerBox &ownerBox) -> std::unique_ptr { + toolbox.eventBeatFactory = + [runtimeScheduler](std::shared_ptr ownerBox) -> std::unique_ptr { auto runLoopObserver = std::make_unique(RunLoopObserver::Activity::BeforeWaiting, ownerBox->owner); - return std::make_unique(std::move(runLoopObserver), runtimeExecutor); + return std::make_unique(std::move(ownerBox), std::move(runLoopObserver), *runtimeScheduler); }; RCTScheduler *scheduler = [[RCTScheduler alloc] initWithToolbox:toolbox]; @@ -303,12 +291,12 @@ - (void)_applicationWillTerminate #pragma mark - RCTSchedulerDelegate -- (void)schedulerDidFinishTransaction:(MountingCoordinator::Shared)mountingCoordinator +- (void)schedulerDidFinishTransaction:(std::shared_ptr)mountingCoordinator { // no-op, we will flush the transaction from schedulerShouldRenderTransactions } -- (void)schedulerShouldRenderTransactions:(MountingCoordinator::Shared)mountingCoordinator +- (void)schedulerShouldRenderTransactions:(std::shared_ptr)mountingCoordinator { [_mountingManager scheduleTransaction:mountingCoordinator]; } 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 new file mode 100644 index 00000000000000..f5725264a80674 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.h @@ -0,0 +1,19 @@ +/* + * 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 // [macOS] +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RCTLinearGradient : NSObject + ++ (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const facebook::react::LinearGradient &)gradient; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm new file mode 100644 index 00000000000000..d091701986c887 --- /dev/null +++ b/packages/react-native/React/Fabric/Utils/RCTLinearGradient.mm @@ -0,0 +1,138 @@ +/* + * 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 "RCTLinearGradient.h" + +#import + +using namespace facebook::react; + +@implementation RCTLinearGradient + ++ (CALayer *)gradientLayerWithSize:(CGSize)size gradient:(const LinearGradient &)gradient +{ +#if !TARGET_OS_OSX // macos does not support linear gradients + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size]; + const auto &direction = gradient.direction; + 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; + + if (direction.type == GradientDirectionType::Angle) { + CGFloat angle = std::get(direction.value); + std::tie(startPoint, endPoint) = getPointsFromAngle(angle, size); + } else if (direction.type == GradientDirectionType::Keyword) { + auto keyword = std::get(direction.value); + CGFloat angle = getAngleForKeyword(keyword, size); + std::tie(startPoint, endPoint) = getPointsFromAngle(angle, size); + } else { + // Default to top-to-bottom gradient + startPoint = CGPointMake(0.0, 0.0); + endPoint = CGPointMake(0.0, size.height); + } + + CGContextDrawLinearGradient(context, cgGradient, startPoint, endPoint, 0); + + for (id color in colors) { + CGColorRelease((__bridge CGColorRef)color); + } + CGGradientRelease(cgGradient); + }]; +#endif + + CALayer *gradientLayer = [CALayer layer]; +#if !TARGET_OS_OSX // macos does not support linear gradients + gradientLayer.contents = (__bridge id)gradientImage.CGImage; +#endif + + return gradientLayer; +} + +// Spec: https://www.w3.org/TR/css-images-3/#linear-gradient-syntax +// Reference: +// https://github.com/chromium/chromium/blob/d32abbe13f5d52be7127fe25d5b778498165fab8/third_party/blink/renderer/core/css/css_gradient_value.cc#L1057 +static std::pair getPointsFromAngle(CGFloat angle, CGSize size) +{ + angle = fmod(angle, 360.0); + if (angle < 0) { + angle += 360.0; + } + + if (angle == 0.0) { + return {CGPointMake(0, size.height), CGPointMake(0, 0)}; + } + if (angle == 90.0) { + return {CGPointMake(0, 0), CGPointMake(size.width, 0)}; + } + if (angle == 180.0) { + return {CGPointMake(0, 0), CGPointMake(0, size.height)}; + } + if (angle == 270.0) { + return {CGPointMake(size.width, 0), CGPointMake(0, 0)}; + } + + CGFloat radians = (90 - angle) * M_PI / 180.0; + CGFloat slope = tan(radians); + CGFloat perpendicularSlope = -1 / slope; + + CGFloat halfHeight = size.height / 2; + CGFloat halfWidth = size.width / 2; + + CGPoint endCorner; + if (angle < 90) { + endCorner = CGPointMake(halfWidth, halfHeight); + } else if (angle < 180) { + endCorner = CGPointMake(halfWidth, -halfHeight); + } else if (angle < 270) { + endCorner = CGPointMake(-halfWidth, -halfHeight); + } else { + endCorner = CGPointMake(-halfWidth, halfHeight); + } + + CGFloat c = endCorner.y - perpendicularSlope * endCorner.x; + CGFloat endX = c / (slope - perpendicularSlope); + CGFloat endY = perpendicularSlope * endX + c; + + return {CGPointMake(halfWidth - endX, halfHeight + endY), CGPointMake(halfWidth + endX, halfHeight - endY)}; +} + +// Spec: https://www.w3.org/TR/css-images-3/#linear-gradient-syntax +// Refer `using keywords` section +static CGFloat getAngleForKeyword(GradientKeyword keyword, CGSize size) +{ + switch (keyword) { + case GradientKeyword::ToTopRight: { + CGFloat angleDeg = atan(size.width / size.height) * 180.0 / M_PI; + return 90.0 - angleDeg; + } + case GradientKeyword::ToBottomRight: + return atan(size.width / size.height) * 180.0 / M_PI + 90.0; + case GradientKeyword::ToTopLeft: + return atan(size.width / size.height) * 180.0 / M_PI + 270.0; + case GradientKeyword::ToBottomLeft: + return atan(size.height / size.width) * 180.0 / M_PI + 180.0; + default: + return 180.0; + } +} + +@end diff --git a/packages/react-native/React/Inspector/RCTCxxInspectorPackagerConnection.mm b/packages/react-native/React/Inspector/RCTCxxInspectorPackagerConnection.mm index 53d465bc64c969..19d53df5ff8512 100644 --- a/packages/react-native/React/Inspector/RCTCxxInspectorPackagerConnection.mm +++ b/packages/react-native/React/Inspector/RCTCxxInspectorPackagerConnection.mm @@ -22,6 +22,7 @@ #import "RCTCxxInspectorPackagerConnection.h" #import "RCTCxxInspectorPackagerConnectionDelegate.h" #import "RCTCxxInspectorWebSocketAdapter.h" +#import "RCTInspectorUtils.h" using namespace facebook::react::jsinspector_modern; @interface RCTCxxInspectorPackagerConnection () { @@ -36,8 +37,10 @@ @implementation RCTCxxInspectorPackagerConnection - (instancetype)initWithURL:(NSURL *)url { if (self = [super init]) { + auto metadata = [RCTInspectorUtils getHostMetadata]; _cxxImpl = std::make_unique( [url absoluteString].UTF8String, + metadata.deviceName.UTF8String, [[NSBundle mainBundle] bundleIdentifier].UTF8String, std::make_unique()); } diff --git a/packages/react-native/React/Inspector/RCTCxxInspectorWebSocketAdapter.mm b/packages/react-native/React/Inspector/RCTCxxInspectorWebSocketAdapter.mm index 46cfb11fa1c817..2a8ea2329a903e 100644 --- a/packages/react-native/React/Inspector/RCTCxxInspectorWebSocketAdapter.mm +++ b/packages/react-native/React/Inspector/RCTCxxInspectorWebSocketAdapter.mm @@ -76,6 +76,14 @@ - (void)webSocket:(__unused SRWebSocket *)webSocket didReceiveMessageWithString: } } +- (void)webSocketDidOpen:(SRWebSocket *)webSocket +{ + // NOTE: We are on the main queue here, per SRWebSocket's defaults. + if (auto delegate = _delegate.lock()) { + delegate->didOpen(); + } +} + - (void)webSocket:(__unused SRWebSocket *)webSocket didCloseWithCode:(__unused NSInteger)code reason:(__unused NSString *)reason diff --git a/packages/react-native/React/Inspector/RCTInspector.mm b/packages/react-native/React/Inspector/RCTInspector.mm index 9385f5d19c3318..634d11a4905f6d 100644 --- a/packages/react-native/React/Inspector/RCTInspector.mm +++ b/packages/react-native/React/Inspector/RCTInspector.mm @@ -71,7 +71,7 @@ @implementation RCTInspector NSMutableArray *array = [NSMutableArray arrayWithCapacity:pages.size()]; for (size_t i = 0; i < pages.size(); i++) { RCTInspectorPage *pageWrapper = [[RCTInspectorPage alloc] initWithId:pages[i].id - title:@(pages[i].title.c_str()) + title:@(pages[i].description.c_str()) vm:@(pages[i].vm.c_str())]; [array addObject:pageWrapper]; } diff --git a/packages/react-native/React/Modules/MacOS/RCTAccessibilityManager.m b/packages/react-native/React/Modules/MacOS/RCTAccessibilityManager.m index f685305cd901fa..3ccf8861e57c00 100644 --- a/packages/react-native/React/Modules/MacOS/RCTAccessibilityManager.m +++ b/packages/react-native/React/Modules/MacOS/RCTAccessibilityManager.m @@ -8,11 +8,10 @@ // [macOS] #if TARGET_OS_OSX -#import "RCTAccessibilityManager.h" +#import #import "RCTBridge.h" #import "RCTConvert.h" -#import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTUIManager.h" diff --git a/packages/react-native/React/Modules/RCTUIManager.m b/packages/react-native/React/Modules/RCTUIManager.m index 40ac778a80633f..a1967d7c4e54de 100644 --- a/packages/react-native/React/Modules/RCTUIManager.m +++ b/packages/react-native/React/Modules/RCTUIManager.m @@ -19,7 +19,6 @@ #import "RCTComponentData.h" #import "RCTConvert.h" #import "RCTDefines.h" -#import "RCTDevSettings.h" // [macOS] #import "RCTEventDispatcherProtocol.h" #import "RCTLayoutAnimation.h" #import "RCTLayoutAnimationGroup.h" @@ -44,7 +43,6 @@ #import "RCTViewManager.h" #import "UIView+React.h" #import "RCTUIKit.h" // [macOS] -#import "RCTDeviceInfo.h" // [macOS] #import @@ -832,7 +830,7 @@ - (void)_removeChildren:(NSArray *)children // [macOS] #else // [macOS [originalSuperview addSubview:removedChild positioned:nextLowerView == nil ? NSWindowBelow : NSWindowAbove relativeTo:nextLowerView]; #endif // macOS] - + NSString *property = deletingLayoutAnimation.property; [deletingLayoutAnimation performAnimations:^{ @@ -1368,7 +1366,7 @@ - (void)_dispatchPropsDidChangeEvents //The macOS default coordinate system has its origin at the lower left of the drawing area, so we need to flip the y-axis coordinate. windowFrame.origin.y = view.window.contentView.frame.size.height - windowFrame.origin.y - windowFrame.size.height; #endif // macOS] - + callback(@[ @(windowFrame.origin.x), @(windowFrame.origin.y), diff --git a/packages/react-native/React/Views/RCTCursor.h b/packages/react-native/React/Views/RCTCursor.h index 2afacddbef70c4..b9bef145a84903 100644 --- a/packages/react-native/React/Views/RCTCursor.h +++ b/packages/react-native/React/Views/RCTCursor.h @@ -7,6 +7,7 @@ #import #import // [macOS] +#import typedef NS_ENUM(NSInteger, RCTCursor) { // [macOS @@ -53,4 +54,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.h b/packages/react-native/React/Views/RCTFont.h index 5c2189ac26fd13..77e23d121ca83f 100644 --- a/packages/react-native/React/Views/RCTFont.h +++ b/packages/react-native/React/Views/RCTFont.h @@ -10,6 +10,7 @@ #import typedef UIFont * (^RCTFontHandler)(CGFloat fontSize, NSString *fontWeightDescription); +typedef CGFloat RCTFontWeight; /** * React Native will use the System font for rendering by default. If you want to @@ -19,6 +20,7 @@ typedef UIFont * (^RCTFontHandler)(CGFloat fontSize, NSString *fontWeightDescrip */ RCT_EXTERN void RCTSetDefaultFontHandler(RCTFontHandler handler); RCT_EXTERN BOOL RCTHasFontHandlerSet(void); +RCT_EXTERN RCTFontWeight RCTGetFontWeight(UIFont *font); @interface RCTFont : NSObject diff --git a/packages/react-native/React/Views/RCTFont.mm b/packages/react-native/React/Views/RCTFont.mm index 3097632ca7d190..2e00bfd116b96e 100644 --- a/packages/react-native/React/Views/RCTFont.mm +++ b/packages/react-native/React/Views/RCTFont.mm @@ -11,8 +11,7 @@ #import -typedef CGFloat RCTFontWeight; -static RCTFontWeight weightOfFont(UIFont *font) +RCTFontWeight RCTGetFontWeight(UIFont *font) { static NSArray *weightSuffixes; static NSArray *fontWeights; @@ -144,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]; } @@ -419,7 +412,7 @@ + (UIFont *)updateFont:(UIFont *)font if (font) { familyName = font.familyName ?: defaultFontFamily; fontSize = font.pointSize ?: defaultFontSize; - fontWeight = weightOfFont(font); + fontWeight = RCTGetFontWeight(font); isItalic = isItalicFont(font); isCondensed = isCondensedFont(font); } @@ -467,17 +460,14 @@ + (UIFont *)updateFont:(UIFont *)font // It's actually a font name, not a font family name, // but we'll do what was meant, not what was said. familyName = font.familyName; - fontWeight = weight ? fontWeight : weightOfFont(font); + fontWeight = weight ? fontWeight : RCTGetFontWeight(font); isItalic = style ? isItalic : isItalicFont(font); isCondensed = isCondensedFont(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 { @@ -493,7 +483,7 @@ + (UIFont *)updateFont:(UIFont *)font for (NSString *name in names) { UIFont *match = [UIFont fontWithName:name size:fontSize]; if (isItalic == isItalicFont(match) && isCondensed == isCondensedFont(match)) { - CGFloat testWeight = weightOfFont(match); + CGFloat testWeight = RCTGetFontWeight(match); if (ABS(testWeight - fontWeight) < ABS(closestWeight - fontWeight)) { font = match; closestWeight = testWeight; diff --git a/packages/react-native/React/Views/RCTShadowView.h b/packages/react-native/React/Views/RCTShadowView.h index 7009685257f22a..982ed1be1f8b84 100644 --- a/packages/react-native/React/Views/RCTShadowView.h +++ b/packages/react-native/React/Views/RCTShadowView.h @@ -152,9 +152,9 @@ typedef void (^RCTApplierBlock)(NSDictionary NS_ASSUME_NONNULL_BEGIN @@ -23,5 +20,3 @@ NS_ASSUME_NONNULL_BEGIN @end NS_ASSUME_NONNULL_END - -#endif // [macOS] diff --git a/packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.m b/packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.m index d2da7e0a607192..35a88c054b6d50 100644 --- a/packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.m +++ b/packages/react-native/React/Views/ScrollView/MacOS/RCTScrollContentLocalData.m @@ -7,7 +7,6 @@ // [macOS] -#if TARGET_OS_OSX #import "RCTScrollContentLocalData.h" @implementation RCTScrollContentLocalData @@ -23,4 +22,3 @@ - (instancetype)initWithVerticalScrollerWidth:(CGFloat)verticalScrollerWidth } @end -#endif \ No newline at end of file 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 8a729defeb5f4d..094010d6a5b6b9 100644 --- a/packages/react-native/React/Views/ScrollView/RCTScrollView.m +++ b/packages/react-native/React/Views/ScrollView/RCTScrollView.m @@ -382,6 +382,7 @@ @implementation RCTScrollView { BOOL _allowNextScrollNoMatterWhat; #if TARGET_OS_OSX // [macOS BOOL _notifyDidScroll; + BOOL _disableScrollEvents; NSPoint _lastScrollPosition; #endif // macOS] CGRect _lastClippedToRect; @@ -506,6 +507,7 @@ - (instancetype)initWithEventDispatcher:(id)eventDis #else // [macOS _scrollView.postsBoundsChangedNotifications = YES; _lastScrollPosition = NSZeroPoint; + _hasOverlayStyleIndicator = NO; #endif // macOS] #if !TARGET_OS_OSX // [macOS] @@ -531,21 +533,23 @@ - (instancetype)initWithEventDispatcher:(id)eventDis } #if TARGET_OS_OSX // [macOS -- (BOOL)canBecomeKeyView +- (void)setFrame:(NSRect)frame { - return [self focusable]; -} - -- (CGRect)focusRingMaskBounds -{ - return [self bounds]; -} + /** + * 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; + [super setFrame:frame]; + _disableScrollEvents = NO; -- (void)drawFocusRingMask -{ - if (self.enableFocusRing) { - NSBezierPath *borderPath = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:2.0 yRadius:2.0]; - [borderPath stroke]; + if (self.window != nil && !self.window.inLiveResize) { + [self performSelector:@selector(scrollViewDocumentViewBoundsDidChange:) withObject:nil afterDelay:0]; } } @@ -572,7 +576,7 @@ - (void)setAccessibilityRole:(NSAccessibilityRole)accessibilityRole - (void)setInverted:(BOOL)inverted { BOOL changed = _inverted != inverted; - _inverted = inverted; + _inverted = inverted; if (changed && _onInvertedDidChange) { _onInvertedDidChange(@{}); } @@ -582,8 +586,10 @@ - (void)setHasOverlayStyleIndicator:(BOOL)hasOverlayStyle { if (hasOverlayStyle == true) { self.scrollView.scrollerStyle = NSScrollerStyleOverlay; + _hasOverlayStyleIndicator = YES; } else { self.scrollView.scrollerStyle = NSScrollerStyleLegacy; + _hasOverlayStyleIndicator = NO; } } #endif // macOS] @@ -905,6 +911,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]; @@ -958,11 +968,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. @@ -973,7 +983,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 @@ -1266,14 +1276,14 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager #endif // macOS] BOOL hasNewView = NO; if (horz) { - CGFloat leftInset = self.inverted ? self->_scrollView.contentInset.right : self->_scrollView.contentInset.left; + CGFloat leftInset = self.inverted ? self->_scrollView.contentInset.left : self->_scrollView.contentInset.right; CGFloat x = self->_scrollView.contentOffset.x + leftInset; - hasNewView = subview.frame.origin.x + subview.frame.size.width > x; + hasNewView = subview.frame.origin.x + subview.frame.size.width >= x; } else { CGFloat bottomInset = - self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom; + self.inverted ? self->_scrollView.contentInset.bottom : self->_scrollView.contentInset.top; CGFloat y = self->_scrollView.contentOffset.y + bottomInset; - hasNewView = subview.frame.origin.y + subview.frame.size.height > y; + hasNewView = subview.frame.origin.y + subview.frame.size.height >= y; } #if !TARGET_OS_OSX // [macOS] if (hasNewView || ii == self->_contentView.subviews.count - 1) { @@ -1356,7 +1366,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]; @@ -1389,6 +1399,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/nativemodule/samples/platform/ios/ReactCommon/RCTNativeSampleTurboModuleSpec.h b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTNativeSampleTurboModuleSpec.h index e37e275e1eba37..cf7919a7bbed55 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTNativeSampleTurboModuleSpec.h +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTNativeSampleTurboModuleSpec.h @@ -15,6 +15,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** * The ObjC protocol based on the JS Flow type for SampleTurboModule. */ @@ -65,3 +67,5 @@ class JSI_EXPORT NativeSampleTurboModuleSpecJSI : public ObjCTurboModule { }; } // namespace facebook::react + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.mm b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.mm index 5613f858cf4d9a..054aa5ca8d360d 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleLegacyModule.mm @@ -141,8 +141,8 @@ - (NSDictionary *)constantsToExport { return @{ @"x" : @(x), - @"y" : y ?: [NSNull null], - @"z" : z ?: [NSNull null], + @"y" : y ? y : [NSNull null], + @"z" : z ? z : [NSNull null], }; } diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboCxxModule.h b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboCxxModule.h index 318ba46aadfd6f..edc4f1ebe7d37d 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboCxxModule.h +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboCxxModule.h @@ -10,18 +10,9 @@ #import #import -/** - * Sample backward-compatible RCTCxxModule-based module. - * With jsi::HostObject, this class is no longer necessary, but the system supports it for - * backward compatibility. - */ -@interface RCTSampleTurboCxxModule_v1 : RCTCxxModule - -@end - /** * Second variant of a sample backward-compatible RCTCxxModule-based module. */ -@interface RCTSampleTurboCxxModule_v2 : RCTCxxModule +@interface RCTSampleTurboCxxModule : RCTCxxModule @end diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboCxxModule.mm b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboCxxModule.mm index 72162e1c656b95..1a78f6fed9a222 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboCxxModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboCxxModule.mm @@ -13,30 +13,7 @@ using namespace facebook; -// ObjC++ wrapper. -@implementation RCTSampleTurboCxxModule_v1 - -RCT_EXPORT_MODULE(); - -- (std::shared_ptr)getTurboModuleWithJsInvoker:(std::shared_ptr)jsInvoker -{ - return std::make_shared(jsInvoker); -} - -- (std::unique_ptr)createModule -{ - return nullptr; -} - -- (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params -{ - return nullptr; -} - -@end - -@implementation RCTSampleTurboCxxModule_v2 +@implementation RCTSampleTurboCxxModule RCT_EXPORT_MODULE(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm index ffefd951c6a83b..e86102441aa65b 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm @@ -9,9 +9,9 @@ #import "RCTSampleTurboModulePlugin.h" #import +#import // [macOS] #import #import -#import // [macOS] using namespace facebook::react; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp new file mode 100644 index 00000000000000..411508e1a81ec3 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp @@ -0,0 +1,193 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "TextInputEventEmitter.h" + +namespace facebook::react { + +static jsi::Value textInputMetricsPayload( + jsi::Runtime& runtime, + const TextInputEventEmitter::Metrics& textInputMetrics) { + auto payload = jsi::Object(runtime); + + payload.setProperty( + runtime, + "text", + jsi::String::createFromUtf8(runtime, textInputMetrics.text)); + + payload.setProperty(runtime, "eventCount", textInputMetrics.eventCount); + + { + auto selection = jsi::Object(runtime); + selection.setProperty( + runtime, "start", textInputMetrics.selectionRange.location); + selection.setProperty( + runtime, + "end", + textInputMetrics.selectionRange.location + + textInputMetrics.selectionRange.length); + payload.setProperty(runtime, "selection", selection); + } + + return payload; +}; + +static jsi::Value textInputMetricsScrollPayload( + jsi::Runtime& runtime, + const TextInputEventEmitter::Metrics& textInputMetrics) { + auto payload = jsi::Object(runtime); + + { + auto contentOffset = jsi::Object(runtime); + contentOffset.setProperty(runtime, "x", textInputMetrics.contentOffset.x); + contentOffset.setProperty(runtime, "y", textInputMetrics.contentOffset.y); + payload.setProperty(runtime, "contentOffset", contentOffset); + } + + { + auto contentInset = jsi::Object(runtime); + contentInset.setProperty(runtime, "top", textInputMetrics.contentInset.top); + contentInset.setProperty( + runtime, "left", textInputMetrics.contentInset.left); + contentInset.setProperty( + runtime, "bottom", textInputMetrics.contentInset.bottom); + contentInset.setProperty( + runtime, "right", textInputMetrics.contentInset.right); + payload.setProperty(runtime, "contentInset", contentInset); + } + + { + auto contentSize = jsi::Object(runtime); + contentSize.setProperty( + runtime, "width", textInputMetrics.contentSize.width); + contentSize.setProperty( + runtime, "height", textInputMetrics.contentSize.height); + payload.setProperty(runtime, "contentSize", contentSize); + } + + { + auto layoutMeasurement = jsi::Object(runtime); + layoutMeasurement.setProperty( + runtime, "width", textInputMetrics.layoutMeasurement.width); + layoutMeasurement.setProperty( + runtime, "height", textInputMetrics.layoutMeasurement.height); + payload.setProperty(runtime, "layoutMeasurement", layoutMeasurement); + } + + payload.setProperty( + runtime, + "zoomScale", + textInputMetrics.zoomScale ? textInputMetrics.zoomScale : 1); + + return payload; +}; + +static jsi::Value textInputMetricsContentSizePayload( + jsi::Runtime& runtime, + const TextInputEventEmitter::Metrics& textInputMetrics) { + auto payload = jsi::Object(runtime); + + { + auto contentSize = jsi::Object(runtime); + contentSize.setProperty( + runtime, "width", textInputMetrics.contentSize.width); + contentSize.setProperty( + runtime, "height", textInputMetrics.contentSize.height); + payload.setProperty(runtime, "contentSize", contentSize); + } + + return payload; +}; + +static jsi::Value keyPressMetricsPayload( + jsi::Runtime& runtime, + const TextInputEventEmitter::KeyPressMetrics& keyPressMetrics) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "eventCount", keyPressMetrics.eventCount); + + std::string key; + if (keyPressMetrics.text.empty()) { + key = "Backspace"; + } else { + if (keyPressMetrics.text.front() == '\n') { + key = "Enter"; + } else if (keyPressMetrics.text.front() == '\t') { + key = "Tab"; + } else if (keyPressMetrics.text.front() == '\x1B') { + key = "Escape"; + } else { + key = keyPressMetrics.text; + } + } + payload.setProperty( + runtime, "key", jsi::String::createFromUtf8(runtime, key)); + return payload; +}; + +void TextInputEventEmitter::onFocus(const Metrics& textInputMetrics) const { + dispatchTextInputEvent("focus", textInputMetrics); +} + +void TextInputEventEmitter::onBlur(const Metrics& textInputMetrics) const { + dispatchTextInputEvent("blur", textInputMetrics); +} + +void TextInputEventEmitter::onChange(const Metrics& textInputMetrics) const { + dispatchTextInputEvent("change", textInputMetrics); +} + +void TextInputEventEmitter::onContentSizeChange( + const Metrics& textInputMetrics) const { + dispatchTextInputContentSizeChangeEvent( + "contentSizeChange", textInputMetrics); +} + +void TextInputEventEmitter::onSelectionChange( + const Metrics& textInputMetrics) const { + dispatchTextInputEvent("selectionChange", textInputMetrics); +} + +void TextInputEventEmitter::onEndEditing( + const Metrics& textInputMetrics) const { + dispatchTextInputEvent("endEditing", textInputMetrics); +} + +void TextInputEventEmitter::onSubmitEditing( + const Metrics& textInputMetrics) const { + dispatchTextInputEvent("submitEditing", textInputMetrics); +} + +void TextInputEventEmitter::onKeyPress( + const KeyPressMetrics& keyPressMetrics) const { + dispatchEvent("keyPress", [keyPressMetrics](jsi::Runtime& runtime) { + return keyPressMetricsPayload(runtime, keyPressMetrics); + }); +} + +void TextInputEventEmitter::onScroll(const Metrics& textInputMetrics) const { + dispatchEvent("scroll", [textInputMetrics](jsi::Runtime& runtime) { + return textInputMetricsScrollPayload(runtime, textInputMetrics); + }); +} + +void TextInputEventEmitter::dispatchTextInputEvent( + const std::string& name, + const Metrics& textInputMetrics) const { + dispatchEvent(name, [textInputMetrics](jsi::Runtime& runtime) { + return textInputMetricsPayload(runtime, textInputMetrics); + }); +} + +void TextInputEventEmitter::dispatchTextInputContentSizeChangeEvent( + const std::string& name, + const Metrics& textInputMetrics) const { + dispatchEvent(name, [textInputMetrics](jsi::Runtime& runtime) { + return textInputMetricsContentSizePayload(runtime, textInputMetrics); + }); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h new file mode 100644 index 00000000000000..9182dd3d2edccb --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +class TextInputEventEmitter : public ViewEventEmitter { + public: + using ViewEventEmitter::ViewEventEmitter; + + struct Metrics { + std::string text; + AttributedString::Range selectionRange; + // ScrollView-like metrics + Size contentSize; + Point contentOffset; + EdgeInsets contentInset; + Size containerSize; + int eventCount; + Size layoutMeasurement; + Float zoomScale; + }; + + struct KeyPressMetrics { + std::string text; + int eventCount; + }; + + void onFocus(const Metrics& textInputMetrics) const; + void onBlur(const Metrics& textInputMetrics) const; + void onChange(const Metrics& textInputMetrics) const; + void onContentSizeChange(const Metrics& textInputMetrics) const; + void onSelectionChange(const Metrics& textInputMetrics) const; + void onEndEditing(const Metrics& textInputMetrics) const; + void onSubmitEditing(const Metrics& textInputMetrics) const; + void onKeyPress(const KeyPressMetrics& keyPressMetrics) const; + void onScroll(const Metrics& textInputMetrics) const; + + private: + void dispatchTextInputEvent( + const std::string& name, + const Metrics& textInputMetrics) const; + + void dispatchTextInputContentSizeChangeEvent( + const std::string& name, + const Metrics& textInputMetrics) const; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputState.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputState.cpp new file mode 100644 index 00000000000000..1bf68a8621cccf --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputState.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "TextInputState.h" + +#ifdef ANDROID +#include +#include +#endif + +namespace facebook::react { + +TextInputState::TextInputState( + AttributedStringBox attributedStringBox, + AttributedString reactTreeAttributedString, + ParagraphAttributes paragraphAttributes, + int64_t mostRecentEventCount) + : attributedStringBox(std::move(attributedStringBox)), + reactTreeAttributedString(std::move(reactTreeAttributedString)), + paragraphAttributes(std::move(paragraphAttributes)), + mostRecentEventCount(mostRecentEventCount) {} + +#ifdef ANDROID +TextInputState::TextInputState( + const TextInputState& previousState, + const folly::dynamic& data) + : attributedStringBox(previousState.attributedStringBox), + reactTreeAttributedString(previousState.reactTreeAttributedString), + paragraphAttributes(previousState.paragraphAttributes), + mostRecentEventCount(data.getDefault( + "mostRecentEventCount", + previousState.mostRecentEventCount) + .getInt()), + cachedAttributedStringId(data.getDefault( + "opaqueCacheId", + previousState.cachedAttributedStringId) + .getInt()){}; + +folly::dynamic TextInputState::getDynamic() const { + LOG(FATAL) << "TextInputState state should only be read using MapBuffer"; +} + +MapBuffer TextInputState::getMapBuffer() const { + auto builder = MapBufferBuilder(); + // If we have a `cachedAttributedStringId` we know that we're (1) not trying + // to set a new string, so we don't need to pass it along; (2) setState was + // called from Java to trigger a relayout with a `cachedAttributedStringId`, + // so Java has all up-to-date information and we should pass an empty map + // through. + if (cachedAttributedStringId == 0) { + // TODO truncation + builder.putInt( + TX_STATE_KEY_MOST_RECENT_EVENT_COUNT, + static_cast(mostRecentEventCount)); + + auto attStringMapBuffer = toMapBuffer(attributedStringBox.getValue()); + builder.putMapBuffer(TX_STATE_KEY_ATTRIBUTED_STRING, attStringMapBuffer); + auto paMapBuffer = toMapBuffer(paragraphAttributes); + builder.putMapBuffer(TX_STATE_KEY_PARAGRAPH_ATTRIBUTES, paMapBuffer); + + builder.putInt(TX_STATE_KEY_HASH, attStringMapBuffer.getInt(AS_KEY_HASH)); + } + return builder.build(); +} +#endif + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputState.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputState.h similarity index 63% rename from packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputState.h rename to packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputState.h index fe7dab58255258..3f980ea78f89cb 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputState.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/TextInputState.h @@ -11,6 +11,11 @@ #include #include +#ifdef ANDROID +#include +#include +#endif + namespace facebook::react { /* @@ -20,6 +25,20 @@ class TextInputState final { public: TextInputState() = default; + TextInputState( + AttributedStringBox attributedStringBox, + AttributedString reactTreeAttributedString, + ParagraphAttributes paragraphAttributes, + int64_t mostRecentEventCount); + +#ifdef ANDROID + TextInputState( + const TextInputState& previousState, + const folly::dynamic& data); + folly::dynamic getDynamic() const; + MapBuffer getMapBuffer() const; +#endif + /* * All content of component. */ @@ -40,14 +59,15 @@ class TextInputState final { */ ParagraphAttributes paragraphAttributes; - /* - * `TextLayoutManager` provides a connection to platform-specific - * text rendering infrastructure which is capable to render the - * `AttributedString`. - */ - std::shared_ptr layoutManager; + int64_t mostRecentEventCount{0}; - size_t mostRecentEventCount{0}; +#ifdef ANDROID + /** + * Stores an opaque cache ID used on the Java side to refer to a specific + * AttributedString for measurement purposes only. + */ + int64_t cachedAttributedStringId{0}; +#endif }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/baseConversions.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/baseConversions.h new file mode 100644 index 00000000000000..8acd9781c37916 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/baseConversions.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +inline void fromRawValue( + const PropsParserContext& /*context*/, + const RawValue& value, + SubmitBehavior& result) { + auto string = static_cast(value); + if (string == "newline") { + result = SubmitBehavior::Newline; + } else if (string == "submit") { + result = SubmitBehavior::Submit; + } else if (string == "blurAndSubmit") { + result = SubmitBehavior::BlurAndSubmit; + } else { + abort(); + } +} + +inline folly::dynamic toDynamic(const SubmitBehavior& value) { + switch (value) { + case SubmitBehavior::Newline: + return "newline"; + case SubmitBehavior::Submit: + return "submit"; + case SubmitBehavior::BlurAndSubmit: + return "blurAndSubmit"; + case SubmitBehavior::Default: + return {nullptr}; + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputState.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/basePrimitives.h similarity index 55% rename from packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputState.cpp rename to packages/react-native/ReactCommon/react/renderer/components/textinput/basePrimitives.h index abe682f52e19a1..68fbdb9ccc48c2 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputState.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/basePrimitives.h @@ -5,6 +5,15 @@ * LICENSE file in the root directory of this source tree. */ -#include "TextInputState.h" +#pragma once -namespace facebook::react {} // namespace facebook::react +namespace facebook::react { + +enum class SubmitBehavior { + Default, + Submit, + BlurAndSubmit, + Newline, +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/MacOSTextInputEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/MacOSTextInputEventEmitter.cpp new file mode 100644 index 00000000000000..b850c148988ad5 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/MacOSTextInputEventEmitter.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "MacOSTextInputEventEmitter.h" + +namespace facebook::react { + +void MacOSTextInputEventEmitter::onAutoCorrectChange( + OnAutoCorrectChange event) const { + dispatchEvent( + "autoCorrectChange", [event = std::move(event)](jsi::Runtime& runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "enabled", event.enabled); + return payload; + }); +} + +void MacOSTextInputEventEmitter::onSpellCheckChange( + OnSpellCheckChange event) const { + dispatchEvent( + "spellCheckChange", [event = std::move(event)](jsi::Runtime& runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "enabled", event.enabled); + return payload; + }); +} + +void MacOSTextInputEventEmitter::onGrammarCheckChange( + OnGrammarCheckChange event) const { + dispatchEvent( + "grammarCheckChange", [event = std::move(event)](jsi::Runtime& runtime) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "enabled", event.enabled); + return payload; + }); +} + +void MacOSTextInputEventEmitter::onPaste(const PasteEvent& pasteEvent) const { + dispatchEvent("paste", [pasteEvent](jsi::Runtime& runtime) { + auto payload = jsi::Object(runtime); + auto dataTransfer = + dataTransferPayload(runtime, pasteEvent.dataTransferItems); + payload.setProperty(runtime, "dataTransfer", dataTransfer); + return payload; + }); +} +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/MacOSTextInputEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/MacOSTextInputEventEmitter.h new file mode 100644 index 00000000000000..cdf3f4fb68af7d --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/MacOSTextInputEventEmitter.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +class MacOSTextInputEventEmitter : public TextInputEventEmitter { + public: + using TextInputEventEmitter::TextInputEventEmitter; + + struct OnAutoCorrectChange { + bool enabled; + }; + void onAutoCorrectChange(OnAutoCorrectChange value) const; + + struct OnSpellCheckChange { + bool enabled; + }; + void onSpellCheckChange(OnSpellCheckChange value) const; + + struct OnGrammarCheckChange { + bool enabled; + }; + void onGrammarCheckChange(OnGrammarCheckChange value) const; + + struct PasteEvent { + std::vector dataTransferItems; + }; + void onPaste(const PasteEvent& pasteEvent) const; +}; +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.cpp index 43f5e9efa54c58..d9735b9c0386b7 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.cpp @@ -45,30 +45,4 @@ TextInputProps::TextInputProps( sourceProps.onChangeSync, {})){}; -TextAttributes TextInputProps::getEffectiveTextAttributes( - Float fontSizeMultiplier) const { - auto result = TextAttributes::defaultTextAttributes(); - result.fontSizeMultiplier = fontSizeMultiplier; - result.apply(textAttributes); - - /* - * These props are applied to `View`, therefore they must not be a part of - * base text attributes. - */ - result.backgroundColor = clearColor(); - result.opacity = 1; - - return result; -} - -ParagraphAttributes TextInputProps::getEffectiveParagraphAttributes() const { - auto result = paragraphAttributes; - - if (!traits.multiline) { - result.maximumNumberOfLines = 1; - } - - return result; -} - } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.h index 449429efc8574c..f42bd79dc198b6 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.h @@ -40,12 +40,6 @@ class TextInputProps final : public BaseTextInputProps { bool onKeyPressSync{false}; bool onChangeSync{false}; - - /* - * Accessors - */ - TextAttributes getEffectiveTextAttributes(Float fontSizeMultiplier) const; - ParagraphAttributes getEffectiveParagraphAttributes() const; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp index b6c7a889093f59..759b012bcd0c3a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp @@ -7,171 +7,8 @@ #include "TextInputShadowNode.h" -#include -#include -#include -#include -#include -#include -#include -#include - namespace facebook::react { extern const char TextInputComponentName[] = "TextInput"; -TextInputShadowNode::TextInputShadowNode( - const ShadowNode& sourceShadowNode, - const ShadowNodeFragment& fragment) - : ConcreteViewShadowNode(sourceShadowNode, fragment) { - auto& sourceTextInputShadowNode = - static_cast(sourceShadowNode); - - if (ReactNativeFeatureFlags::enableCleanTextInputYogaNode()) { - if (!fragment.children && !fragment.props && - sourceTextInputShadowNode.getIsLayoutClean()) { - // This ParagraphShadowNode was cloned but did not change - // in a way that affects its layout. Let's mark it clean - // to stop Yoga from traversing it. - cleanLayout(); - } - } -} - -AttributedStringBox TextInputShadowNode::attributedStringBoxToMeasure( - const LayoutContext& layoutContext) const { - bool hasMeaningfulState = - getState() && getState()->getRevision() != State::initialRevisionValue; - - if (hasMeaningfulState) { - auto attributedStringBox = getStateData().attributedStringBox; - if (attributedStringBox.getMode() == - AttributedStringBox::Mode::OpaquePointer || - !attributedStringBox.getValue().isEmpty()) { - return getStateData().attributedStringBox; - } - } - - auto attributedString = hasMeaningfulState - ? AttributedString{} - : getAttributedString(layoutContext); - - if (attributedString.isEmpty()) { - auto placeholder = getConcreteProps().placeholder; - // Note: `zero-width space` is insufficient in some cases (e.g. when we need - // to measure the "hight" of the font). - // TODO T67606511: We will redefine the measurement of empty strings as part - // of T67606511 - auto string = !placeholder.empty() - ? placeholder - : BaseTextShadowNode::getEmptyPlaceholder(); - auto textAttributes = getConcreteProps().getEffectiveTextAttributes( - layoutContext.fontSizeMultiplier); - attributedString.appendFragment({string, textAttributes, {}}); - } - - return AttributedStringBox{attributedString}; -} - -AttributedString TextInputShadowNode::getAttributedString( - const LayoutContext& layoutContext) const { - auto textAttributes = getConcreteProps().getEffectiveTextAttributes( - layoutContext.fontSizeMultiplier); - auto attributedString = AttributedString{}; - - attributedString.appendFragment(AttributedString::Fragment{ - .string = getConcreteProps().text, - .textAttributes = textAttributes, - // TODO: Is this really meant to be by value? - .parentShadowView = ShadowView{}}); - - auto attachments = Attachments{}; - BaseTextShadowNode::buildAttributedString( - textAttributes, *this, attributedString, attachments); - - return attributedString; -} - -void TextInputShadowNode::setTextLayoutManager( - std::shared_ptr textLayoutManager) { - ensureUnsealed(); - textLayoutManager_ = std::move(textLayoutManager); -} - -void TextInputShadowNode::updateStateIfNeeded( - const LayoutContext& layoutContext) { - ensureUnsealed(); - - auto reactTreeAttributedString = getAttributedString(layoutContext); - const auto& state = getStateData(); - - react_native_assert(textLayoutManager_); - react_native_assert( - (!state.layoutManager || state.layoutManager == textLayoutManager_) && - "`StateData` refers to a different `TextLayoutManager`"); - - if (state.reactTreeAttributedString == reactTreeAttributedString && - state.layoutManager == textLayoutManager_) { - return; - } - - auto newState = TextInputState{}; - newState.attributedStringBox = AttributedStringBox{reactTreeAttributedString}; - newState.paragraphAttributes = getConcreteProps().paragraphAttributes; - newState.reactTreeAttributedString = reactTreeAttributedString; - newState.layoutManager = textLayoutManager_; - newState.mostRecentEventCount = getConcreteProps().mostRecentEventCount; - setStateData(std::move(newState)); -} - -#pragma mark - LayoutableShadowNode - -Size TextInputShadowNode::measureContent( - const LayoutContext& layoutContext, - const LayoutConstraints& layoutConstraints) const { - TextLayoutContext textLayoutContext{}; - textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor; - return textLayoutManager_ - ->measure( - attributedStringBoxToMeasure(layoutContext), - getConcreteProps().getEffectiveParagraphAttributes(), - textLayoutContext, - layoutConstraints) - .size; -} - -Float TextInputShadowNode::baseline( - const LayoutContext& layoutContext, - Size size) const { - auto attributedString = getAttributedString(layoutContext); - - if (attributedString.isEmpty()) { - auto placeholderString = !getConcreteProps().placeholder.empty() - ? getConcreteProps().placeholder - : BaseTextShadowNode::getEmptyPlaceholder(); - auto textAttributes = getConcreteProps().getEffectiveTextAttributes( - layoutContext.fontSizeMultiplier); - attributedString.appendFragment( - {std::move(placeholderString), textAttributes, {}}); - } - - // Yoga expects a baseline relative to the Node's border-box edge instead of - // the content, so we need to adjust by the padding and border widths, which - // have already been set by the time of baseline alignment - auto top = YGNodeLayoutGetBorder(&yogaNode_, YGEdgeTop) + - YGNodeLayoutGetPadding(&yogaNode_, YGEdgeTop); - - AttributedStringBox attributedStringBox{std::move(attributedString)}; - return textLayoutManager_->baseline( - attributedStringBox, - getConcreteProps().getEffectiveParagraphAttributes(), - size) + - top; -} - -void TextInputShadowNode::layout(LayoutContext layoutContext) { - updateStateIfNeeded(layoutContext); - ConcreteViewShadowNode::layout(layoutContext); -} - } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h index f553ea284eaba1..188f64ec83259b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h @@ -7,14 +7,10 @@ #pragma once -#include -#include +#include #include -#include -#include -#include -#include -#include +#include +#include namespace facebook::react { @@ -23,65 +19,13 @@ extern const char TextInputComponentName[]; /* * `ShadowNode` for component. */ -class TextInputShadowNode final : public ConcreteViewShadowNode< +class TextInputShadowNode final : public BaseTextInputShadowNode< TextInputComponentName, TextInputProps, - TextInputEventEmitter, - TextInputState>, - public BaseTextShadowNode { + MacOSTextInputEventEmitter, + TextInputState> { public: - using ConcreteViewShadowNode::ConcreteViewShadowNode; - - TextInputShadowNode( - const ShadowNode& sourceShadowNode, - const ShadowNodeFragment& fragment); - - static ShadowNodeTraits BaseTraits() { - auto traits = ConcreteViewShadowNode::BaseTraits(); - traits.set(ShadowNodeTraits::Trait::LeafYogaNode); - traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode); - traits.set(ShadowNodeTraits::Trait::BaselineYogaNode); - return traits; - } - - /* - * Associates a shared `TextLayoutManager` with the node. - * `TextInputShadowNode` uses the manager to measure text content - * and construct `TextInputState` objects. - */ - void setTextLayoutManager( - std::shared_ptr textLayoutManager); - -#pragma mark - LayoutableShadowNode - - Size measureContent( - const LayoutContext& layoutContext, - const LayoutConstraints& layoutConstraints) const override; - void layout(LayoutContext layoutContext) override; - - Float baseline(const LayoutContext& layoutContext, Size size) const override; - - private: - /* - * Creates a `State` object if needed. - */ - void updateStateIfNeeded(const LayoutContext& layoutContext); - - /* - * Returns a `AttributedString` which represents text content of the node. - */ - AttributedString getAttributedString( - const LayoutContext& layoutContext) const; - - /* - * Returns an `AttributedStringBox` which represents text content that should - * be used for measuring purposes. It might contain actual text value, - * placeholder value or some character that represents the size of the font. - */ - AttributedStringBox attributedStringBoxToMeasure( - const LayoutContext& layoutContext) const; - - std::shared_ptr textLayoutManager_; + using BaseTextInputShadowNode::BaseTextInputShadowNode; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/primitives.h index 2057764b8b9ed7..91aeade42813d7 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 @@ -7,8 +7,11 @@ #pragma once +#include +#include #include #include +#include // [macOS] namespace facebook::react { @@ -47,14 +50,6 @@ enum class ReturnKeyType { Continue, }; -// iOS & Android. -enum class SubmitBehavior { - Default, - Submit, - BlurAndSubmit, - Newline, -}; - // iOS-only enum class TextInputAccessoryVisibilityMode { Never, @@ -223,6 +218,27 @@ class TextInputTraits final { */ std::string passwordRules{}; + /* + * List of key combinations that should submit. + * macOS + * Default value: `empty` applies as 'Enter' key. + */ + std::vector submitKeyEvents{}; + + /* + * When set to `true`, the text will be cleared after the submit. + * macOS-only + * Default value: `false` + */ + bool clearTextOnSubmit{false}; + + /* + * Can be empty (`null` in JavaScript) which means `default`. + * maOS + * Default value: `empty` (`null`). + */ + std::optional grammarCheck{}; + /* * If `false`, the iOS system will not insert an extra space after a paste * operation neither delete one or two spaces after a cut or delete operation. diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h index 7713c3c459977b..3b79474a65b702 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/propsConversions.h @@ -141,6 +141,12 @@ static TextInputTraits convertRawProp( "passwordRules", sourceTraits.passwordRules, defaultTraits.passwordRules); + traits.grammarCheck = convertRawProp( + context, + rawProps, + "grammarCheck", + sourceTraits.grammarCheck, + defaultTraits.grammarCheck); traits.smartInsertDelete = convertRawProp( context, rawProps, @@ -193,4 +199,49 @@ inline void fromRawValue( LOG(ERROR) << "Unsupported Selection type"; } } + +static inline void fromRawValue( + const PropsParserContext& context, + const RawValue& value, + KeyEvent& result) { + auto map = (std::unordered_map)value; + + auto tmp_key = map.find("key"); + if (tmp_key != map.end()) { + fromRawValue(context, tmp_key->second, result.key); + } + auto tmp_altKey = map.find("altKey"); + if (tmp_altKey != map.end()) { + fromRawValue(context, tmp_altKey->second, result.altKey); + } + auto tmp_shiftKey = map.find("shiftKey"); + if (tmp_shiftKey != map.end()) { + fromRawValue(context, tmp_shiftKey->second, result.shiftKey); + } + auto tmp_ctrlKey = map.find("ctrlKey"); + if (tmp_ctrlKey != map.end()) { + fromRawValue(context, tmp_ctrlKey->second, result.ctrlKey); + } + auto tmp_metaKey = map.find("metaKey"); + if (tmp_metaKey != map.end()) { + fromRawValue(context, tmp_metaKey->second, result.metaKey); + } + auto tmp_functionKey = map.find("functionKey"); + if (tmp_functionKey != map.end()) { + fromRawValue(context, tmp_functionKey->second, result.functionKey); + } +} + +static inline void fromRawValue( + const PropsParserContext& context, + const RawValue& value, + std::vector& result) { + auto items = (std::vector)value; + for (const auto& item : items) { + KeyEvent newItem; + fromRawValue(context, item, newItem); + result.emplace_back(newItem); + } +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/cxx/react/renderer/components/view/HostPlatformTouch.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/cxx/react/renderer/components/view/HostPlatformTouch.h index 0d441117751c89..bd5029f53f11ca 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/cxx/react/renderer/components/view/HostPlatformTouch.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/cxx/react/renderer/components/view/HostPlatformTouch.h @@ -1,14 +1,70 @@ -/* - * 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. - */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. #pragma once #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/HostPlatformTouch.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformTouch.h new file mode 100644 index 00000000000000..bd5029f53f11ca --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformTouch.h @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +namespace facebook::react { + +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 new file mode 100644 index 00000000000000..425b4bb2120f8f --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "HostPlatformViewEventEmitter.h" + +namespace facebook::react { + +#pragma mark - Keyboard Events + +static jsi::Value keyEventPayload( + jsi::Runtime& runtime, + const KeyEvent& event) { + auto payload = jsi::Object(runtime); + payload.setProperty( + runtime, "key", jsi::String::createFromUtf8(runtime, event.key)); + payload.setProperty(runtime, "ctrlKey", event.ctrlKey); + payload.setProperty(runtime, "shiftKey", event.shiftKey); + payload.setProperty(runtime, "altKey", event.altKey); + payload.setProperty(runtime, "metaKey", event.metaKey); + payload.setProperty(runtime, "capsLockKey", event.capsLockKey); + payload.setProperty(runtime, "numericPadKey", event.numericPadKey); + payload.setProperty(runtime, "helpKey", event.helpKey); + payload.setProperty(runtime, "functionKey", event.functionKey); + return payload; +}; + +void HostPlatformViewEventEmitter::onKeyDown(const KeyEvent& keyEvent) const { + dispatchEvent("keyDown", [keyEvent](jsi::Runtime& runtime) { + return keyEventPayload(runtime, keyEvent); + }); +} + +void HostPlatformViewEventEmitter::onKeyUp(const KeyEvent& keyEvent) const { + dispatchEvent("keyUp", [keyEvent](jsi::Runtime& runtime) { + return keyEventPayload(runtime, keyEvent); + }); +} + +#pragma mark - Mouse Events + +static jsi::Object mouseEventPayload( + jsi::Runtime& runtime, + const MouseEvent& event) { + auto payload = jsi::Object(runtime); + payload.setProperty(runtime, "clientX", event.clientX); + payload.setProperty(runtime, "clientY", event.clientY); + payload.setProperty(runtime, "screenX", event.screenX); + payload.setProperty(runtime, "screenY", event.screenY); + payload.setProperty(runtime, "altKey", event.altKey); + payload.setProperty(runtime, "ctrlKey", event.ctrlKey); + payload.setProperty(runtime, "shiftKey", event.shiftKey); + payload.setProperty(runtime, "metaKey", event.metaKey); + return payload; +}; + +void HostPlatformViewEventEmitter::onMouseEnter( + const MouseEvent& mouseEvent) const { + 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); + }); +} + +void HostPlatformViewEventEmitter::onDoubleClick( + const MouseEvent& mouseEvent) const { + dispatchEvent("doubleClick", [mouseEvent](jsi::Runtime& runtime) { + return mouseEventPayload(runtime, mouseEvent); + }); +} + +#pragma mark - Drag and Drop Events + +static jsi::Value dataTransferPayload( + jsi::Runtime& runtime, + const std::vector& dataTransferItems) { + auto filesArray = jsi::Array(runtime, dataTransferItems.size()); + auto itemsArray = jsi::Array(runtime, dataTransferItems.size()); + auto typesArray = jsi::Array(runtime, dataTransferItems.size()); + int i = 0; + for (const auto& transferItem : dataTransferItems) { + auto fileObject = jsi::Object(runtime); + fileObject.setProperty(runtime, "name", transferItem.name); + fileObject.setProperty(runtime, "type", transferItem.type); + fileObject.setProperty(runtime, "uri", transferItem.uri); + if (transferItem.size.has_value()) { + fileObject.setProperty(runtime, "size", *transferItem.size); + } + if (transferItem.width.has_value()) { + fileObject.setProperty(runtime, "width", *transferItem.width); + } + if (transferItem.height.has_value()) { + fileObject.setProperty(runtime, "height", *transferItem.height); + } + filesArray.setValueAtIndex(runtime, i, fileObject); + + auto itemObject = jsi::Object(runtime); + itemObject.setProperty(runtime, "kind", transferItem.kind); + itemObject.setProperty(runtime, "type", transferItem.type); + itemsArray.setValueAtIndex(runtime, i, itemObject); + + typesArray.setValueAtIndex(runtime, i, transferItem.type); + i++; + } + + auto dataTransferObject = jsi::Object(runtime); + dataTransferObject.setProperty(runtime, "files", filesArray); + dataTransferObject.setProperty(runtime, "items", itemsArray); + dataTransferObject.setProperty(runtime, "types", typesArray); + + return dataTransferObject; +} + +static jsi::Value dragEventPayload( + jsi::Runtime& runtime, + const DragEvent& event) { + auto payload = mouseEventPayload(runtime, event); + auto dataTransferObject = + dataTransferPayload(runtime, event.dataTransferItems); + payload.setProperty(runtime, "dataTransfer", dataTransferObject); + return payload; +} + +void HostPlatformViewEventEmitter::onDragEnter( + const DragEvent& dragEvent) const { + dispatchEvent("dragEnter", [dragEvent](jsi::Runtime& runtime) { + return dragEventPayload(runtime, dragEvent); + }); +} + +void HostPlatformViewEventEmitter::onDragLeave( + const DragEvent& dragEvent) const { + dispatchEvent("dragLeave", [dragEvent](jsi::Runtime& runtime) { + return dragEventPayload(runtime, dragEvent); + }); +} + +void HostPlatformViewEventEmitter::onDrop(const DragEvent& dragEvent) const { + dispatchEvent("drop", [dragEvent](jsi::Runtime& runtime) { + return dragEventPayload(runtime, dragEvent); + }); +} + +#pragma mark - Focus Events + +void HostPlatformViewEventEmitter::onFocus() const { + dispatchEvent("focus"); +} + +void HostPlatformViewEventEmitter::onBlur() const { + dispatchEvent("blur"); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h new file mode 100644 index 00000000000000..78131d2ba92d6d --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include "KeyEvent.h" +#include "MouseEvent.h" + +namespace facebook::react { + +class HostPlatformViewEventEmitter : public BaseViewEventEmitter { + public: + using BaseViewEventEmitter::BaseViewEventEmitter; + +#pragma mark - Keyboard Events + + void onKeyDown(const KeyEvent& keyEvent) const; + void onKeyUp(const KeyEvent& keyEvent) const; + +#pragma mark - Mouse Events + + 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(const DragEvent& dragEvent) const; + void onDragLeave(const DragEvent& dragEvent) const; + void onDrop(const DragEvent& dragEvent) const; + static jsi::Value dataTransferPayload( + jsi::Runtime& runtime, + const std::vector& dataTransferItems); + +#pragma mark - Focus Events + + void onFocus() const; + void onBlur() const; +}; + +} // 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 new file mode 100644 index 00000000000000..1f666ae86cf269 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h @@ -0,0 +1,98 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +struct HostPlatformViewEvents { + std::bitset<32> bits{}; + + enum class Offset : std::size_t { + // Keyboard Events + KeyDown = 1, + KeyUp = 2, + + // Mouse Events + MouseEnter = 3, + MouseLeave = 4, + DoubleClick = 5, + }; + + constexpr bool operator[](const Offset offset) const { + return bits[static_cast(offset)]; + } + + std::bitset<32>::reference operator[](const Offset offset) { + return bits[static_cast(offset)]; + } +}; + +inline static bool operator==( + const HostPlatformViewEvents& lhs, + const HostPlatformViewEvents& rhs) { + return lhs.bits == rhs.bits; +} + +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) { + HostPlatformViewEvents result{}; + using Offset = HostPlatformViewEvents::Offset; + + 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; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp new file mode 100644 index 00000000000000..f53647a1daef23 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp @@ -0,0 +1,124 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "HostPlatformViewProps.h" + +#include +#include +#include +#include + +namespace facebook::react { + +HostPlatformViewProps::HostPlatformViewProps( + const PropsParserContext& context, + const HostPlatformViewProps& sourceProps, + const RawProps& rawProps) + : BaseViewProps(context, sourceProps, rawProps), + hostPlatformEvents( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.hostPlatformEvents + : convertRawProp( + context, + rawProps, + sourceProps.hostPlatformEvents, + {})), + enableFocusRing( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.enableFocusRing + : convertRawProp( + context, + rawProps, + "enableFocusRing", + sourceProps.enableFocusRing, + true)), + focusable( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.focusable + : convertRawProp( + context, + rawProps, + "focusable", + sourceProps.focusable, + {})), + draggedTypes( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.draggedTypes + : convertRawProp( + context, + rawProps, + "draggedTypes", + sourceProps.draggedTypes, + {})), + tooltip( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.tooltip + : convertRawProp( + context, + rawProps, + "tooltip", + sourceProps.tooltip, + {})), + validKeysDown( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.validKeysDown + : convertRawProp( + context, + rawProps, + "validKeysDown", + sourceProps.validKeysDown, + {})), + validKeysUp( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.validKeysUp + : convertRawProp( + context, + rawProps, + "validKeysUp", + sourceProps.validKeysUp, + {})){}; + +#define VIEW_EVENT_CASE_MACOS(eventType) \ + case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \ + const auto offset = HostPlatformViewEvents::Offset::eventType; \ + HostPlatformViewEvents defaultViewEvents{}; \ + bool res = defaultViewEvents[offset]; \ + if (value.hasValue()) { \ + fromRawValue(context, value, res); \ + } \ + hostPlatformEvents[offset] = res; \ + return; \ + } + +void HostPlatformViewProps::setProp( + const PropsParserContext& context, + RawPropsPropNameHash hash, + const char* propName, + const RawValue& value) { + // All Props structs setProp methods must always, unconditionally, + // call all super::setProp methods, since multiple structs may + // reuse the same values. + BaseViewProps::setProp(context, hash, propName, value); + + static auto defaults = HostPlatformViewProps{}; + + switch (hash) { + VIEW_EVENT_CASE_MACOS(DoubleClick); + VIEW_EVENT_CASE_MACOS(KeyDown); + VIEW_EVENT_CASE_MACOS(KeyUp); + VIEW_EVENT_CASE_MACOS(MouseEnter); + VIEW_EVENT_CASE_MACOS(MouseLeave); + RAW_SET_PROP_SWITCH_CASE_BASIC(draggedTypes); + RAW_SET_PROP_SWITCH_CASE_BASIC(enableFocusRing); + RAW_SET_PROP_SWITCH_CASE_BASIC(focusable); + RAW_SET_PROP_SWITCH_CASE_BASIC(tooltip); + RAW_SET_PROP_SWITCH_CASE_BASIC(validKeysDown); + RAW_SET_PROP_SWITCH_CASE_BASIC(validKeysUp); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h new file mode 100644 index 00000000000000..4f7322cf8441c5 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include "HostPlatformViewEvents.h" +#include "KeyEvent.h" + +namespace facebook::react { +class HostPlatformViewProps : public BaseViewProps { + public: + HostPlatformViewProps() = default; + HostPlatformViewProps( + const PropsParserContext& context, + const HostPlatformViewProps& sourceProps, + const RawProps& rawProps); + + void setProp( + const PropsParserContext& context, + RawPropsPropNameHash hash, + const char* propName, + const RawValue& value); + + HostPlatformViewEvents hostPlatformEvents{}; + + bool enableFocusRing{true}; + bool focusable{false}; + + std::vector draggedTypes{}; + std::optional tooltip{}; + std::optional> validKeysDown{}; + std::optional> validKeysUp{}; +}; +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewTraitsInitializer.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewTraitsInitializer.h new file mode 100644 index 00000000000000..195f32d3a403ac --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewTraitsInitializer.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react::HostPlatformViewTraitsInitializer { + +inline bool formsStackingContext(const ViewProps& props) { + constexpr decltype(HostPlatformViewEvents::bits) mouseEventMask = { + (1 << (int)HostPlatformViewEvents::Offset::MouseEnter) | + (1 << (int)HostPlatformViewEvents::Offset::MouseLeave) | + (1 << (int)HostPlatformViewEvents::Offset::DoubleClick)}; + return (props.hostPlatformEvents.bits & mouseEventMask).any() || + props.tooltip; +} + +inline bool formsView(const ViewProps& props) { + return props.focusable; +} + +} // namespace facebook::react::HostPlatformViewTraitsInitializer diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/KeyEvent.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/KeyEvent.h new file mode 100644 index 00000000000000..5fbb9b735af80c --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/KeyEvent.h @@ -0,0 +1,135 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +/* + * Describes a request to handle a key input. + */ +struct HandledKey { + /** + * The key for the event aligned to https://www.w3.org/TR/uievents-key/. + */ + std::string key{}; + + /* + * A flag indicating if the alt key is pressed. + */ + std::optional altKey{}; + + /* + * A flag indicating if the control key is pressed. + */ + std::optional ctrlKey{}; + + /* + * A flag indicating if the shift key is pressed. + */ + std::optional shiftKey{}; + + /* + * A flag indicating if the meta key is pressed. + */ + std::optional metaKey{}; +}; + +inline static bool operator==(const HandledKey& lhs, const HandledKey& rhs) { + return lhs.key == rhs.key && lhs.altKey == rhs.altKey && + lhs.ctrlKey == rhs.ctrlKey && lhs.shiftKey == rhs.shiftKey && + lhs.metaKey == rhs.metaKey; +} + +/** + * Key event emitted by handled key events. + */ +struct KeyEvent { + /** + * The key for the event aligned to https://www.w3.org/TR/uievents-key/. + */ + std::string key{}; + + /* + * A flag indicating if the alt key is pressed. + */ + bool altKey{false}; + + /* + * A flag indicating if the control key is pressed. + */ + bool ctrlKey{false}; + + /* + * A flag indicating if the shift key is pressed. + */ + bool shiftKey{false}; + + /* + * A flag indicating if the meta key is pressed. + */ + bool metaKey{false}; + + /* + * A flag indicating if the caps lock key is pressed. + */ + bool capsLockKey{false}; + + /* + * A flag indicating if the key on the numeric pad is pressed. + */ + bool numericPadKey{false}; + + /* + * A flag indicating if the help key is pressed. + */ + bool helpKey{false}; + + /* + * A flag indicating if a function key is pressed. + */ + bool functionKey{false}; +}; + +inline static bool operator==(const KeyEvent& lhs, const HandledKey& rhs) { + return lhs.key == rhs.key && + (!rhs.altKey.has_value() || lhs.altKey == *rhs.altKey) && + (!rhs.ctrlKey.has_value() || lhs.ctrlKey == *rhs.ctrlKey) && + (!rhs.shiftKey.has_value() || lhs.shiftKey == *rhs.shiftKey) && + (!rhs.metaKey.has_value() || lhs.metaKey == *rhs.metaKey); +} + +inline void fromRawValue( + const PropsParserContext& context, + const RawValue& value, + HandledKey& result) { + if (value.hasType>()) { + auto map = static_cast>(value); + for (const auto& pair : map) { + if (pair.first == "key") { + result.key = static_cast(pair.second); + } else if (pair.first == "altKey") { + result.altKey = static_cast(pair.second); + } else if (pair.first == "ctrlKey") { + result.ctrlKey = static_cast(pair.second); + } else if (pair.first == "shiftKey") { + result.shiftKey = static_cast(pair.second); + } else if (pair.first == "metaKey") { + result.metaKey = static_cast(pair.second); + } + } + } else if (value.hasType()) { + result.key = (std::string)value; + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h new file mode 100644 index 00000000000000..4edc571b707949 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MouseEvent.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +/* + * Describes a mouse enter/leave event. + */ +struct MouseEvent { + /** + * Pointer horizontal location in target view. + */ + Float clientX{0}; + + /** + * Pointer vertical location in target view. + */ + Float clientY{0}; + + /** + * Pointer horizontal location in window. + */ + Float screenX{0}; + + /** + * Pointer vertical location in window. + */ + Float screenY{0}; + + /* + * A flag indicating if the alt key is pressed. + */ + bool altKey{false}; + + /* + * A flag indicating if the control key is pressed. + */ + bool ctrlKey{false}; + + /* + * A flag indicating if the shift key is pressed. + */ + bool shiftKey{false}; + + /* + * A flag indicating if the meta key is pressed. + */ + bool metaKey{false}; +}; + +struct DataTransferItem { + std::string name{}; + std::string kind{}; + std::string type{}; + std::string uri{}; + std::optional size{}; + std::optional width{}; + std::optional height{}; +}; + +struct DragEvent : MouseEvent { + std::vector dataTransferItems; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm index 9743762bd0f013..91467919721069 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 @@ -59,27 +59,30 @@ }]; 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 { @@ -97,7 +100,10 @@ int32_t ColorFromUIColor(RCTUIColor *color) // [macOS] [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&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]]; + [[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); @@ -125,7 +131,10 @@ int32_t ColorFromUIColor(const std::shared_ptr &uiColor) blue:components.blue alpha:components.alpha]; } - return [RCTUIColor colorWithRed:components.red green:components.green blue:components.blue alpha:components.alpha]; // [macOS] + return [RCTUIColor colorWithRed:components.red + green:components.green + blue:components.blue + alpha:components.alpha]; // [macOS] } } // anonymous namespace diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.h b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.h index 641e9c3303283a..9f164e34183ac7 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.h @@ -15,4 +15,5 @@ facebook::react::ColorComponents RCTPlatformColorComponentsFromSemanticItems( std::vector& semanticItems); RCTUIColor* RCTPlatformColorFromSemanticItems( // [macOS] std::vector& semanticItems); -RCTUIColor* RCTPlatformColorFromColor(const facebook::react::Color& color); // [macOS] +RCTUIColor* RCTPlatformColorFromColor( + const facebook::react::Color& color); // [macOS] diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm index 20ac1e6a296299..b7821d0c700c83 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm @@ -95,10 +95,12 @@ @"systemBrown" : @{ kFallbackARGBKey : @(0xFFa2845e) // iOS 13.0 }, + @"systemCyan" : @{}, @"systemGreen" : @{}, @"systemIndigo" : @{ kFallbackARGBKey : @(0xFF5856d6) // iOS 13.0 }, + @"systemMint" : @{}, @"systemOrange" : @{}, @"systemPink" : @{}, @"systemPurple" : @{}, @@ -158,7 +160,7 @@ return _UIColorFromHexValue(fallbackRGB); } } else { - Class uiColorClass = [RCTUIColor class]; // [macOS] + Class uiColorClass = [RCTUIColor class]; IMP imp = [uiColorClass methodForSelector:objcColorSelector]; id (*getUIColor)(id, SEL) = ((id(*)(id, SEL))imp); id colorObject = getUIColor(uiColorClass, objcColorSelector); diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/ImageManager.mm b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/ImageManager.mm index c8b41d05f8a276..0b1c63f3693419 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/ImageManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/ImageManager.mm @@ -35,6 +35,15 @@ } ImageRequest ImageManager::requestImage(const ImageSource &imageSource, SurfaceId surfaceId) const +{ + return requestImage(imageSource, surfaceId, ImageRequestParams{}, {}); +} + +ImageRequest ImageManager::requestImage( + const ImageSource &imageSource, + SurfaceId surfaceId, + const ImageRequestParams & /*imageRequestParams*/, + Tag /*tag*/) const { RCTImageManager *imageManager = (__bridge RCTImageManager *)self_; return [imageManager requestImage:imageSource surfaceId:surfaceId]; diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/ImageRequestParams.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/ImageRequestParams.h new file mode 100644 index 00000000000000..b10d38148b638c --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/ImageRequestParams.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +class ImageRequestParams { + public: + ImageRequestParams() {} + ImageRequestParams(Float blurRadius) : blurRadius(blurRadius) {} + + Float blurRadius{}; + + bool operator==(const ImageRequestParams& rhs) const { + return this->blurRadius == rhs.blurRadius; + } + + bool operator!=(const ImageRequestParams& rhs) const { + return !(*this == rhs); + } +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImageManager.mm b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImageManager.mm index f0f352f5f3149d..601dfe45ac38e9 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImageManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImageManager.mm @@ -7,7 +7,7 @@ #import "RCTImageManager.h" -#import +#import #import #import @@ -38,7 +38,7 @@ - (instancetype)initWithImageLoader:(id)i - (ImageRequest)requestImage:(ImageSource)imageSource surfaceId:(SurfaceId)surfaceId { - SystraceSection s("RCTImageManager::requestImage"); + TraceSection s("RCTImageManager::requestImage"); NSURLRequest *request = NSURLRequestFromImageSource(imageSource); std::shared_ptr telemetry; @@ -48,8 +48,9 @@ - (ImageRequest)requestImage:(ImageSource)imageSource surfaceId:(SurfaceId)surfa telemetry = nullptr; } + auto sharedResumeFunction = SharedFunction<>(); auto sharedCancelationFunction = SharedFunction<>(); - auto imageRequest = ImageRequest(imageSource, telemetry, sharedCancelationFunction); + auto imageRequest = ImageRequest(imageSource, telemetry, sharedResumeFunction, sharedCancelationFunction); auto weakObserverCoordinator = (std::weak_ptr)imageRequest.getSharedObserverCoordinator(); @@ -88,21 +89,28 @@ - (ImageRequest)requestImage:(ImageSource)imageSource surfaceId:(SurfaceId)surfa observerCoordinator->nativeImageResponseProgress((float)progress / (float)total, progress, total); }; - RCTImageURLLoaderRequest *loaderRequest = - [self->_imageLoader loadImageWithURLRequest:request - size:CGSizeMake(imageSource.size.width, imageSource.size.height) - scale:imageSource.scale - clipped:NO - resizeMode:RCTResizeModeStretch - priority:RCTImageLoaderPriorityImmediate - attribution:{ - .surfaceId = surfaceId, - } - progressBlock:progressBlock - partialLoadBlock:nil - completionBlock:completionBlock]; - RCTImageLoaderCancellationBlock cancelationBlock = loaderRequest.cancellationBlock; - sharedCancelationFunction.assign([cancelationBlock]() { cancelationBlock(); }); + void (^startRequest)() = ^() { + RCTImageURLLoaderRequest *loaderRequest = + [self->_imageLoader loadImageWithURLRequest:request + size:CGSizeMake(imageSource.size.width, imageSource.size.height) + scale:imageSource.scale + clipped:NO + resizeMode:RCTResizeModeStretch + priority:RCTImageLoaderPriorityImmediate + attribution:{ + .surfaceId = surfaceId, + } + progressBlock:progressBlock + partialLoadBlock:nil + completionBlock:completionBlock]; + + RCTImageLoaderCancellationBlock cancelationBlock = loaderRequest.cancellationBlock; + sharedCancelationFunction.assign([cancelationBlock]() { cancelationBlock(); }); + }; + + startRequest(); + + sharedResumeFunction.assign([startRequest]() { startRequest(); }); }); return imageRequest; diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h index c25c6b08a54e90..fd310472c39ca2 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h @@ -7,6 +7,7 @@ #import // [macOS] +#import #import #import @@ -25,6 +26,8 @@ inline static UIViewContentMode RCTContentModeFromImageResizeMode(facebook::reac // Repeat resize mode is handled by the UIImage. Use scale to fill // so the repeated image fills the UIImageView. return UIViewContentModeScaleToFill; + case facebook::react::ImageResizeMode::None: + return UIViewContentModeTopLeft; } } @@ -41,6 +44,27 @@ inline std::string toString(const facebook::react::ImageResizeMode &value) return "center"; case facebook::react::ImageResizeMode::Repeat: return "repeat"; + case facebook::react::ImageResizeMode::None: + return "none"; + } +} + +inline static NSURLRequestCachePolicy NSURLRequestCachePolicyFromImageSource( + const facebook::react::ImageSource &imageSource) +{ + switch (imageSource.cache) { + case facebook::react::ImageSource::CacheStategy::Reload: + return NSURLRequestReloadIgnoringLocalCacheData; + break; + case facebook::react::ImageSource::CacheStategy::ForceCache: + return NSURLRequestReturnCacheDataElseLoad; + break; + case facebook::react::ImageSource::CacheStategy::OnlyIfCached: + return NSURLRequestReturnCacheDataDontLoad; + break; + default: + return NSURLRequestUseProtocolCachePolicy; + break; } } @@ -102,13 +126,21 @@ inline static NSURLRequest *NSURLRequestFromImageSource(const facebook::react::I NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; - /* - // TODO(shergin): To be implemented. - request.HTTPBody = ...; - request.HTTPMethod = ...; - request.cachePolicy = ...; - request.allHTTPHeaderFields = ...; - */ + NSString *method = @"GET"; + if (!imageSource.method.empty()) { + method = [[NSString alloc] initWithUTF8String:imageSource.method.c_str()].uppercaseString; + } + NSData *body = nil; + if (!imageSource.body.empty()) { + body = [NSData dataWithBytes:imageSource.body.c_str() length:imageSource.body.size()]; + } + NSURLRequestCachePolicy cachePolicy = NSURLRequestCachePolicyFromImageSource(imageSource); + + if ([method isEqualToString:@"GET"] && imageSource.headers.empty() && body == nil && + cachePolicy == NSURLRequestUseProtocolCachePolicy) { + return request; + } + for (const auto &header : imageSource.headers) { NSString *key = [NSString stringWithUTF8String:header.first.c_str()]; NSString *value = [NSString stringWithUTF8String:header.second.c_str()]; @@ -117,5 +149,9 @@ inline static NSURLRequest *NSURLRequestFromImageSource(const facebook::react::I } } - return [request copy]; + request.HTTPBody = body; + request.HTTPMethod = method; + request.cachePolicy = cachePolicy; + + return request; } diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTSyncImageManager.mm b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTSyncImageManager.mm index a5a338fd28ed8d..b5c2dc83bd8cd2 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTSyncImageManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTSyncImageManager.mm @@ -38,7 +38,10 @@ - (ImageRequest)requestImage:(ImageSource)imageSource surfaceId:(SurfaceId)surfa { auto telemetry = std::make_shared(surfaceId); auto sharedCancelationFunction = SharedFunction<>(); - auto imageRequest = ImageRequest(imageSource, telemetry, sharedCancelationFunction); + + // Sync image request is not cancellable so it does not need to be resumed. + auto sharedResumeFunction = SharedFunction<>(); + auto imageRequest = ImageRequest(imageSource, telemetry, sharedResumeFunction, sharedCancelationFunction); auto weakObserverCoordinator = (std::weak_ptr)imageRequest.getSharedObserverCoordinator(); @@ -86,8 +89,6 @@ - (ImageRequest)requestImage:(ImageSource)imageSource surfaceId:(SurfaceId)surfa progressBlock:progressBlock partialLoadBlock:nil completionBlock:completionBlock]; - RCTImageLoaderCancellationBlock cancelationBlock = loaderRequest.cancellationBlock; - sharedCancelationFunction.assign([cancelationBlock]() { cancelationBlock(); }); auto result = dispatch_group_wait(imageWaitGroup, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC)); if (result != 0) { 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 b2299fb0a4442d..66a892d9d6d189 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 @@ -86,6 +86,7 @@ inline static UIFontTextStyle RCTUIFontTextStyleForDynamicTypeRamp(const Dynamic } #endif // [macOS] +#if !TARGET_OS_OSX // [macOS] inline static CGFloat RCTBaseSizeForDynamicTypeRamp(const DynamicTypeRamp &dynamicTypeRamp) { // Values taken from @@ -115,6 +116,7 @@ inline static CGFloat RCTBaseSizeForDynamicTypeRamp(const DynamicTypeRamp &dynam return 34.0; } } +#endif // [macOS] inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const TextAttributes &textAttributes) { @@ -162,7 +164,8 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex inline static RCTUIColor *RCTEffectiveForegroundColorFromTextAttributes(const TextAttributes &textAttributes) // [macOS] { - RCTUIColor *effectiveForegroundColor = RCTUIColorFromSharedColor(textAttributes.foregroundColor) ? RCTUIColorFromSharedColor(textAttributes.foregroundColor) : [RCTUIColor blackColor]; // [macOS] + RCTUIColor *effectiveForegroundColor = + RCTUIColorFromSharedColor(textAttributes.foregroundColor) ?: [RCTUIColor blackColor]; // [macOS] if (!isnan(textAttributes.opacity)) { effectiveForegroundColor = [effectiveForegroundColor @@ -340,7 +343,7 @@ void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) if (!font) { return; } - + maximumFontLineHeight = MAX(UIFontLineHeight(font), maximumFontLineHeight); // [macOS] }]; diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm index e82928d3d3dc8e..98944ca3f6fbd1 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm @@ -7,6 +7,7 @@ #import "RCTFontUtils.h" #import +#import #import #import @@ -45,12 +46,6 @@ static RCTFontProperties RCTResolveFontProperties( return fontProperties; } -static UIFontWeight RCTGetFontWeight(UIFont *font) -{ - NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; - return [traits[UIFontWeightTrait] doubleValue]; -} - static RCTFontStyle RCTGetFontStyle(UIFont *font) { NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; @@ -169,17 +164,26 @@ static RCTFontStyle RCTGetFontStyle(UIFont *font) #else // [macOS NSArray *fontNames = @[]; #endif // macOS] + UIFontWeight fontWeight = fontProperties.weight; if (fontNames.count == 0) { // Gracefully handle being given a font name rather than font family, for // example: "Helvetica Light Oblique" rather than just "Helvetica". font = [UIFont fontWithName:fontProperties.family size:effectiveFontSize]; - - if (!font) { + if (font) { +#if !TARGET_OS_OSX // [macOS] + fontNames = [UIFont fontNamesForFamilyName:font.familyName]; +#else // [macOS + fontNames = @[]; +#endif // macOS] + fontWeight = fontWeight ?: RCTGetFontWeight(font); + } else { // Failback to system font. font = [UIFont systemFontOfSize:effectiveFontSize weight:fontProperties.weight]; } - } else { + } + + if (fontNames.count > 0) { // Get the closest font that matches the given weight for the fontFamily CGFloat closestWeight = INFINITY; for (NSString *name in fontNames) { @@ -190,7 +194,7 @@ static RCTFontStyle RCTGetFontStyle(UIFont *font) } CGFloat testWeight = RCTGetFontWeight(fontMatch); - if (ABS(testWeight - fontProperties.weight) < ABS(closestWeight - fontProperties.weight)) { + if (ABS(testWeight - fontWeight) < ABS(closestWeight - fontWeight)) { font = fontMatch; closestWeight = testWeight; } diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index 390a07c8f6ead2..35ca825585dfdb 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -116,16 +116,15 @@ - (void)drawAttributedString:(AttributedString)attributedString bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2) cornerRadius:2]; #else // [macOS - NSBezierPath *path = [NSBezierPath - bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2) - xRadius:2 - yRadius:2]; + NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2) + xRadius:2 + yRadius:2]; #endif // macOS[ if (highlightPath) { #if !TARGET_OS_OSX // [macOS] [highlightPath appendPath:path]; #else // [macOS - [highlightPath appendBezierPath:path]; + [highlightPath appendBezierPath:path]; #endif // macOS] } else { highlightPath = path; @@ -214,27 +213,15 @@ - (LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedStr facebook::react::Point{usedRect.origin.x, usedRect.origin.y}, facebook::react::Size{usedRect.size.width, usedRect.size.height}}; - if (ReactNativeFeatureFlags::enableAlignItemsBaselineOnFabricIOS()) { - CGFloat baseline = - [layoutManager locationForGlyphAtIndex:range.location].y; - auto line = LineMeasurement{ - std::string([renderedString UTF8String]), - rect, - overallRect.size.height - baseline, - font.capHeight, - baseline, - font.xHeight}; - blockParagraphLines->push_back(line); - } else { - auto line = LineMeasurement{ - std::string([renderedString UTF8String]), - rect, - -font.descender, - font.capHeight, - font.ascender, - font.xHeight}; - blockParagraphLines->push_back(line); - } + CGFloat baseline = [layoutManager locationForGlyphAtIndex:range.location].y; + auto line = LineMeasurement{ + std::string([renderedString UTF8String]), + rect, + overallRect.size.height - baseline, + font.capHeight, + baseline, + font.xHeight}; + blockParagraphLines->push_back(line); }]; return paragraphLines; } @@ -358,9 +345,35 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage NSTextContainer *textContainer = layoutManager.textContainers.firstObject; [layoutManager ensureLayoutForTextContainer:textContainer]; + NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + __block BOOL textDidWrap = NO; + [layoutManager + enumerateLineFragmentsForGlyphRange:glyphRange + usingBlock:^( + CGRect overallRect, + CGRect usedRect, + NSTextContainer *_Nonnull usedTextContainer, + NSRange lineGlyphRange, + BOOL *_Nonnull stop) { + NSRange range = [layoutManager characterRangeForGlyphRange:lineGlyphRange + actualGlyphRange:nil]; + NSUInteger lastCharacterIndex = range.location + range.length - 1; + BOOL endsWithNewLine = + [textStorage.string characterAtIndex:lastCharacterIndex] == '\n'; + if (!endsWithNewLine && textStorage.string.length > lastCharacterIndex + 1) { + textDidWrap = YES; + *stop = YES; + } + }]; + CGSize size = [layoutManager usedRectForTextContainer:textContainer].size; + if (textDidWrap) { + size.width = textContainer.size.width; + } + size = (CGSize){RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height)}; + __block auto attachments = TextMeasurement::Attachments{}; [textStorage @@ -376,18 +389,9 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range inTextContainer:textContainer]; CGRect frame; - if (ReactNativeFeatureFlags::enableAlignItemsBaselineOnFabricIOS()) { - CGFloat baseline = [layoutManager locationForGlyphAtIndex:range.location].y; + CGFloat baseline = [layoutManager locationForGlyphAtIndex:range.location].y; - frame = {{glyphRect.origin.x, glyphRect.origin.y + baseline - attachmentSize.height}, attachmentSize}; - } else { - UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil]; - - frame = { - {glyphRect.origin.x, - glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender}, - attachmentSize}; - } + frame = {{glyphRect.origin.x, glyphRect.origin.y + baseline - attachmentSize.height}, attachmentSize}; auto rect = facebook::react::Rect{ facebook::react::Point{frame.origin.x, frame.origin.y}, diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h index 9a416670ecbf17..8b2f86c3e0ba4f 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h @@ -49,13 +49,13 @@ inline static NSLineBreakStrategy RCTNSLineBreakStrategyFromLineBreakStrategy( case facebook::react::LineBreakStrategy::PushOut: return NSLineBreakStrategyPushOut; case facebook::react::LineBreakStrategy::HangulWordPriority: - if (@available(iOS 14.0, *)) { + if (@available(iOS 14.0, macOS 11.0, *)) { // [macOS] return NSLineBreakStrategyHangulWordPriority; } else { return NSLineBreakStrategyNone; } case facebook::react::LineBreakStrategy::Standard: - if (@available(iOS 14.0, *)) { + if (@available(iOS 14.0, macOS 11.0, *)) { // [macOS] return NSLineBreakStrategyStandard; } else { return NSLineBreakStrategyNone; @@ -114,7 +114,8 @@ inline static NSUnderlineStyle RCTNSUnderlineStyleFromTextDecorationStyle( } // TODO: this file has some duplicates method, we can remove it -inline static RCTUIColor *_Nullable RCTUIColorFromSharedColor(const facebook::react::SharedColor &sharedColor) // [macOS] +inline static RCTUIColor *_Nullable RCTUIColorFromSharedColor( + const facebook::react::SharedColor &sharedColor) // [macOS] { return RCTPlatformColorFromColor(*sharedColor); } diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm index a34667060b08c6..cc0edbccfbcae5 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm @@ -5,23 +5,23 @@ * LICENSE file in the root directory of this source tree. */ -#include "TextLayoutManager.h" -#include -#include - +#import "TextLayoutManager.h" #import "RCTTextLayoutManager.h" +#import +#import + namespace facebook::react { TextLayoutManager::TextLayoutManager(const ContextContainer::Shared &contextContainer) { - self_ = wrapManagedObject([RCTTextLayoutManager new]); + nativeTextLayoutManager_ = wrapManagedObject([RCTTextLayoutManager new]); } std::shared_ptr TextLayoutManager::getNativeTextLayoutManager() const { - assert(self_ && "Stored NativeTextLayoutManager must not be null."); - return self_; + assert(nativeTextLayoutManager_ && "Stored NativeTextLayoutManager must not be null."); + return nativeTextLayoutManager_; } TextMeasurement TextLayoutManager::measure( @@ -30,7 +30,7 @@ const TextLayoutContext &layoutContext, const LayoutConstraints &layoutConstraints) const { - RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(self_); + RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(nativeTextLayoutManager_); auto measurement = TextMeasurement{}; @@ -92,7 +92,7 @@ react_native_assert(attributedStringBox.getMode() == AttributedStringBox::Mode::Value); const auto &attributedString = attributedStringBox.getValue(); - RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(self_); + RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(nativeTextLayoutManager_); auto measurement = lineMeasureCache_.get({attributedString, paragraphAttributes, size}, [&](const LineMeasureCacheKey &key) { @@ -105,18 +105,4 @@ return measurement; } -Float TextLayoutManager::baseline( - const AttributedStringBox &attributedStringBox, - const ParagraphAttributes ¶graphAttributes, - const Size &size) const -{ - auto lines = this->measureLines(attributedStringBox, paragraphAttributes, size); - - if (!lines.empty()) { - return lines[0].ascender; - } else { - return 0; - } -} - } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHermesInstance.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHermesInstance.mm index bd3ea5c7a97ae2..d23d991cfdbb99 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHermesInstance.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHermesInstance.mm @@ -26,10 +26,7 @@ std::shared_ptr msgQueueThread) noexcept { return _hermesInstance->createJSRuntime( - _reactNativeConfig, - _crashManagerProvider ? _crashManagerProvider() : nullptr, - std::move(msgQueueThread), - _allocInOldGenBeforeTTI); + _crashManagerProvider ? _crashManagerProvider() : nullptr, std::move(msgQueueThread), _allocInOldGenBeforeTTI); } } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost+Internal.h b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost+Internal.h index e1708784a45340..d039fa668f0462 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost+Internal.h +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost+Internal.h @@ -14,5 +14,8 @@ - (void)registerSegmentWithId:(NSNumber *)segmentId path:(NSString *)path; - (void)setBundleURLProvider:(RCTHostBundleURLProvider)bundleURLProvider; - (void)setContextContainerHandler:(id)contextContainerHandler; +- (void)reload; + +@property (nonatomic, readonly) RCTBundleManager *bundleManager; @end diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.h b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.h index 8f32300607290f..ccf8d9d214e14c 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.h +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.h @@ -59,6 +59,9 @@ typedef std::shared_ptr (^RCTHostJSEngineProv jsEngineProvider:(RCTHostJSEngineProvider)jsEngineProvider launchOptions:(nullable NSDictionary *)launchOptions __deprecated; +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + @property (nonatomic, weak, nullable) id runtimeDelegate; @property (nonatomic, readonly) RCTSurfacePresenter *surfacePresenter; diff --git a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm index 48511d3cc71c26..55f74bdd7054e7 100644 --- a/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm +++ b/packages/react-native/ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm @@ -52,7 +52,7 @@ @interface RCTHost () .appDisplayName = [metadata.appDisplayName UTF8String], .appIdentifier = [metadata.appIdentifier UTF8String], .deviceName = [metadata.deviceName UTF8String], - .integrationName = "iOS Bridgeless (RCTHost)", + .integrationName = [[NSString stringWithFormat:@"%@ Bridgeless (RCTHost)", metadata.platform] UTF8String], .platform = [metadata.platform UTF8String], .reactNativeVersion = [metadata.reactNativeVersion UTF8String], }; @@ -273,6 +273,11 @@ - (RCTSurfacePresenter *)surfacePresenter return [_instance surfacePresenter]; } +- (RCTBundleManager *)bundleManager +{ + return _bundleManager; +} + - (void)callFunctionOnJSModule:(NSString *)moduleName method:(NSString *)method args:(NSArray *)args { [_instance callFunctionOnJSModule:moduleName method:method args:args]; @@ -282,25 +287,7 @@ - (void)callFunctionOnJSModule:(NSString *)moduleName method:(NSString *)method - (void)didReceiveReloadCommand { - [_instance invalidate]; - _instance = nil; - if (_bundleURLProvider) { - [self _setBundleURL:_bundleURLProvider()]; - } - - _instance = [[RCTInstance alloc] initWithDelegate:self - jsRuntimeFactory:[self _provideJSEngine] - bundleManager:_bundleManager - turboModuleManagerDelegate:_turboModuleManagerDelegate - moduleRegistry:_moduleRegistry - parentInspectorTarget:_inspectorTarget.get() - launchOptions:_launchOptions]; - [_hostDelegate hostDidStart:self]; - - for (RCTFabricSurface *surface in [self _getAttachedSurfaces]) { - [surface resetWithSurfacePresenter:self.surfacePresenter]; - [_instance callFunctionOnBufferedRuntimeExecutor:[surface](facebook::jsi::Runtime &_) { [surface start]; }]; - } + [self _reloadWithShouldRestartSurfaces:YES]; } - (void)dealloc @@ -315,13 +302,32 @@ - (void)dealloc #pragma mark - RCTInstanceDelegate -- (void)instance:(RCTInstance *)instance +- (BOOL)instance:(RCTInstance *)instance didReceiveJSErrorStack:(NSArray *> *)stack message:(NSString *)message + originalMessage:(NSString *_Nullable)originalMessage + name:(NSString *_Nullable)name + componentStack:(NSString *_Nullable)componentStack exceptionId:(NSUInteger)exceptionId isFatal:(BOOL)isFatal + extraData:(NSDictionary *)extraData { - [_hostDelegate host:self didReceiveJSErrorStack:stack message:message exceptionId:exceptionId isFatal:isFatal]; + if (![_hostDelegate respondsToSelector:@selector(host: + didReceiveJSErrorStack:message:originalMessage:name:componentStack + :exceptionId:isFatal:extraData:)]) { + return NO; + } + + [_hostDelegate host:self + didReceiveJSErrorStack:stack + message:message + originalMessage:originalMessage + name:name + componentStack:componentStack + exceptionId:exceptionId + isFatal:isFatal + extraData:extraData]; + return YES; } - (void)instance:(RCTInstance *)instance didInitializeRuntime:(facebook::jsi::Runtime &)runtime @@ -353,6 +359,11 @@ - (void)setContextContainerHandler:(id)contextConta _contextContainerHandler = contextContainerHandler; } +- (void)reload +{ + [self _reloadWithShouldRestartSurfaces:NO]; +} + #pragma mark - Private - (void)_attachSurface:(RCTFabricSurface *)surface @@ -397,6 +408,33 @@ - (void)_setBundleURL:(NSURL *)bundleURL RCTReloadCommandSetBundleURL(_bundleURL); } +- (void)_reloadWithShouldRestartSurfaces:(BOOL)shouldRestartSurfaces +{ + [_instance invalidate]; + _instance = nil; + if (_bundleURLProvider) { + [self _setBundleURL:_bundleURLProvider()]; + } + + _instance = [[RCTInstance alloc] initWithDelegate:self + jsRuntimeFactory:[self _provideJSEngine] + bundleManager:_bundleManager + turboModuleManagerDelegate:_turboModuleManagerDelegate + moduleRegistry:_moduleRegistry + parentInspectorTarget:_inspectorTarget.get() + launchOptions:_launchOptions]; + [_hostDelegate hostDidStart:self]; + + for (RCTFabricSurface *surface in [self _getAttachedSurfaces]) { + [surface resetWithSurfacePresenter:self.surfacePresenter]; + if (shouldRestartSurfaces) { + [_instance callFunctionOnBufferedRuntimeExecutor:[surface](facebook::jsi::Runtime &_) { [surface start]; }]; + } + } +} + +#pragma mark - jsinspector_modern + - (jsinspector_modern::HostTarget *)inspectorTarget { return _inspectorTarget.get(); 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 20d453ff12b651..d710c7e06254a3 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 @@ -9,6 +9,7 @@ #import +#import #import #import #import @@ -32,11 +33,13 @@ #import #import #import +#import #import #import #import #import #import +#import #import #import #import @@ -67,7 +70,7 @@ void RCTInstanceSetRuntimeDiagnosticFlags(NSString *flags) sRuntimeDiagnosticFlags = [flags copy]; } -@interface RCTInstance () +@interface RCTInstance () @end @implementation RCTInstance { @@ -119,8 +122,11 @@ - (instancetype)initWithDelegate:(id)delegate [_bridgeModuleDecorator.callableJSModules setBridgelessJSModuleMethodInvoker:^( NSString *moduleName, NSString *methodName, NSArray *args, dispatch_block_t onComplete) { - // TODO: Make RCTInstance call onComplete [weakSelf callFunctionOnJSModule:moduleName method:methodName args:args]; + if (onComplete) { + [weakSelf + callFunctionOnBufferedRuntimeExecutor:[onComplete](facebook::jsi::Runtime &_) { onComplete(); }]; + } }]; } _launchOptions = launchOptions; @@ -134,7 +140,7 @@ - (void)callFunctionOnJSModule:(NSString *)moduleName method:(NSString *)method { if (_valid) { _reactInstance->callFunctionOnModule( - [moduleName UTF8String], [method UTF8String], convertIdToFollyDynamic(args ?: @[])); + [moduleName UTF8String], [method UTF8String], convertIdToFollyDynamic(args ? args : @[])); } } @@ -142,8 +148,8 @@ - (void)invalidate { std::lock_guard lock(_invalidationMutex); _valid = false; - if (self->_reactInstance) { - self->_reactInstance->unregisterFromInspector(); + if (_reactInstance) { + _reactInstance->unregisterFromInspector(); } [_surfacePresenter suspend]; [_jsThreadManager dispatchToJSThread:^{ @@ -208,15 +214,13 @@ - (Class)getModuleClassFromName:(const char *)name return nullptr; } -#pragma mark - RCTTurboModuleManagerRuntimeHandler - -- (RuntimeExecutor)runtimeExecutorForTurboModuleManager:(RCTTurboModuleManager *)turboModuleManager +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge { - if (_valid) { - return _reactInstance->getBufferedRuntimeExecutor(); + if ([_appTMMDelegate respondsToSelector:@selector(extraModulesForBridge:)]) { + return [_appTMMDelegate extraModulesForBridge:nil]; } - return nullptr; + return @[]; } #pragma mark - Private @@ -232,7 +236,9 @@ - (void)_start objCTimerRegistryRawPtr->setTimerManager(timerManager); __weak __typeof(self) weakSelf = self; - auto onJsError = [=](const JsErrorHandler::ParsedError &error) { [weakSelf _handleJSError:error]; }; + auto onJsError = [=](jsi::Runtime &runtime, const JsErrorHandler::ProcessedError &error) { + [weakSelf _handleJSError:error withRuntime:runtime]; + }; // Create the React Instance _reactInstance = std::make_unique( @@ -274,7 +280,6 @@ - (void)_start bridgeModuleDecorator:_bridgeModuleDecorator delegate:self jsInvoker:jsCallInvoker]; - _turboModuleManager.runtimeHandler = self; #if RCT_DEV /** @@ -301,7 +306,7 @@ - (void)_start "RCTEventDispatcher", facebook::react::wrapManagedObject([_turboModuleManager moduleForName:"RCTEventDispatcher"])); contextContainer->insert("RCTBridgeModuleDecorator", facebook::react::wrapManagedObject(_bridgeModuleDecorator)); - contextContainer->insert("RuntimeScheduler", std::weak_ptr(_reactInstance->getRuntimeScheduler())); + contextContainer->insert(RuntimeSchedulerKey, std::weak_ptr(_reactInstance->getRuntimeScheduler())); contextContainer->insert("RCTBridgeProxy", facebook::react::wrapManagedObject(bridgeProxy)); _surfacePresenter = [[RCTSurfacePresenter alloc] @@ -311,14 +316,15 @@ - (void)_start // This enables RCTViewRegistry in modules to return UIViews from its viewForReactTag method __weak RCTSurfacePresenter *weakSurfacePresenter = _surfacePresenter; - [_bridgeModuleDecorator.viewRegistry_DEPRECATED setBridgelessComponentViewProvider:^RCTPlatformView *(NSNumber *reactTag) { // [macOS] - RCTSurfacePresenter *strongSurfacePresenter = weakSurfacePresenter; - if (strongSurfacePresenter == nil) { - return nil; - } + [_bridgeModuleDecorator.viewRegistry_DEPRECATED + setBridgelessComponentViewProvider:^RCTPlatformView *(NSNumber *reactTag) { // [macOS] + RCTSurfacePresenter *strongSurfacePresenter = weakSurfacePresenter; + if (strongSurfacePresenter == nil) { + return nil; + } - return [strongSurfacePresenter findComponentViewWithTag_DO_NOT_USE_DEPRECATED:reactTag.integerValue]; - }]; + return [strongSurfacePresenter findComponentViewWithTag_DO_NOT_USE_DEPRECATED:reactTag.integerValue]; + }]; // DisplayLink is used to call timer callbacks. _displayLink = [RCTDisplayLink new]; @@ -337,7 +343,7 @@ - (void)_start }); RCTInstallNativeComponentRegistryBinding(runtime); - if (RCTGetUseNativeViewConfigsInBridgelessMode()) { + if (ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode()) { installLegacyUIManagerConstantsProviderBinding(runtime); } @@ -475,23 +481,59 @@ - (void)_loadScriptFromSource:(RCTSource *)source [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTInstanceDidLoadBundle" object:nil]; } -- (void)_handleJSError:(const JsErrorHandler::ParsedError &)error +- (void)_handleJSError:(const JsErrorHandler::ProcessedError &)error withRuntime:(jsi::Runtime &)runtime { - NSString *message = [NSString stringWithCString:error.message.c_str() encoding:[NSString defaultCStringEncoding]]; + NSMutableDictionary *errorData = [NSMutableDictionary new]; + errorData[@"message"] = @(error.message.c_str()); + if (error.originalMessage) { + errorData[@"originalMessage"] = @(error.originalMessage->c_str()); + } + if (error.name) { + errorData[@"name"] = @(error.name->c_str()); + } + if (error.componentStack) { + errorData[@"componentStack"] = @(error.componentStack->c_str()); + } + NSMutableArray *> *stack = [NSMutableArray new]; - for (const JsErrorHandler::ParsedError::StackFrame &frame : error.frames) { - [stack addObject:@{ - @"file" : [NSString stringWithCString:frame.fileName.c_str() encoding:[NSString defaultCStringEncoding]], - @"methodName" : [NSString stringWithCString:frame.methodName.c_str() encoding:[NSString defaultCStringEncoding]], - @"lineNumber" : [NSNumber numberWithInt:frame.lineNumber], - @"column" : [NSNumber numberWithInt:frame.columnNumber], - }]; + for (const JsErrorHandler::ProcessedError::StackFrame &frame : error.stack) { + NSMutableDictionary *stackFrame = [NSMutableDictionary new]; + if (frame.file) { + stackFrame[@"file"] = @(frame.file->c_str()); + } + stackFrame[@"methodName"] = @(frame.methodName.c_str()); + if (frame.lineNumber) { + stackFrame[@"lineNumber"] = @(*frame.lineNumber); + } + if (frame.column) { + stackFrame[@"column"] = @(*frame.column); + } + [stack addObject:stackFrame]; + } + + errorData[@"stack"] = stack; + errorData[@"id"] = @(error.id); + errorData[@"isFatal"] = @(error.isFatal); + + id extraData = + TurboModuleConvertUtils::convertJSIValueToObjCObject(runtime, jsi::Value(runtime, error.extraData), nullptr); + if (extraData) { + errorData[@"extraData"] = extraData; + } + + if (![_delegate instance:self + didReceiveJSErrorStack:errorData[@"stack"] + message:errorData[@"message"] + originalMessage:errorData[@"originalMessage"] + name:errorData[@"name"] + componentStack:errorData[@"componentStack"] + exceptionId:error.id + isFatal:error.isFatal // [macOS] TODO(T210743757): Remove fork + extraData:errorData[@"extraData"]]) { + JS::NativeExceptionsManager::ExceptionData jsErrorData{errorData}; + id exceptionsManager = [_turboModuleManager moduleForName:"ExceptionsManager"]; + [exceptionsManager reportException:jsErrorData]; } - [_delegate instance:self - didReceiveJSErrorStack:stack - message:message - exceptionId:error.exceptionId - isFatal:error.isFatal]; } @end diff --git a/packages/react-native/index.js b/packages/react-native/index.js index e14ffd0bf5fb16..6b7d19c3bfa716 100644 --- a/packages/react-native/index.js +++ b/packages/react-native/index.js @@ -223,7 +223,7 @@ module.exports = { return require('./Libraries/ReactNative/AppRegistry'); }, get AppState(): AppState { - return require('./Libraries/AppState/AppState'); + return require('./Libraries/AppState/AppState').default; }, get BackHandler(): BackHandler { return require('./Libraries/Utilities/BackHandler'); @@ -259,7 +259,7 @@ module.exports = { return require('./Libraries/Interaction/InteractionManager'); }, get Keyboard(): Keyboard { - return require('./Libraries/Components/Keyboard/Keyboard'); + return require('./Libraries/Components/Keyboard/Keyboard').default; }, get LayoutAnimation(): LayoutAnimation { return require('./Libraries/LayoutAnimation/LayoutAnimation'); diff --git a/packages/react-native/src/private/animated/NativeAnimatedHelper.js b/packages/react-native/src/private/animated/NativeAnimatedHelper.js index e4cdcf3d0e7ea2..1ff9ce6dd421ec 100644 --- a/packages/react-native/src/private/animated/NativeAnimatedHelper.js +++ b/packages/react-native/src/private/animated/NativeAnimatedHelper.js @@ -8,7 +8,6 @@ * @format */ -import type {EventSubscription} from '../../../Libraries/vendor/emitter/EventEmitter'; import type {EventConfig} from '../../../Libraries/Animated/AnimatedEvent'; import type { AnimationConfig, @@ -18,13 +17,14 @@ import type { AnimatedNodeConfig, EventMapping, } from '../../../Libraries/Animated/NativeAnimatedModule'; +import type {EventSubscription} from '../../../Libraries/vendor/emitter/EventEmitter'; -import * as ReactNativeFeatureFlags from '../featureflags/ReactNativeFeatureFlags'; +import NativeAnimatedNonTurboModule from '../../../Libraries/Animated/NativeAnimatedModule'; +import NativeAnimatedTurboModule from '../../../Libraries/Animated/NativeAnimatedTurboModule'; import NativeEventEmitter from '../../../Libraries/EventEmitter/NativeEventEmitter'; import RCTDeviceEventEmitter from '../../../Libraries/EventEmitter/RCTDeviceEventEmitter'; import Platform from '../../../Libraries/Utilities/Platform'; -import NativeAnimatedNonTurboModule from '../../../Libraries/Animated/NativeAnimatedModule'; -import NativeAnimatedTurboModule from '../../../Libraries/Animated/NativeAnimatedTurboModule'; +import * as ReactNativeFeatureFlags from '../featureflags/ReactNativeFeatureFlags'; import invariant from 'invariant'; import nullthrows from 'nullthrows'; @@ -46,7 +46,7 @@ const isSingleOpBatching = Platform.OS === 'android' && NativeAnimatedModule?.queueAndExecuteBatchedOperations != null && ReactNativeFeatureFlags.animatedShouldUseSingleOp(); -let flushQueueTimeout = null; +let flushQueueImmediate = null; const eventListenerGetValueCallbacks: { [number]: (value: number) => void, @@ -142,9 +142,13 @@ const API = { queueOperations = true; if ( ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush() && - flushQueueTimeout + flushQueueImmediate ) { - clearTimeout(flushQueueTimeout); + if (ReactNativeFeatureFlags.enableAnimatedClearImmediateFix()) { + clearImmediate(flushQueueImmediate); + } else { + clearTimeout(flushQueueImmediate); + } } }, @@ -161,9 +165,9 @@ const API = { invariant(NativeAnimatedModule, 'Native animated module is not available'); if (ReactNativeFeatureFlags.animatedShouldDebounceQueueFlush()) { - const prevTimeout = flushQueueTimeout; - clearImmediate(prevTimeout); - flushQueueTimeout = setImmediate(API.flushQueue); + const prevImmediate = flushQueueImmediate; + clearImmediate(prevImmediate); + flushQueueImmediate = setImmediate(API.flushQueue); } else { API.flushQueue(); } @@ -171,12 +175,11 @@ const API = { flushQueue: (isSingleOpBatching ? (): void => { - // TODO: (T136971132) invariant( - NativeAnimatedModule || process.env.NODE_ENV === 'test', + NativeAnimatedModule, 'Native animated module is not available', ); - flushQueueTimeout = null; + flushQueueImmediate = null; if (singleOpQueue.length === 0) { return; @@ -193,12 +196,11 @@ const API = { singleOpQueue.length = 0; } : (): void => { - // TODO: (T136971132) invariant( - NativeAnimatedModule || process.env.NODE_ENV === 'test', + NativeAnimatedModule, 'Native animated module is not available', ); - flushQueueTimeout = null; + flushQueueImmediate = null; if (queue.length === 0) { return; diff --git a/packages/react-native/src/private/debugging/ReactDevToolsSettingsManager.ios.js b/packages/react-native/src/private/debugging/ReactDevToolsSettingsManager.ios.js new file mode 100644 index 00000000000000..b5bc2a655587a1 --- /dev/null +++ b/packages/react-native/src/private/debugging/ReactDevToolsSettingsManager.ios.js @@ -0,0 +1,30 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import Settings from '../../../Libraries/Settings/Settings'; + +const GLOBAL_HOOK_SETTINGS = 'ReactDevTools::HookSettings'; + +const ReactDevToolsSettingsManager = { + setGlobalHookSettings(settings: string): void { + Settings.set({ + [GLOBAL_HOOK_SETTINGS]: settings, + }); + }, + getGlobalHookSettings(): ?string { + const value = Settings.get(GLOBAL_HOOK_SETTINGS); + if (typeof value === 'string') { + return value; + } + return null; + }, +}; + +module.exports = ReactDevToolsSettingsManager; diff --git a/packages/react-native/src/private/setup/setUpDOM.js b/packages/react-native/src/private/setup/setUpDOM.js index 04f12e49dd6bcf..f815c86c4e842f 100644 --- a/packages/react-native/src/private/setup/setUpDOM.js +++ b/packages/react-native/src/private/setup/setUpDOM.js @@ -8,8 +8,7 @@ * @format */ -import DOMRect from '../webapis/dom/geometry/DOMRect'; -import DOMRectReadOnly from '../webapis/dom/geometry/DOMRectReadOnly'; +import {polyfillGlobal} from '../../../Libraries/Utilities/PolyfillFunctions'; let initialized = false; @@ -20,9 +19,18 @@ export default function setUpDOM() { initialized = true; - // $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it - global.DOMRect = DOMRect; + polyfillGlobal( + 'DOMRect', + () => require('../webapis/dom/geometry/DOMRect').default, + ); - // $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it - global.DOMRectReadOnly = DOMRectReadOnly; + polyfillGlobal( + 'DOMRectReadOnly', + () => require('../webapis/dom/geometry/DOMRectReadOnly').default, + ); + + polyfillGlobal( + 'NodeList', + () => require('../webapis/dom/oldstylecollections/NodeList').default, + ); } diff --git a/packages/react-native/src/private/setup/setUpMutationObserver.js b/packages/react-native/src/private/setup/setUpMutationObserver.js index 387d893ab75c6a..e3f03b59172eae 100644 --- a/packages/react-native/src/private/setup/setUpMutationObserver.js +++ b/packages/react-native/src/private/setup/setUpMutationObserver.js @@ -23,4 +23,9 @@ export default function setUpMutationObserver() { 'MutationObserver', () => require('../webapis/mutationobserver/MutationObserver').default, ); + + polyfillGlobal( + 'MutationRecord', + () => require('../webapis/mutationobserver/MutationRecord').default, + ); } diff --git a/packages/react-native/src/private/specs/components/AndroidHorizontalScrollContentViewNativeComponent.js b/packages/react-native/src/private/specs/components/AndroidHorizontalScrollContentViewNativeComponent.js index 98bf5c55ee4d87..7b0f3bdfe1aeec 100644 --- a/packages/react-native/src/private/specs/components/AndroidHorizontalScrollContentViewNativeComponent.js +++ b/packages/react-native/src/private/specs/components/AndroidHorizontalScrollContentViewNativeComponent.js @@ -23,4 +23,5 @@ type NativeType = HostComponent; export default (codegenNativeComponent( 'AndroidHorizontalScrollContentView', + {interfaceOnly: true}, ): NativeType); diff --git a/packages/react-native/src/private/specs/components/RCTModalHostViewNativeComponent.js b/packages/react-native/src/private/specs/components/RCTModalHostViewNativeComponent.js index 86bf895d767526..58ec2940079bc8 100644 --- a/packages/react-native/src/private/specs/components/RCTModalHostViewNativeComponent.js +++ b/packages/react-native/src/private/specs/components/RCTModalHostViewNativeComponent.js @@ -58,6 +58,14 @@ type NativeProps = $ReadOnly<{| */ statusBarTranslucent?: WithDefault, + /** + * The `navigationBarTranslucent` prop determines whether your modal should go under + * the system navigationbar. + * + * See https://reactnative.dev/docs/modal#navigationBarTranslucent + */ + navigationBarTranslucent?: WithDefault, + /** * The `hardwareAccelerated` prop controls whether to force hardware * acceleration for the underlying window. diff --git a/packages/react-native/src/private/specs/modules/NativeAccessibilityInfo.js b/packages/react-native/src/private/specs/modules/NativeAccessibilityInfo.js index 0524dfa3682bb8..358134c9ed1bdb 100644 --- a/packages/react-native/src/private/specs/modules/NativeAccessibilityInfo.js +++ b/packages/react-native/src/private/specs/modules/NativeAccessibilityInfo.js @@ -16,6 +16,12 @@ export interface Spec extends TurboModule { +isReduceMotionEnabled: ( onSuccess: (isReduceMotionEnabled: boolean) => void, ) => void; + +isInvertColorsEnabled?: ( + onSuccess: (isInvertColorsEnabled: boolean) => void, + ) => void; + +isHighTextContrastEnabled?: ( + onSuccess: (isHighTextContrastEnabled: boolean) => void, + ) => void; +isTouchExplorationEnabled: ( onSuccess: (isScreenReaderEnabled: boolean) => void, ) => void; @@ -28,6 +34,9 @@ export interface Spec extends TurboModule { mSec: number, onSuccess: (recommendedTimeoutMillis: number) => void, ) => void; + +isGrayscaleEnabled?: ( + onSuccess: (isGrayscaleEnabled: boolean) => void, + ) => void; } export default (TurboModuleRegistry.get('AccessibilityInfo'): ?Spec); diff --git a/packages/react-native/src/private/specs/modules/NativeAccessibilityManager.js b/packages/react-native/src/private/specs/modules/NativeAccessibilityManager.js index 594998633aa030..d5f617bc5b590a 100644 --- a/packages/react-native/src/private/specs/modules/NativeAccessibilityManager.js +++ b/packages/react-native/src/private/specs/modules/NativeAccessibilityManager.js @@ -35,6 +35,10 @@ export interface Spec extends TurboModule { onSuccess: (isReduceMotionEnabled: boolean) => void, onError: (error: Object) => void, ) => void; + +getCurrentDarkerSystemColorsState?: ( + onSuccess: (isDarkerSystemColorsEnabled: boolean) => void, + onError: (error: Object) => void, + ) => void; +getCurrentPrefersCrossFadeTransitionsState?: ( onSuccess: (prefersCrossFadeTransitions: boolean) => void, onError: (error: Object) => void, @@ -47,7 +51,7 @@ export interface Spec extends TurboModule { onSuccess: (isScreenReaderEnabled: boolean) => void, onError: (error: Object) => void, ) => void; - +setAccessibilityContentSizeMultipliers: (JSMultipliers: {| + +setAccessibilityContentSizeMultipliers: (JSMultipliers: { +extraSmall?: ?number, +small?: ?number, +medium?: ?number, @@ -60,7 +64,7 @@ export interface Spec extends TurboModule { +accessibilityExtraLarge?: ?number, +accessibilityExtraExtraLarge?: ?number, +accessibilityExtraExtraExtraLarge?: ?number, - |}) => void; + }) => void; +setAccessibilityFocus: (reactTag: number) => void; +announceForAccessibility: (announcement: string) => void; +announceForAccessibilityWithOptions?: ( diff --git a/packages/react-native/src/private/specs/modules/NativeActionSheetManager.js b/packages/react-native/src/private/specs/modules/NativeActionSheetManager.js index 37407bd01cf61d..2364a83c4faf49 100644 --- a/packages/react-native/src/private/specs/modules/NativeActionSheetManager.js +++ b/packages/react-native/src/private/specs/modules/NativeActionSheetManager.js @@ -24,6 +24,7 @@ export interface Spec extends TurboModule { +anchor?: ?number, +tintColor?: ?number, +cancelButtonTintColor?: ?number, + +disabledButtonTintColor?: ?number, +userInterfaceStyle?: ?string, +disabledButtonIndices?: Array, |}, @@ -37,6 +38,7 @@ export interface Spec extends TurboModule { +anchor?: ?number, +tintColor?: ?number, +cancelButtonTintColor?: ?number, + +disabledButtonTintColor?: ?number, +excludedActivityTypes?: ?Array, +userInterfaceStyle?: ?string, |}, diff --git a/packages/react-native/src/private/specs/modules/NativeAlertManager.js b/packages/react-native/src/private/specs/modules/NativeAlertManager.js index 03000c8f78e0fa..92b7f927e5b1ef 100644 --- a/packages/react-native/src/private/specs/modules/NativeAlertManager.js +++ b/packages/react-native/src/private/specs/modules/NativeAlertManager.js @@ -12,7 +12,7 @@ import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry'; -export type Args = {| +export type Args = { title?: string, message?: string, buttons?: Array, // TODO(T67565166): have a better type @@ -28,7 +28,7 @@ export type Args = {| modal?: ?boolean, critical?: ?boolean, // macOS] -|}; +}; export interface Spec extends TurboModule { +alertWithArgs: ( diff --git a/packages/react-native/src/private/specs/modules/NativeAppearance.js b/packages/react-native/src/private/specs/modules/NativeAppearance.js index c12236e7a53c8e..d7bc36160eaf81 100644 --- a/packages/react-native/src/private/specs/modules/NativeAppearance.js +++ b/packages/react-native/src/private/specs/modules/NativeAppearance.js @@ -12,21 +12,15 @@ import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry'; -export type ColorSchemeName = 'light' | 'dark'; +export type ColorSchemeName = 'light' | 'dark' | 'unspecified'; export type AppearancePreferences = { - // TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union - // types. - /* 'light' | 'dark' */ - colorScheme?: ?string, + colorScheme?: ?ColorSchemeName, }; export interface Spec extends TurboModule { - // TODO: (hramos) T52919652 Use ?ColorSchemeName once codegen supports union - // types. - /* 'light' | 'dark' */ - +getColorScheme: () => ?string; - +setColorScheme: (colorScheme: string) => void; + +getColorScheme: () => ?ColorSchemeName; + +setColorScheme: (colorScheme: ColorSchemeName) => void; // RCTEventEmitter +addListener: (eventName: string) => void; diff --git a/packages/react-native/src/private/specs/modules/NativeExceptionsManager.js b/packages/react-native/src/private/specs/modules/NativeExceptionsManager.js index 5e54f53dd3670b..399e8f6d4ea68e 100644 --- a/packages/react-native/src/private/specs/modules/NativeExceptionsManager.js +++ b/packages/react-native/src/private/specs/modules/NativeExceptionsManager.js @@ -12,15 +12,15 @@ import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry'; -const Platform = require('../../../../Libraries/Utilities/Platform'); +const Platform = require('../../../../Libraries/Utilities/Platform').default; -export type StackFrame = {| +export type StackFrame = { column: ?number, file: ?string, lineNumber: ?number, methodName: string, collapse?: boolean, -|}; +}; export type ExceptionData = { message: string, originalMessage: ?string, @@ -47,11 +47,6 @@ export interface Spec extends TurboModule { exceptionId: number, ) => void; +reportException?: (data: ExceptionData) => void; - +updateExceptionMessage: ( - message: string, - stack: Array, - exceptionId: number, - ) => void; // TODO(T53311281): This is a noop on iOS now. Implement it. +dismissRedbox?: () => void; } @@ -74,13 +69,6 @@ const ExceptionsManager = { ) { NativeModule.reportSoftException(message, stack, exceptionId); }, - updateExceptionMessage( - message: string, - stack: Array, - exceptionId: number, - ) { - NativeModule.updateExceptionMessage(message, stack, exceptionId); - }, dismissRedbox(): void { if ( Platform.OS !== 'ios' && diff --git a/packages/react-native/src/private/webapis/dom/geometry/DOMRect.js b/packages/react-native/src/private/webapis/dom/geometry/DOMRect.js index 07af232a399891..650a65cd243d44 100644 --- a/packages/react-native/src/private/webapis/dom/geometry/DOMRect.js +++ b/packages/react-native/src/private/webapis/dom/geometry/DOMRect.js @@ -14,7 +14,7 @@ * licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/). */ -import DOMRectReadOnly, {type DOMRectLike} from './DOMRectReadOnly'; +import DOMRectReadOnly, {type DOMRectInit} from './DOMRectReadOnly'; // flowlint unsafe-getters-setters:off @@ -72,7 +72,7 @@ export default class DOMRect extends DOMRectReadOnly { /** * Creates a new `DOMRect` object with a given location and dimensions. */ - static fromRect(rect?: ?DOMRectLike): DOMRect { + static fromRect(rect?: ?DOMRectInit): DOMRect { if (!rect) { return new DOMRect(); } diff --git a/packages/react-native/src/private/webapis/dom/geometry/DOMRectReadOnly.js b/packages/react-native/src/private/webapis/dom/geometry/DOMRectReadOnly.js index 1c2d4a870778ce..0b35c46e238aff 100644 --- a/packages/react-native/src/private/webapis/dom/geometry/DOMRectReadOnly.js +++ b/packages/react-native/src/private/webapis/dom/geometry/DOMRectReadOnly.js @@ -16,7 +16,7 @@ // flowlint sketchy-null:off, unsafe-getters-setters:off -export interface DOMRectLike { +export interface DOMRectInit { x?: ?number; y?: ?number; width?: ?number; @@ -146,7 +146,7 @@ export default class DOMRectReadOnly { /** * Creates a new `DOMRectReadOnly` object with a given location and dimensions. */ - static fromRect(rect?: ?DOMRectLike): DOMRectReadOnly { + static fromRect(rect?: ?DOMRectInit): DOMRectReadOnly { if (!rect) { return new DOMRectReadOnly(); } diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js index f091f3555b7be5..e7850e8bad733b 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js @@ -11,7 +11,7 @@ // flowlint unsafe-getters-setters:off import type { - HostComponent, + HostInstance, INativeMethods, InternalInstanceHandle, MeasureInWindowOnSuccessCallback, @@ -19,14 +19,13 @@ import type { MeasureOnSuccessCallback, ViewConfig, } from '../../../../../Libraries/Renderer/shims/ReactNativeTypes'; -import type {ElementRef} from 'react'; import TextInputState from '../../../../../Libraries/Components/TextInput/TextInputState'; import {getFabricUIManager} from '../../../../../Libraries/ReactNative/FabricUIManager'; import {create as createAttributePayload} from '../../../../../Libraries/ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload'; import warnForStyleProps from '../../../../../Libraries/ReactNative/ReactFabricPublicInstance/warnForStyleProps'; import ReadOnlyElement, {getBoundingClientRect} from './ReadOnlyElement'; -import ReadOnlyNode from './ReadOnlyNode'; +import ReadOnlyNode, {setInstanceHandle} from './ReadOnlyNode'; import { getPublicInstanceFromInternalInstanceHandle, getShadowNode, @@ -36,7 +35,26 @@ import nullthrows from 'nullthrows'; const noop = () => {}; -export default class ReactNativeElement +// Ideally, this class would be exported as-is, but this implementation is +// significantly slower than the existing `ReactFabricHostComponent`. +// This is a very hot code path (this class is instantiated once per rendered +// host component in the tree) and we can't regress performance here. +// +// This implementation is slower because this is a subclass and we have to call +// super(), which is a very slow operation the way that Babel transforms it at +// the moment. +// +// The optimization we're doing is using an old-style function constructor, +// where we're not required to use `super()`, and we make that constructor +// extend this class so it inherits all the methods and it sets the class +// hierarchy correctly. +// +// An alternative implementation was to implement the constructor as a function +// returning a manually constructed instance using `Object.create()` but that +// was slower than this method because the engine has to create an object than +// we then discard to create a new one. + +class ReactNativeElementMethods extends ReadOnlyElement implements INativeMethods { @@ -44,8 +62,10 @@ export default class ReactNativeElement __nativeTag: number; __internalInstanceHandle: InternalInstanceHandle; - #viewConfig: ViewConfig; + __viewConfig: ViewConfig; + // This constructor isn't really used. See the `ReactNativeElement` function + // below. constructor( tag: number, viewConfig: ViewConfig, @@ -55,7 +75,7 @@ export default class ReactNativeElement this.__nativeTag = tag; this.__internalInstanceHandle = internalInstanceHandle; - this.#viewConfig = viewConfig; + this.__viewConfig = viewConfig; } get offsetHeight(): number { @@ -143,7 +163,7 @@ export default class ReactNativeElement } measureLayout( - relativeToNativeNode: number | ElementRef>, + relativeToNativeNode: number | HostInstance, onSuccess: MeasureLayoutOnSuccessCallback, onFail?: () => void /* currently unused */, ) { @@ -172,12 +192,12 @@ export default class ReactNativeElement setNativeProps(nativeProps: {...}): void { if (__DEV__) { - warnForStyleProps(nativeProps, this.#viewConfig.validAttributes); + warnForStyleProps(nativeProps, this.__viewConfig.validAttributes); } const updatePayload = createAttributePayload( nativeProps, - this.#viewConfig.validAttributes, + this.__viewConfig.validAttributes, ); const node = getShadowNode(this); @@ -187,3 +207,24 @@ export default class ReactNativeElement } } } + +// Alternative constructor just implemented to provide a better performance than +// calling super() in the original class. +function ReactNativeElement( + this: ReactNativeElementMethods, + tag: number, + viewConfig: ViewConfig, + internalInstanceHandle: InternalInstanceHandle, +) { + this.__nativeTag = tag; + this.__internalInstanceHandle = internalInstanceHandle; + this.__viewConfig = viewConfig; + setInstanceHandle(this, internalInstanceHandle); +} + +ReactNativeElement.prototype = Object.create( + ReactNativeElementMethods.prototype, +); + +// $FlowExpectedError[prop-missing] +export default ReactNativeElement as typeof ReactNativeElementMethods; diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js index e368332a2526f8..39bb71ad00cee8 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js @@ -26,6 +26,8 @@ let ReadOnlyElementClass: Class; export default class ReadOnlyNode { constructor(internalInstanceHandle: InternalInstanceHandle) { + // This constructor is inlined in `ReactNativeElement` so if you modify + // this make sure that their implementation stays in sync. setInstanceHandle(this, internalInstanceHandle); } @@ -293,7 +295,7 @@ export function getInstanceHandle(node: ReadOnlyNode): InternalInstanceHandle { return node[INSTANCE_HANDLE_KEY]; } -function setInstanceHandle( +export function setInstanceHandle( node: ReadOnlyNode, instanceHandle: InternalInstanceHandle, ): void { diff --git a/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserver.js b/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserver.js index 1783adbc16742c..2bada91d3e793c 100644 --- a/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserver.js +++ b/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserver.js @@ -25,6 +25,18 @@ type IntersectionObserverInit = { // root?: ReactNativeElement, // This option exists on the Web but it's not currently supported in React Native. // rootMargin?: string, // This option exists on the Web but it's not currently supported in React Native. threshold?: number | $ReadOnlyArray, + + /** + * This is a React Native specific option (not spec compliant) that specifies + * ratio threshold(s) of the intersection area to the total `root` area. + * + * If set, it will either be a singular ratio value between 0-1 (inclusive) + * or an array of such ratios. + * + * Note: If `rnRootThreshold` is set, and `threshold` is not set, + * `threshold` will not default to [0] (as per spec) + */ + rnRootThreshold?: number | $ReadOnlyArray, }; /** @@ -44,13 +56,16 @@ type IntersectionObserverInit = { * elements with the same observer. * * This implementation only supports the `threshold` option at the moment - * (`root` and `rootMargin` are not supported). + * (`root` and `rootMargin` are not supported) and provides a React Native specific + * option `rnRootThreshold`. + * */ export default class IntersectionObserver { _callback: IntersectionObserverCallback; _thresholds: $ReadOnlyArray; _observationTargets: Set = new Set(); _intersectionObserverId: ?IntersectionObserverId; + _rootThresholds: $ReadOnlyArray | null; constructor( callback: IntersectionObserverCallback, @@ -83,7 +98,12 @@ export default class IntersectionObserver { } this._callback = callback; - this._thresholds = normalizeThresholds(options?.threshold); + + this._rootThresholds = normalizeRootThreshold(options?.rnRootThreshold); + this._thresholds = normalizeThreshold( + options?.threshold, + this._rootThresholds != null, // only provide default if no rootThreshold + ); } /** @@ -115,14 +135,27 @@ export default class IntersectionObserver { * A list of thresholds, sorted in increasing numeric order, where each * threshold is a ratio of intersection area to bounding box area of an * observed target. - * Notifications for a target are generated when any of the thresholds are - * crossed for that target. - * If no value was passed to the constructor, `0` is used. + * Notifications for a target are generated when any of the thresholds specified + * in `rnRootThreshold` or `threshold` are crossed for that target. + * + * If no value was passed to the constructor, and no `rnRootThreshold` + * is set, `0` is used. */ get thresholds(): $ReadOnlyArray { return this._thresholds; } + /** + * A list of root thresholds, sorted in increasing numeric order, where each + * threshold is a ratio of intersection area to bounding box area of the specified + * root view, which defaults to the viewport. + * Notifications for a target are generated when any of the thresholds specified + * in `rnRootThreshold` or `threshold` are crossed for that target. + */ + get rnRootThresholds(): $ReadOnlyArray | null { + return this._rootThresholds; + } + /** * Adds an element to the set of target elements being watched by the * `IntersectionObserver`. @@ -131,6 +164,12 @@ export default class IntersectionObserver { * To stop observing the element, call `IntersectionObserver.unobserve()`. */ observe(target: ReactNativeElement): void { + if (target == null) { + throw new TypeError( + "Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is null or undefined.", + ); + } + if (!(target instanceof ReactNativeElement)) { throw new TypeError( "Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is not of type 'ReactNativeElement'.", @@ -215,32 +254,84 @@ export default class IntersectionObserver { * Converts the user defined `threshold` value into an array of sorted valid * threshold options for `IntersectionObserver` (double ∈ [0, 1]). * + * If `defaultEmpty` is true, then defaults to empty array, otherwise [0]. + * * @example * normalizeThresholds(0.5); // → [0.5] * normalizeThresholds([1, 0.5, 0]); // → [0, 0.5, 1] * normalizeThresholds(['1', '0.5', '0']); // → [0, 0.5, 1] + * normalizeThresholds(null); // → [0] + * normalizeThresholds([null, null]); // → [0, 0] + * + * normalizeThresholds([null], true); // → [0] + * normalizeThresholds(null, true); // → [] + * normalizeThresholds([], true); // → [] */ -function normalizeThresholds(threshold: mixed): $ReadOnlyArray { +function normalizeThreshold( + threshold: mixed, + defaultEmpty: boolean = false, +): $ReadOnlyArray { if (Array.isArray(threshold)) { if (threshold.length > 0) { - return threshold.map(normalizeThresholdValue).sort(); + return threshold + .map(t => normalizeThresholdValue(t, 'threshold')) + .map(t => t ?? 0) + .sort(); + } else if (defaultEmpty) { + return []; } else { return [0]; } } - return [normalizeThresholdValue(threshold)]; + const normalized = normalizeThresholdValue(threshold, 'threshold'); + if (normalized == null) { + return defaultEmpty ? [] : [0]; + } + + return [normalized]; +} + +/** + * Converts the user defined `rnRootThreshold` value into an array of sorted valid + * threshold options for `IntersectionObserver` (double ∈ [0, 1]). + * + * If invalid array or null, returns null. + * + * @example + * normalizeRootThreshold(0.5); // → [0.5] + * normalizeRootThresholds([1, 0.5, 0]); // → [0, 0.5, 1] + * normalizeRootThresholds([null, '0.5', '0']); // → [0, 0.5] + * normalizeRootThresholds(null); // → null + * normalizeRootThresholds([null, null]); // → null + */ +function normalizeRootThreshold( + rootThreshold: mixed, +): null | $ReadOnlyArray { + if (Array.isArray(rootThreshold)) { + const normalizedArr = rootThreshold + .map(rt => normalizeThresholdValue(rt, 'rnRootThreshold')) + .filter((rt): rt is number => rt != null) + .sort(); + return normalizedArr.length === 0 ? null : normalizedArr; + } + + const normalized = normalizeThresholdValue(rootThreshold, 'rnRootThreshold'); + return normalized == null ? null : [normalized]; } -function normalizeThresholdValue(threshold: mixed): number { +function normalizeThresholdValue( + threshold: mixed, + property: string, +): null | number { if (threshold == null) { - return 0; + return null; } const thresholdAsNumber = Number(threshold); if (!Number.isFinite(thresholdAsNumber)) { throw new TypeError( - "Failed to read the 'threshold' property from 'IntersectionObserverInit': The provided double value is non-finite.", + `Failed to read the '${property}' property from 'IntersectionObserverInit': The provided double value is non-finite.`, ); } diff --git a/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverEntry.js b/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverEntry.js index 9de2dcf4044eef..ea55c66be09273 100644 --- a/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverEntry.js +++ b/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverEntry.js @@ -74,6 +74,32 @@ export default class IntersectionObserverEntry { return Math.min(ratio, 1); } + /** + * Returns the ratio of the `intersectionRect` to the `boundingRootRect`. + */ + get rnRootIntersectionRatio(): number { + const intersectionRect = this.intersectionRect; + + const rootRect = this._nativeEntry.rootRect; + const boundingRootRect = new DOMRectReadOnly( + rootRect[0], + rootRect[1], + rootRect[2], + rootRect[3], + ); + + if (boundingRootRect.width === 0 || boundingRootRect.height === 0) { + return 0; + } + + const ratio = + (intersectionRect.width * intersectionRect.height) / + (boundingRootRect.width * boundingRootRect.height); + + // Prevent rounding errors from making this value greater than 1. + return Math.min(ratio, 1); + } + /** * Returns a `DOMRectReadOnly` representing the target's visible area. */ diff --git a/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverManager.js b/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverManager.js index d2d0067323c79a..3a6e73d355eb79 100644 --- a/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverManager.js +++ b/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverManager.js @@ -162,6 +162,7 @@ export function observe({ intersectionObserverId, targetShadowNode, thresholds: registeredObserver.observer.thresholds, + rootThresholds: registeredObserver.observer.rnRootThresholds, }); return true; diff --git a/packages/react-native/src/private/webapis/intersectionobserver/specs/NativeIntersectionObserver.js b/packages/react-native/src/private/webapis/intersectionobserver/specs/NativeIntersectionObserver.js index 2286214561cf93..e2d9a32efde752 100644 --- a/packages/react-native/src/private/webapis/intersectionobserver/specs/NativeIntersectionObserver.js +++ b/packages/react-native/src/private/webapis/intersectionobserver/specs/NativeIntersectionObserver.js @@ -17,6 +17,7 @@ export type NativeIntersectionObserverEntry = { targetInstanceHandle: mixed, targetRect: $ReadOnlyArray, // It's actually a tuple with x, y, width and height rootRect: $ReadOnlyArray, // It's actually a tuple with x, y, width and height + // TODO(T209328432) - Remove optionality of intersectionRect when native changes are released intersectionRect: ?$ReadOnlyArray, // It's actually a tuple with x, y, width and height isIntersectingAboveThresholds: boolean, time: number, @@ -26,6 +27,7 @@ export type NativeIntersectionObserverObserveOptions = { intersectionObserverId: number, targetShadowNode: mixed, thresholds: $ReadOnlyArray, + rootThresholds?: ?$ReadOnlyArray, }; export interface Spec extends TurboModule { diff --git a/packages/react-native/src/private/webapis/performance/EventTiming.js b/packages/react-native/src/private/webapis/performance/EventTiming.js index 110689a0601187..f57e50b65d1ce0 100644 --- a/packages/react-native/src/private/webapis/performance/EventTiming.js +++ b/packages/react-native/src/private/webapis/performance/EventTiming.js @@ -16,8 +16,8 @@ import type { } from './PerformanceEntry'; import {PerformanceEntry} from './PerformanceEntry'; -import {warnNoNativePerformanceObserver} from './PerformanceObserver'; -import NativePerformanceObserver from './specs/NativePerformanceObserver'; +import NativePerformance from './specs/NativePerformance'; +import {warnNoNativePerformance} from './Utilities'; export type PerformanceEventTimingJSON = { ...PerformanceEntryJSON, @@ -85,14 +85,18 @@ function getCachedEventCounts(): Map { if (cachedEventCounts) { return cachedEventCounts; } - if (!NativePerformanceObserver) { - warnNoNativePerformanceObserver(); - return new Map(); + + if (!NativePerformance || !NativePerformance?.getEventCounts) { + warnNoNativePerformance(); + cachedEventCounts = new Map(); + return cachedEventCounts; } - cachedEventCounts = new Map( - NativePerformanceObserver.getEventCounts(), + const eventCounts = new Map( + NativePerformance.getEventCounts?.() ?? [], ); + cachedEventCounts = eventCounts; + // $FlowFixMe[incompatible-call] global.queueMicrotask(() => { // To be consistent with the calls to the API from the same task, @@ -101,7 +105,8 @@ function getCachedEventCounts(): Map { // after the current task is guaranteed to have finished. cachedEventCounts = null; }); - return cachedEventCounts ?? new Map(); + + return eventCounts; } /** diff --git a/packages/react-native/src/private/webapis/performance/Performance.js b/packages/react-native/src/private/webapis/performance/Performance.js index 5861d3582803b8..c84b3964feb022 100644 --- a/packages/react-native/src/private/webapis/performance/Performance.js +++ b/packages/react-native/src/private/webapis/performance/Performance.js @@ -12,25 +12,21 @@ import type { DOMHighResTimeStamp, + PerformanceEntryList, PerformanceEntryType, } from './PerformanceEntry'; -import type {PerformanceEntryList} from './PerformanceObserver'; import type {DetailType, PerformanceMarkOptions} from './UserTiming'; -import warnOnce from '../../../../Libraries/Utilities/warnOnce'; import {EventCounts} from './EventTiming'; import MemoryInfo from './MemoryInfo'; -import {ALWAYS_LOGGED_ENTRY_TYPES} from './PerformanceEntry'; -import {warnNoNativePerformanceObserver} from './PerformanceObserver'; import { performanceEntryTypeToRaw, rawToPerformanceEntry, } from './RawPerformanceEntry'; -import {RawPerformanceEntryTypeValues} from './RawPerformanceEntry'; import ReactNativeStartupTiming from './ReactNativeStartupTiming'; import NativePerformance from './specs/NativePerformance'; -import NativePerformanceObserver from './specs/NativePerformanceObserver'; import {PerformanceMark, PerformanceMeasure} from './UserTiming'; +import {warnNoNativePerformance} from './Utilities'; declare var global: { // This value is defined directly via JSI, if available. @@ -40,24 +36,6 @@ declare var global: { const getCurrentTimeStamp: () => DOMHighResTimeStamp = NativePerformance?.now ?? global.nativePerformanceNow ?? (() => Date.now()); -// We want some of the performance entry types to be always logged, -// even if they are not currently observed - this is either to be able to -// retrieve them at any time via Performance.getEntries* or to refer by other entries -// (such as when measures may refer to marks, even if the latter are not observed) -if (NativePerformanceObserver?.setIsBuffered) { - NativePerformanceObserver?.setIsBuffered( - ALWAYS_LOGGED_ENTRY_TYPES.map(performanceEntryTypeToRaw), - true, - ); -} - -function warnNoNativePerformance() { - warnOnce( - 'missing-native-performance', - 'Missing native implementation of Performance', - ); -} - export type PerformanceMeasureOptions = { detail?: DetailType, start?: DOMHighResTimeStamp, @@ -65,6 +43,9 @@ export type PerformanceMeasureOptions = { end?: DOMHighResTimeStamp, }; +const ENTRY_TYPES_AVAILABLE_FROM_TIMELINE: $ReadOnlyArray = + ['mark', 'measure']; + /** * Partial implementation of the Performance interface for RN, * corresponding to the standard in @@ -128,27 +109,30 @@ export default class Performance { markName: string, markOptions?: PerformanceMarkOptions, ): PerformanceMark { - const mark = new PerformanceMark(markName, markOptions); - - if (NativePerformance?.mark) { - NativePerformance.mark(markName, mark.startTime); + let computedStartTime; + if (NativePerformance?.markWithResult) { + computedStartTime = NativePerformance.markWithResult( + markName, + markOptions?.startTime, + ); } else { warnNoNativePerformance(); + computedStartTime = performance.now(); } - return mark; + return new PerformanceMark(markName, { + startTime: computedStartTime, + detail: markOptions?.detail, + }); } clearMarks(markName?: string): void { - if (!NativePerformanceObserver?.clearEntries) { - warnNoNativePerformanceObserver(); + if (!NativePerformance?.clearMarks) { + warnNoNativePerformance(); return; } - NativePerformanceObserver?.clearEntries( - RawPerformanceEntryTypeValues.MARK, - markName, - ); + NativePerformance.clearMarks(markName); } measure( @@ -165,6 +149,7 @@ export default class Performance { if (typeof startMarkOrOptions === 'string') { startMarkName = startMarkOrOptions; + options = {}; } else if (startMarkOrOptions !== undefined) { options = startMarkOrOptions; if (endMark !== undefined) { @@ -202,40 +187,39 @@ export default class Performance { duration = options.duration ?? duration; } - const measure = new PerformanceMeasure(measureName, { - // FIXME(T196011255): this is incorrect, as we're only assigning the - // start/end if they're specified as a number, but not if they're - // specified as previous mark names. - startTime, - duration, - }); - - if (NativePerformance?.measure) { - NativePerformance.measure( - measureName, - startTime, - endTime, - duration, - startMarkName, - endMarkName, - ); + let computedStartTime = startTime; + let computedDuration = duration; + + if (NativePerformance?.measureWithResult) { + [computedStartTime, computedDuration] = + NativePerformance.measureWithResult( + measureName, + startTime, + endTime, + duration, + startMarkName, + endMarkName, + ); } else { warnNoNativePerformance(); } + const measure = new PerformanceMeasure(measureName, { + startTime: computedStartTime, + duration: computedDuration ?? 0, + detail: options?.detail, + }); + return measure; } clearMeasures(measureName?: string): void { - if (!NativePerformanceObserver?.clearEntries) { - warnNoNativePerformanceObserver(); + if (!NativePerformance?.clearMeasures) { + warnNoNativePerformance(); return; } - NativePerformanceObserver?.clearEntries( - RawPerformanceEntryTypeValues.MEASURE, - measureName, - ); + NativePerformance?.clearMeasures(measureName); } /** @@ -252,28 +236,28 @@ export default class Performance { * https://www.w3.org/TR/performance-timeline/#extensions-to-the-performance-interface */ getEntries(): PerformanceEntryList { - if (!NativePerformanceObserver?.getEntries) { - warnNoNativePerformanceObserver(); + if (!NativePerformance?.getEntries) { + warnNoNativePerformance(); return []; } - return NativePerformanceObserver.getEntries().map(rawToPerformanceEntry); + return NativePerformance.getEntries().map(rawToPerformanceEntry); } getEntriesByType(entryType: PerformanceEntryType): PerformanceEntryList { - if (!ALWAYS_LOGGED_ENTRY_TYPES.includes(entryType)) { - console.warn( - `Performance.getEntriesByType: Only valid for ${JSON.stringify( - ALWAYS_LOGGED_ENTRY_TYPES, - )} entry types, got ${entryType}`, - ); + if ( + entryType != null && + !ENTRY_TYPES_AVAILABLE_FROM_TIMELINE.includes(entryType) + ) { + console.warn('Deprecated API for given entry type.'); return []; } - if (!NativePerformanceObserver?.getEntries) { - warnNoNativePerformanceObserver(); + if (!NativePerformance?.getEntriesByType) { + warnNoNativePerformance(); return []; } - return NativePerformanceObserver.getEntries( + + return NativePerformance.getEntriesByType( performanceEntryTypeToRaw(entryType), ).map(rawToPerformanceEntry); } @@ -283,24 +267,21 @@ export default class Performance { entryType?: PerformanceEntryType, ): PerformanceEntryList { if ( - entryType !== undefined && - !ALWAYS_LOGGED_ENTRY_TYPES.includes(entryType) + entryType != null && + !ENTRY_TYPES_AVAILABLE_FROM_TIMELINE.includes(entryType) ) { - console.warn( - `Performance.getEntriesByName: Only valid for ${JSON.stringify( - ALWAYS_LOGGED_ENTRY_TYPES, - )} entry types, got ${entryType}`, - ); + console.warn('Deprecated API for given entry type.'); return []; } - if (!NativePerformanceObserver?.getEntries) { - warnNoNativePerformanceObserver(); + if (!NativePerformance?.getEntriesByName) { + warnNoNativePerformance(); return []; } - return NativePerformanceObserver.getEntries( - entryType != null ? performanceEntryTypeToRaw(entryType) : undefined, + + return NativePerformance.getEntriesByName( entryName, + entryType != null ? performanceEntryTypeToRaw(entryType) : undefined, ).map(rawToPerformanceEntry); } } diff --git a/packages/react-native/src/private/webapis/performance/PerformanceEntry.js b/packages/react-native/src/private/webapis/performance/PerformanceEntry.js index 4eca7229847782..5e5cd830d018b3 100644 --- a/packages/react-native/src/private/webapis/performance/PerformanceEntry.js +++ b/packages/react-native/src/private/webapis/performance/PerformanceEntry.js @@ -21,11 +21,6 @@ export type PerformanceEntryJSON = { ... }; -export const ALWAYS_LOGGED_ENTRY_TYPES: $ReadOnlyArray = [ - 'mark', - 'measure', -]; - export class PerformanceEntry { #name: string; #entryType: PerformanceEntryType; @@ -69,3 +64,5 @@ export class PerformanceEntry { }; } } + +export type PerformanceEntryList = $ReadOnlyArray; diff --git a/packages/react-native/src/private/webapis/performance/PerformanceObserver.js b/packages/react-native/src/private/webapis/performance/PerformanceObserver.js index 4f1d90f080ffa0..adff3bf671ce0e 100644 --- a/packages/react-native/src/private/webapis/performance/PerformanceObserver.js +++ b/packages/react-native/src/private/webapis/performance/PerformanceObserver.js @@ -10,20 +10,19 @@ import type { DOMHighResTimeStamp, + PerformanceEntryList, PerformanceEntryType, } from './PerformanceEntry'; +import type {OpaqueNativeObserverHandle} from './specs/NativePerformance'; -import warnOnce from '../../../../Libraries/Utilities/warnOnce'; import {PerformanceEventTiming} from './EventTiming'; -import {PerformanceEntry} from './PerformanceEntry'; import { performanceEntryTypeToRaw, rawToPerformanceEntry, rawToPerformanceEntryType, } from './RawPerformanceEntry'; -import NativePerformanceObserver from './specs/NativePerformanceObserver'; - -export type PerformanceEntryList = $ReadOnlyArray; +import NativePerformance from './specs/NativePerformance'; +import {warnNoNativePerformance} from './Utilities'; export {PerformanceEntry} from './PerformanceEntry'; @@ -56,99 +55,34 @@ export class PerformanceObserverEntryList { } } +export type PerformanceObserverCallbackOptions = { + droppedEntriesCount: number, +}; + export type PerformanceObserverCallback = ( list: PerformanceObserverEntryList, observer: PerformanceObserver, // The number of buffered entries which got dropped from the buffer due to the buffer being full: - droppedEntryCount?: number, + options?: PerformanceObserverCallbackOptions, ) => void; -export type PerformanceObserverInit = - | { - entryTypes: Array, - } - | { - type: PerformanceEntryType, - durationThreshold?: DOMHighResTimeStamp, - }; - -type PerformanceObserverConfig = {| - callback: PerformanceObserverCallback, - entryTypes: $ReadOnlySet, - durationThreshold: ?number, -|}; - -const observerCountPerEntryType: Map = new Map(); -const registeredObservers: Map = - new Map(); -let isOnPerformanceEntryCallbackSet: boolean = false; - -// This is a callback that gets scheduled and periodically called from the native side -const onPerformanceEntry = () => { - if (!NativePerformanceObserver) { - return; - } - const entryResult = NativePerformanceObserver.popPendingEntries(); - const rawEntries = entryResult?.entries ?? []; - const droppedEntriesCount = entryResult?.droppedEntriesCount; - if (rawEntries.length === 0) { - return; - } - const entries = rawEntries.map(rawToPerformanceEntry); - for (const [observer, observerConfig] of registeredObservers.entries()) { - const entriesForObserver: PerformanceEntryList = entries.filter(entry => { - if (!observerConfig.entryTypes.has(entry.entryType)) { - return false; - } - - if ( - entry.entryType === 'event' && - observerConfig.durationThreshold != null - ) { - return entry.duration >= observerConfig.durationThreshold; - } - - return true; - }); - if (entriesForObserver.length !== 0) { - try { - observerConfig.callback( - new PerformanceObserverEntryList(entriesForObserver), - observer, - droppedEntriesCount, - ); - } catch (error) { - console.error(error); - } - } - } +export type PerformanceObserverInit = { + entryTypes?: Array, + type?: PerformanceEntryType, + buffered?: boolean, + durationThreshold?: DOMHighResTimeStamp, }; -export function warnNoNativePerformanceObserver() { - warnOnce( - 'missing-native-performance-observer', - 'Missing native implementation of PerformanceObserver', - ); -} - -function applyDurationThresholds() { - const durationThresholds = Array.from(registeredObservers.values()) - .map(observerConfig => observerConfig.durationThreshold) - .filter(Boolean); - - return Math.min(...durationThresholds); -} - function getSupportedPerformanceEntryTypes(): $ReadOnlyArray { - if (!NativePerformanceObserver) { + if (!NativePerformance) { return Object.freeze([]); } - if (!NativePerformanceObserver.getSupportedPerformanceEntryTypes) { + if (!NativePerformance.getSupportedPerformanceEntryTypes) { // fallback if getSupportedPerformanceEntryTypes is not defined on native side return Object.freeze(['mark', 'measure', 'event']); } return Object.freeze( - NativePerformanceObserver.getSupportedPerformanceEntryTypes().map( + NativePerformance.getSupportedPerformanceEntryTypes().map( rawToPerformanceEntryType, ), ); @@ -175,111 +109,86 @@ function getSupportedPerformanceEntryTypes(): $ReadOnlyArray { + const rawEntries = NativePerformance.takeRecords?.( + this.#nativeObserverHandle, + true, // sort records + ); + if (!rawEntries) { + return; } - } - // Disconnect all observers if this was the last one - registeredObservers.delete(this); - if (registeredObservers.size === 0) { - NativePerformanceObserver.setOnPerformanceEntryCallback(undefined); - isOnPerformanceEntryCallbackSet = false; - } + const entries = rawEntries.map(rawToPerformanceEntry); + const entryList = new PerformanceObserverEntryList(entries); + + let droppedEntriesCount = 0; + if (!this.#calledAtLeastOnce) { + droppedEntriesCount = + NativePerformance.getDroppedEntriesCount?.( + this.#nativeObserverHandle, + ) ?? 0; + this.#calledAtLeastOnce = true; + } - applyDurationThresholds(); + this.#callback(entryList, this, {droppedEntriesCount}); + }); } #validateObserveOptions(options: PerformanceObserverInit): void { @@ -309,7 +218,7 @@ export class PerformanceObserver { ); } - if (entryTypes && durationThreshold !== undefined) { + if (entryTypes && durationThreshold != null) { throw new TypeError( "Failed to execute 'observe' on 'PerformanceObserver': An observe() call must not include both entryTypes and durationThreshold arguments.", ); @@ -320,12 +229,4 @@ export class PerformanceObserver { getSupportedPerformanceEntryTypes(); } -function union(a: $ReadOnlySet, b: $ReadOnlySet): Set { - return new Set([...a, ...b]); -} - -function difference(a: $ReadOnlySet, b: $ReadOnlySet): Set { - return new Set([...a].filter(x => !b.has(x))); -} - export {PerformanceEventTiming}; diff --git a/packages/react-native/src/private/webapis/performance/RawPerformanceEntry.js b/packages/react-native/src/private/webapis/performance/RawPerformanceEntry.js index 3a4e540e275fb6..7fe59110fe6770 100644 --- a/packages/react-native/src/private/webapis/performance/RawPerformanceEntry.js +++ b/packages/react-native/src/private/webapis/performance/RawPerformanceEntry.js @@ -12,7 +12,7 @@ import type {PerformanceEntryType} from './PerformanceEntry'; import type { RawPerformanceEntry, RawPerformanceEntryType, -} from './specs/NativePerformanceObserver'; +} from './specs/NativePerformance'; import {PerformanceEventTiming} from './EventTiming'; import {PerformanceLongTaskTiming} from './LongTasks'; diff --git a/packages/react-native/src/private/webapis/performance/UserTiming.js b/packages/react-native/src/private/webapis/performance/UserTiming.js index f3d7865a5cdce6..55ff802ea16476 100644 --- a/packages/react-native/src/private/webapis/performance/UserTiming.js +++ b/packages/react-native/src/private/webapis/performance/UserTiming.js @@ -25,12 +25,12 @@ export type TimeStampOrName = DOMHighResTimeStamp | string; export type PerformanceMeasureInit = { detail?: DetailType, - startTime?: DOMHighResTimeStamp, - duration?: DOMHighResTimeStamp, + startTime: DOMHighResTimeStamp, + duration: DOMHighResTimeStamp, }; export class PerformanceMark extends PerformanceEntry { - detail: DetailType; + #detail: DetailType; constructor(markName: string, markOptions?: PerformanceMarkOptions) { super({ @@ -41,20 +41,24 @@ export class PerformanceMark extends PerformanceEntry { }); if (markOptions) { - this.detail = markOptions.detail; + this.#detail = markOptions.detail; } } + + get detail(): DetailType { + return this.#detail; + } } export class PerformanceMeasure extends PerformanceEntry { #detail: DetailType; - constructor(measureName: string, measureOptions?: PerformanceMeasureInit) { + constructor(measureName: string, measureOptions: PerformanceMeasureInit) { super({ name: measureName, entryType: 'measure', - startTime: measureOptions?.startTime ?? 0, - duration: measureOptions?.duration ?? 0, + startTime: measureOptions.startTime, + duration: measureOptions.duration, }); if (measureOptions) { diff --git a/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js b/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js index b05f2e74e4e1e6..d1b81768f23184 100644 --- a/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js +++ b/packages/react-native/src/private/webapis/performance/specs/NativePerformance.js @@ -16,19 +16,77 @@ export type NativeMemoryInfo = {[key: string]: ?number}; export type ReactNativeStartupTiming = {[key: string]: ?number}; +export type RawPerformanceEntryType = number; + +export type RawPerformanceEntry = { + name: string, + entryType: RawPerformanceEntryType, + startTime: number, + duration: number, + + // For "event" entries only: + processingStart?: number, + processingEnd?: number, + interactionId?: number, +}; + +export type OpaqueNativeObserverHandle = mixed; + +export type NativeBatchedObserverCallback = () => void; +export type NativePerformanceMarkResult = number; +export type NativePerformanceMeasureResult = $ReadOnlyArray; // [startTime, duration] + +export type PerformanceObserverInit = { + entryTypes?: $ReadOnlyArray, + type?: number, + buffered?: boolean, + durationThreshold?: number, +}; + export interface Spec extends TurboModule { +now?: () => number; - +mark: (name: string, startTime: number) => void; - +measure: ( + +markWithResult?: ( + name: string, + startTime?: number, + ) => NativePerformanceMarkResult; + +measureWithResult?: ( name: string, startTime: number, endTime: number, duration?: number, startMark?: string, endMark?: string, - ) => void; + ) => NativePerformanceMeasureResult; + +clearMarks?: (entryName?: string) => void; + +clearMeasures?: (entryName?: string) => void; + +getEntries?: () => $ReadOnlyArray; + +getEntriesByName?: ( + entryName: string, + entryType?: ?RawPerformanceEntryType, + ) => $ReadOnlyArray; + +getEntriesByType?: ( + entryType: RawPerformanceEntryType, + ) => $ReadOnlyArray; + +getEventCounts?: () => $ReadOnlyArray<[string, number]>; +getSimpleMemoryInfo: () => NativeMemoryInfo; +getReactNativeStartupTiming: () => ReactNativeStartupTiming; + + +createObserver?: ( + callback: NativeBatchedObserverCallback, + ) => OpaqueNativeObserverHandle; + +getDroppedEntriesCount?: (observer: OpaqueNativeObserverHandle) => number; + + +observe?: ( + observer: OpaqueNativeObserverHandle, + options: PerformanceObserverInit, + ) => void; + +disconnect?: (observer: OpaqueNativeObserverHandle) => void; + +takeRecords?: ( + observer: OpaqueNativeObserverHandle, + sort: boolean, + ) => $ReadOnlyArray; + + +getSupportedPerformanceEntryTypes?: () => $ReadOnlyArray; } export default (TurboModuleRegistry.get('NativePerformanceCxx'): ?Spec); diff --git a/packages/rn-tester/IntegrationTests/LayoutEventsTest.js b/packages/rn-tester/IntegrationTests/LayoutEventsTest.js index 3d90f89ab81017..9d65625fa8637f 100644 --- a/packages/rn-tester/IntegrationTests/LayoutEventsTest.js +++ b/packages/rn-tester/IntegrationTests/LayoutEventsTest.js @@ -12,8 +12,8 @@ import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; import type { - Layout, - LayoutEvent, + LayoutRectangle, + LayoutChangeEvent, } from 'react-native/Libraries/Types/CoreEventTypes'; const React = require('react'); @@ -23,7 +23,7 @@ const deepDiffer = require('react-native/Libraries/Utilities/differ/deepDiffer') const {Image, LayoutAnimation, StyleSheet, Text, View} = ReactNative; const {Platform} = ReactNative; // [macOS] const {TestModule} = ReactNative.NativeModules; -function debug(...args: Array) { +function debug(...args: Array) { // console.log.apply(null, arguments); } @@ -32,9 +32,9 @@ type Props = $ReadOnly<{||}>; type State = { didAnimation: boolean, extraText?: string, - imageLayout?: Layout, - textLayout?: Layout, - viewLayout?: Layout, + imageLayout?: LayoutRectangle, + textLayout?: LayoutRectangle, + viewLayout?: LayoutRectangle, viewStyle?: ViewStyleProp, containerStyle?: ViewStyleProp, ... @@ -111,7 +111,11 @@ class LayoutEventsTest extends React.Component { }); }; - compare(node: string, measured: Layout, onLayout?: ?Layout): void { + compare( + node: string, + measured: LayoutRectangle, + onLayout?: ?LayoutRectangle, + ): void { if (deepDiffer(measured, onLayout)) { const data = {measured, onLayout}; throw new Error( @@ -122,19 +126,19 @@ class LayoutEventsTest extends React.Component { } } - onViewLayout: (e: LayoutEvent) => void = (e: LayoutEvent) => { + onViewLayout: (e: LayoutChangeEvent) => void = (e: LayoutChangeEvent) => { // $FlowFixMe[incompatible-call] debug('received view layout event\n', e.nativeEvent); this.setState({viewLayout: e.nativeEvent.layout}, this.checkLayout); }; - onTextLayout: (e: LayoutEvent) => void = (e: LayoutEvent) => { + onTextLayout: (e: LayoutChangeEvent) => void = (e: LayoutChangeEvent) => { // $FlowFixMe[incompatible-call] debug('received text layout event\n', e.nativeEvent); this.setState({textLayout: e.nativeEvent.layout}, this.checkLayout); }; - onImageLayout: (e: LayoutEvent) => void = (e: LayoutEvent) => { + onImageLayout: (e: LayoutChangeEvent) => void = (e: LayoutChangeEvent) => { // $FlowFixMe[incompatible-call] debug('received image layout event\n', e.nativeEvent); this.setState({imageLayout: e.nativeEvent.layout}, this.checkLayout); diff --git a/packages/rn-tester/js/RNTesterApp.ios.js b/packages/rn-tester/js/RNTesterApp.ios.js index 281ee25afd77a1..c48e3446d88644 100644 --- a/packages/rn-tester/js/RNTesterApp.ios.js +++ b/packages/rn-tester/js/RNTesterApp.ios.js @@ -12,7 +12,7 @@ import type {RNTesterModuleInfo} from './types/RNTesterTypes'; import type {Node} from 'react'; import RNTesterModuleContainer from './components/RNTesterModuleContainer'; -import SnapshotViewIOS from './examples/Snapshot/SnapshotViewIOS.ios'; +import SnapshotViewIOS from './examples/Snapshot/SnapshotViewIOS'; import RNTesterApp from './RNTesterAppShared'; import RNTesterList from './utils/RNTesterList'; import React from 'react'; diff --git a/packages/rn-tester/js/RNTesterApp.macos.js b/packages/rn-tester/js/RNTesterApp.macos.js index f8f9e4d05814b5..c48e3446d88644 100644 --- a/packages/rn-tester/js/RNTesterApp.macos.js +++ b/packages/rn-tester/js/RNTesterApp.macos.js @@ -1,5 +1,5 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * 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. @@ -8,9 +8,48 @@ * @flow */ -// [macOS] +import type {RNTesterModuleInfo} from './types/RNTesterTypes'; +import type {Node} from 'react'; -/* $FlowFixMe allow macOS to share iOS file */ -const RNTesterApp = require('./RNTesterApp.ios'); +import RNTesterModuleContainer from './components/RNTesterModuleContainer'; +import SnapshotViewIOS from './examples/Snapshot/SnapshotViewIOS'; +import RNTesterApp from './RNTesterAppShared'; +import RNTesterList from './utils/RNTesterList'; +import React from 'react'; +import {AppRegistry} from 'react-native'; + +AppRegistry.registerComponent('SetPropertiesExampleApp', () => + require('./examples/SetPropertiesExample/SetPropertiesExampleApp'), +); +AppRegistry.registerComponent('RootViewSizeFlexibilityExampleApp', () => + require('./examples/RootViewSizeFlexibilityExample/RootViewSizeFlexibilityExampleApp'), +); +AppRegistry.registerComponent('RNTesterApp', () => RNTesterApp); + +// Register suitable examples for snapshot tests +RNTesterList.Components.concat(RNTesterList.APIs).forEach( + (Example: RNTesterModuleInfo) => { + const ExampleModule = Example.module; + if (ExampleModule.displayName) { + class Snapshotter extends React.Component<{...}> { + render(): Node { + return ( + + {}} + /> + + ); + } + } + + AppRegistry.registerComponent( + ExampleModule.displayName, + () => Snapshotter, + ); + } + }, +); module.exports = RNTesterApp; diff --git a/packages/rn-tester/js/RNTesterAppShared.js b/packages/rn-tester/js/RNTesterAppShared.js index 336fc9d9839156..e41e6cd640d30f 100644 --- a/packages/rn-tester/js/RNTesterAppShared.js +++ b/packages/rn-tester/js/RNTesterAppShared.js @@ -8,13 +8,14 @@ * @flow */ -import type {RNTesterModuleInfo} from './types/RNTesterTypes'; +import type {RNTesterModuleInfo, ScreenTypes} from './types/RNTesterTypes'; import RNTesterModuleContainer from './components/RNTesterModuleContainer'; import RNTesterModuleList from './components/RNTesterModuleList'; import RNTesterNavBar, {navBarHeight} from './components/RNTesterNavbar'; import {RNTesterThemeContext, themes} from './components/RNTesterTheme'; import RNTTitleBar from './components/RNTTitleBar'; +import {title as PlaygroundTitle} from './examples/Playground/PlaygroundExample'; import RNTesterList from './utils/RNTesterList'; import { RNTesterNavigationActionsType, @@ -96,14 +97,11 @@ const RNTesterApp = ({ return false; }; - BackHandler.addEventListener('hardwareBackPress', handleHardwareBackPress); - - return () => { - BackHandler.removeEventListener( - 'hardwareBackPress', - handleHardwareBackPress, - ); - }; + const subscription = BackHandler.addEventListener( + 'hardwareBackPress', + handleHardwareBackPress, + ); + return () => subscription.remove(); }, [activeModuleKey, handleBackPress]); const handleModuleCardPress = React.useCallback( @@ -127,11 +125,22 @@ const RNTesterApp = ({ ); const handleNavBarPress = React.useCallback( - (args: {screen: string}) => { - dispatch({ - type: RNTesterNavigationActionsType.NAVBAR_PRESS, - data: {screen: args.screen}, - }); + (args: {screen: ScreenTypes}) => { + if (args.screen === 'playgrounds') { + dispatch({ + type: RNTesterNavigationActionsType.NAVBAR_OPEN_MODULE_PRESS, + data: { + key: 'PlaygroundExample', + title: PlaygroundTitle, + screen: args.screen, + }, + }); + } else { + dispatch({ + type: RNTesterNavigationActionsType.NAVBAR_PRESS, + data: {screen: args.screen}, + }); + } }, [dispatch], ); diff --git a/packages/rn-tester/js/components/ListExampleShared.js b/packages/rn-tester/js/components/ListExampleShared.js index 1991b54527e081..18aad335c604a1 100644 --- a/packages/rn-tester/js/components/ListExampleShared.js +++ b/packages/rn-tester/js/components/ListExampleShared.js @@ -10,8 +10,9 @@ 'use strict'; -const React = require('react'); -const { +import RNTesterText from './RNTesterText'; +import React from 'react'; +import { ActivityIndicator, Animated, Image, @@ -24,7 +25,7 @@ const { TextInput, TouchableHighlight, View, -} = require('react-native'); +} from 'react-native'; export type Item = { title: string, @@ -72,6 +73,7 @@ class ItemComponent extends React.PureComponent<{ onShowUnderlay?: () => void, onHideUnderlay?: () => void, textSelectable?: ?boolean, + testID?: ?string, isSelected?: ?boolean, // [macOS] ... }> { @@ -99,6 +101,7 @@ class ItemComponent extends React.PureComponent<{ ]}> {!item.noImage && } @@ -244,7 +247,7 @@ function getItemLayout( data: any, index: number, horizontal?: boolean, -): {|index: number, length: number, offset: number|} { +): {index: number, length: number, offset: number} { const [length, separator, header] = horizontal ? [HORIZ_WIDTH, 0, HEADER.width] : [ITEM_HEIGHT, SEPARATOR_HEIGHT, HEADER.height]; @@ -260,14 +263,16 @@ function renderSmallSwitchOption( label: string, value: boolean, setValue: boolean => void, + testID?: string, ): null | React.Node { if (Platform.isTV) { return null; } return ( - {label}: + {label}: -
- - - ); - } - const filter = ({example: e, filterRegex}: $FlowFixMe) => filterRegex.test(e.title); @@ -87,17 +75,26 @@ export default function RNTesterModuleContainer(props: Props): React.Node { }, ]; - return example != null ? ( + const singleModule = + example ?? (module.examples.length === 1 ? module.examples[0] : null); + + return singleModule != null ? ( <> - - - + {singleModule.scrollable === true ? ( + + + + ) : ( + + + + )} ) : ( <> diff --git a/packages/rn-tester/js/components/RNTesterModuleList.js b/packages/rn-tester/js/components/RNTesterModuleList.js index 34e4d5e769814d..8cc47961b2d5b4 100644 --- a/packages/rn-tester/js/components/RNTesterModuleList.js +++ b/packages/rn-tester/js/components/RNTesterModuleList.js @@ -61,7 +61,7 @@ const renderSectionHeader = ({section}: {section: any, ...}) => ( ); -const RNTesterModuleList: React$AbstractComponent = React.memo( +const RNTesterModuleList: React.ComponentType = React.memo( ({sections, handleModuleCardPress}) => { const filter = ({example, filterRegex, category}: any) => filterRegex.test(example.module.title) && diff --git a/packages/rn-tester/js/components/RNTesterNavbar.js b/packages/rn-tester/js/components/RNTesterNavbar.js index 2f0621e4a342c6..e67d7f55fd674f 100644 --- a/packages/rn-tester/js/components/RNTesterNavbar.js +++ b/packages/rn-tester/js/components/RNTesterNavbar.js @@ -8,12 +8,15 @@ * @flow strict-local */ +import type {ScreenTypes} from '../types/RNTesterTypes'; import type {RNTesterTheme} from './RNTesterTheme'; import {RNTesterThemeContext} from './RNTesterTheme'; import * as React from 'react'; import {Image, Pressable, StyleSheet, Text, View} from 'react-native'; +type NavBarOnPressHandler = ({screen: ScreenTypes}) => void; + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ const NavbarButton = ({ @@ -53,8 +56,8 @@ const ComponentTab = ({ isComponentActive, handleNavBarPress, theme, -}: $TEMPORARY$object<{ - handleNavBarPress: (data: {screen: string}) => void, +}: $ReadOnly<{ + handleNavBarPress: NavBarOnPressHandler, isComponentActive: boolean, theme: RNTesterTheme, }>) => ( @@ -70,12 +73,33 @@ const ComponentTab = ({ /> ); +const PlaygroundTab = ({ + isComponentActive, + handleNavBarPress, + theme, +}: $ReadOnly<{ + handleNavBarPress: NavBarOnPressHandler, + isComponentActive: boolean, + theme: RNTesterTheme, +}>) => ( + handleNavBarPress({screen: 'playgrounds'})} + activeImage={theme.NavBarPlaygroundActiveIcon} + inactiveImage={theme.NavBarPlaygroundInactiveIcon} + isActive={isComponentActive} + theme={theme} + iconStyle={styles.componentIcon} + /> +); + const APITab = ({ isAPIActive, handleNavBarPress, theme, -}: $TEMPORARY$object<{ - handleNavBarPress: (data: {screen: string}) => void, +}: $ReadOnly<{ + handleNavBarPress: NavBarOnPressHandler, isAPIActive: boolean, theme: RNTesterTheme, }>) => ( @@ -92,7 +116,7 @@ const APITab = ({ ); type Props = $ReadOnly<{| - handleNavBarPress: (data: {screen: string}) => void, + handleNavBarPress: NavBarOnPressHandler, screen: string, isExamplePageOpen: boolean, |}>; @@ -106,6 +130,7 @@ const RNTesterNavbar = ({ const isAPIActive = screen === 'apis' && !isExamplePageOpen; const isComponentActive = screen === 'components' && !isExamplePageOpen; + const isPlaygroundActive = screen === 'playgrounds'; return ( @@ -115,6 +140,11 @@ const RNTesterNavbar = ({ handleNavBarPress={handleNavBarPress} theme={theme} /> + { return ( - {label} + {label} ); diff --git a/packages/rn-tester/js/components/RNTesterTheme.js b/packages/rn-tester/js/components/RNTesterTheme.js index 6c7858e17c57d0..1421157e0ea1c1 100644 --- a/packages/rn-tester/js/components/RNTesterTheme.js +++ b/packages/rn-tester/js/components/RNTesterTheme.js @@ -50,6 +50,8 @@ export type RNTesterTheme = { NavBarComponentsInactiveIcon: ImageSource, NavBarAPIsActiveIcon: ImageSource, NavBarAPIsInactiveIcon: ImageSource, + NavBarPlaygroundActiveIcon: ImageSource, + NavBarPlaygroundInactiveIcon: ImageSource, ... }; @@ -90,6 +92,8 @@ export const RNTesterLightTheme = { NavBarComponentsInactiveIcon: require('./../assets/bottom-nav-components-icon-light.png'), NavBarAPIsActiveIcon: require('./../assets/bottom-nav-apis-icon-dark.png'), NavBarAPIsInactiveIcon: require('./../assets/bottom-nav-apis-icon-light.png'), + NavBarPlaygroundActiveIcon: require('./../assets/bottom-nav-playgrounds-icon-dark.png'), + NavBarPlaygroundInactiveIcon: require('./../assets/bottom-nav-playgrounds-icon-light.png'), }; export const RNTesterDarkTheme = { @@ -129,6 +133,8 @@ export const RNTesterDarkTheme = { NavBarComponentsInactiveIcon: require('./../assets/bottom-nav-components-icon-dark.png'), NavBarAPIsActiveIcon: require('./../assets/bottom-nav-apis-icon-light.png'), NavBarAPIsInactiveIcon: require('./../assets/bottom-nav-apis-icon-dark.png'), + NavBarPlaygroundActiveIcon: require('./../assets/bottom-nav-playgrounds-icon-dark.png'), + NavBarPlaygroundInactiveIcon: require('./../assets/bottom-nav-playgrounds-icon-light.png'), }; export const themes = {light: RNTesterLightTheme, dark: RNTesterDarkTheme}; diff --git a/packages/rn-tester/js/examples/ASAN/ASANCrashExample.js b/packages/rn-tester/js/examples/ASAN/ASANCrashExample.js index 0dcd696a7891d2..025a19023ea8d6 100644 --- a/packages/rn-tester/js/examples/ASAN/ASANCrashExample.js +++ b/packages/rn-tester/js/examples/ASAN/ASANCrashExample.js @@ -1,11 +1,11 @@ /** - * Copyright (c) Facebook, Inc. and its affiliates. + * 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. * - * @format * @flow strict-local + * @format */ import type {Node} from 'react'; diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.js index 08ea74daba6759..6b41f435790363 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.js @@ -12,15 +12,9 @@ import RNTesterBlock from '../../components/RNTesterBlock'; import RNTesterPage from '../../components/RNTesterPage'; -import { - Alert, - StyleSheet, - Text, - TouchableWithoutFeedback, - View, -} from 'react-native'; - -const React = require('react'); +import RNTesterText from '../../components/RNTesterText'; +import React from 'react'; +import {Alert, StyleSheet, TouchableWithoutFeedback, View} from 'react-native'; const importantForAccessibilityValues = [ 'auto', @@ -67,27 +61,27 @@ class AccessibilityAndroidExample extends React.Component< return ( - - + + Bacon {this.state.count} Ipsum{'\n'} - - Dolor sit amet{'\n'} - Eggsecetur{'\n'} - {'\n'} - + + Dolor sit amet{'\n'} + Eggsecetur{'\n'} + {'\n'} + http://github.com - - + + - Click me + Click me - Clicked {this.state.count} times + Clicked {this.state.count} times @@ -102,7 +96,7 @@ class AccessibilityAndroidExample extends React.Component< ] }> - Hello + Hello - world + world - + Change importantForAccessibility for background layout. - + - Background layout importantForAccessibility - + + Background layout importantForAccessibility + + { importantForAccessibilityValues[ this.state.backgroundImportantForAcc ] } - + - + Change importantForAccessibility for forground layout. - + - Forground layout importantForAccessibility - + + Forground layout importantForAccessibility + + { importantForAccessibilityValues[ this.state.forgroundImportantForAcc ] } - + - + In the following example, the words "test", "inline links", "another link", and "link that spans multiple lines because the text is so long", should each be independently focusable elements, announced as their content followed by ", Link". - - + + They should be focused in order from top to bottom *after* the contents of the entire paragraph. - - + + Focusing on the paragraph itself should also announce that there are "links available", and opening Talkback's links menu should show these same links. - - + + Clicking on each link, or selecting the link From Talkback's links menu should trigger an alert. - - + + The links that wraps to multiple lines will intentionally only draw a focus outline around the first line, but using the "explore by touch" tap-and-drag gesture should move focus to this link even if the second line is touched. - - + + Using the "Explore by touch" gesture and touching an area that is *not* a link should move focus to the entire paragraph. - - Example - + + Example + This is a{' '} - { Alert.alert('pressed test'); }}> test - {' '} + {' '} of{' '} - { Alert.alert('pressed Inline Links'); }}> inline links - {' '} + {' '} in React Native. Here's{' '} - { Alert.alert('pressed another link'); }}> another link - + . Here is a{' '} - { Alert.alert('pressed long link'); }}> link that spans multiple lines because the text is so long. - + This sentence has no links in it. - + ); @@ -241,6 +239,9 @@ class AccessibilityAndroidExample extends React.Component< } const styles = StyleSheet.create({ + buttonText: { + color: 'black', + }, touchableContainer: { position: 'absolute', left: 10, diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index d14e944a0e451f..306e066ce833dc 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -10,18 +10,16 @@ 'use strict'; -import type {PressEvent} from 'react-native/Libraries/Types/CoreEventTypes'; +import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; import type {EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter'; -import {RNTesterThemeContext} from '../../components/RNTesterTheme'; - -const RNTesterBlock = require('../../components/RNTesterBlock'); -const checkImageSource = require('./check.png'); -const mixedCheckboxImageSource = require('./mixed.png'); -const uncheckImageSource = require('./uncheck.png'); -const React = require('react'); -const {createRef} = require('react'); -const { +import RNTesterBlock from '../../components/RNTesterBlock'; +import RNTesterText from '../../components/RNTesterText'; +import checkImageSource from './check.png'; +import mixedCheckboxImageSource from './mixed.png'; +import uncheckImageSource from './uncheck.png'; +import React, {createRef} from 'react'; +import { AccessibilityInfo, Alert, Button, @@ -31,13 +29,12 @@ const { ScrollView, StyleSheet, Switch, - Text, TextInput, TouchableNativeFeedback, TouchableOpacity, TouchableWithoutFeedback, View, -} = require('react-native'); +} from 'react-native'; const styles = StyleSheet.create({ sectionContainer: { @@ -106,214 +103,208 @@ const styles = StyleSheet.create({ class AccessibilityExample extends React.Component<{}> { render(): React.Node { return ( - - {theme => ( - - - - Text's accessibilityLabel is the raw text itself unless it is - set explicitly. - - + + + + Text's accessibilityLabel is the raw text itself unless it is set + explicitly. + + - - - This text component's accessibilityLabel is set explicitly. - - + + + This text component's accessibilityLabel is set explicitly. + + - - - This is text one. - This is text two. - - + + + + This is text one. + + + This is text two. + + + - - - This is text one. - This is text two. - - + + + + This is text one. + + + This is text two. + + + - - - This is text one. - This is text two. - - + + + + This is text one. + + + This is text two. + + + - - - - This view's children are hidden from the accessibility tree - - - + + + + This view's children are hidden from the accessibility tree + + + - {/* Android screen readers will say the accessibility hint instead of the text + {/* Android screen readers will say the accessibility hint instead of the text since the view doesn't have a label. */} - - - This is text one. - This is text two. - - + + + + This is text one. + + + This is text two. + + + - - - This is text one. - This is text two. - - + + + + This is text one. + + + This is text two. + + + - - - This is a title. - - + + + This is a title. + + - - - This is a title. - - + + This is a title. + - - Alert.alert('Link has been clicked!')} - accessibilityRole="link"> - - - Click me - - - - + + Alert.alert('Link has been clicked!')} + accessibilityRole="link"> + + Click me + + + - - Alert.alert('Button has been pressed!')} - accessibilityRole="button"> - Click me - - + + Alert.alert('Button has been pressed!')} + accessibilityRole="button"> + Click me + + - - Alert.alert('Button has been pressed!')} - accessibilityRole="button" - accessibilityState={{disabled: true}} - disabled={true}> - - - I am disabled. Clicking me will not trigger any action. - - - - + + Alert.alert('Button has been pressed!')} + accessibilityRole="button" + accessibilityState={{disabled: true}} + disabled={true}> + + + I am disabled. Clicking me will not trigger any action. + + + + - - Alert.alert('Disabled Button has been pressed!')} - accessibilityLabel={ - 'You are pressing Disabled TouchableOpacity' - } - accessibilityState={{disabled: true}}> - - - I am disabled. Clicking me will not trigger any action. - - - - - - - - This view is selected and disabled. - - - + + Alert.alert('Disabled Button has been pressed!')} + accessibilityLabel={'You are pressing Disabled TouchableOpacity'} + accessibilityState={{disabled: true}}> + + + I am disabled. Clicking me will not trigger any action. + + + + + + + This view is selected and disabled. + + - - - - Accessible view with label, hint, role, and state - - - + + + + Accessible view with label, hint, role, and state + + + - - - - Accessible view with label, hint, role, and state - - - + + + + Accessible view with label, hint, role, and state + + + - - - - Mail Address - - - - First Name - - - - - - - - Enable Notifications - - - - + + + Mail Address + + First Name + - )} - + + + + + Enable Notifications + + + + + ); } } @@ -321,170 +312,138 @@ class AccessibilityExample extends React.Component<{}> { class AutomaticContentGrouping extends React.Component<{}> { render(): React.Node { return ( - - {theme => ( - - - - - - Text number 1 with a role - - - Text number 2 - - - - - - - { - switch (event.nativeEvent.actionName) { - case 'cut': - Alert.alert('Alert', 'cut action success'); - break; - case 'copy': - Alert.alert('Alert', 'copy action success'); - break; - case 'paste': - Alert.alert('Alert', 'paste action success'); - break; - } - }} - accessibilityRole="button"> - - - Text number 1 - - - Text number 2Text number 3 - - - - + + + + + + Text number 1 with a role + + Text number 2 + + + - - - - Text number 1 - - - - + + { + switch (event.nativeEvent.actionName) { + case 'cut': + Alert.alert('Alert', 'cut action success'); + break; + case 'copy': + Alert.alert('Alert', 'copy action success'); + break; + case 'paste': + Alert.alert('Alert', 'paste action success'); + break; + } + }} + accessibilityRole="button"> + + Text number 1 + + Text number 2 + Text number 3 + + + + - - - - - Text number 1 - - console.warn('onPress child')} - accessible={false} - accessibilityLabel="this is my label" - accessibilityRole="image" - accessibilityState={{disabled: true}} - accessibilityValue={{ - text: 'this is the accessibility value', - }}> - - Text number 3 - - - - - + + + + Text number 1 + + + + - + + + + Text number 1 - + focusable={true} + onPress={() => console.warn('onPress child')} + accessible={false} + accessibilityLabel="this is my label" + accessibilityRole="image" + accessibilityState={{disabled: true}} + accessibilityValue={{ + text: 'this is the accessibility value', + }}> + Text number 3 - + + + - - - - Text number 2 - - Text number 3Text number 4 - - - - + + + + + - - console.warn('onPress child')} - accessible={true} - accessibilityRole="button"> - - - - - + + + + Text number 2 + + Text number 3 + Text number 4 + + + + - - - - - + + console.warn('onPress child')} + accessible={true} + accessibilityRole="button"> + + + + + + + + + - )} - + + ); } } @@ -500,7 +459,7 @@ class CheckboxExample extends React.Component< }; _onCheckboxPress = () => { - let checkboxState: boolean | $TEMPORARY$string<'mixed'> = false; + let checkboxState: boolean | string = false; if (this.state.checkboxState === false) { checkboxState = 'mixed'; } else if (this.state.checkboxState === 'mixed') { @@ -510,26 +469,20 @@ class CheckboxExample extends React.Component< } this.setState({ - checkboxState: checkboxState, + checkboxState, }); }; render(): React.Node { return ( - - {theme => ( - - - Checkbox example - - - )} - + + Checkbox example + ); } } @@ -548,26 +501,20 @@ class SwitchExample extends React.Component< const switchState = !this.state.switchState; this.setState({ - switchState: switchState, + switchState, }); }; render(): React.Node { return ( - - {theme => ( - - - Switch example - - - )} - + + Switch example + ); } } @@ -601,7 +548,7 @@ class SelectionExample extends React.Component< if (!isEnabled) { accessibilityHint = 'use the button on the right to enable selection'; } - let buttonTitle = isEnabled ? 'Disable selection' : 'Enable selection'; + const buttonTitle = isEnabled ? 'Disable selection' : 'Enable selection'; const touchableHint = ` (touching the TouchableOpacity will ${ isSelected ? 'disable' : 'enable' } accessibilityState.selected)`; @@ -626,9 +573,9 @@ class SelectionExample extends React.Component< }} style={styles.touchable} accessibilityHint={accessibilityHint}> - + {`Selectable TouchableOpacity Example ${touchableHint}`} - + - {theme => ( - - - Expandable element example - - - )} - + + Expandable element example + ); } } @@ -719,7 +660,7 @@ class NestedCheckBox extends React.Component< } setTimeout(() => { this.setState({ - checkbox1: checkbox1, + checkbox1, checkbox2: checkbox1, checkbox3: checkbox1, }); @@ -730,7 +671,7 @@ class NestedCheckBox extends React.Component< const checkbox2 = !this.state.checkbox2; this.setState({ - checkbox2: checkbox2, + checkbox2, checkbox1: checkbox2 && this.state.checkbox3 ? true @@ -744,7 +685,7 @@ class NestedCheckBox extends React.Component< const checkbox3 = !this.state.checkbox3; this.setState({ - checkbox3: checkbox3, + checkbox3, checkbox1: this.state.checkbox2 && checkbox3 ? true @@ -756,59 +697,55 @@ class NestedCheckBox extends React.Component< render(): React.Node { return ( - - {theme => ( - - - - Meat - - - - Beef - - - - Bacon - - - )} - + + + + Meat + + + + Beef + + + + Bacon + + ); } } @@ -816,220 +753,168 @@ class NestedCheckBox extends React.Component< class AccessibilityRoleAndStateExample extends React.Component<{}> { render(): React.Node { const content = [ - This is some text, - This is some text, - This is some text, - This is some text, - This is some text, - This is some text, - This is some text, + This is some text, + This is some text, + This is some text, + This is some text, + This is some text, + This is some text, + This is some text, ]; return ( - - {theme => ( - - - - - {content} - - - - - - - {content} - - - - - - - {content} - - - - - - - - Alert example - - - - - - Combobox example - - - - - Menu example - - - - - Menu bar example - - - - - Menu item example - - - - - Progress bar example - - - - - Radio button example - - - - - Radio group example - - - - - Scrollbar example - - - - - Spin button example - - - - - - Tab example - - - - - Tab list example - - - - - Timer example - - - - - Toolbar example - - - - - State busy example - - - - - Drop Down List example - - - - - Pager example - - - - - Toggle Button example - - - - - Viewgroup example - - - - - Webview example - - - - - - Nested checkbox with delayed state change - - - - + + + + {content} + + + + + {content} + + + + + {content} + + + + + + Alert example + + + + Combobox example + + + Menu example + + + Menu bar example + + + Menu item example + + + Progress bar example + + + Radio button example + + + Radio group example + + + Scrollbar example + + + Spin button example + + + + Tab example + + + Tab list example + + + Timer example + + + Toolbar example + + + State busy example + + + Drop Down List example + + + Pager example + + + Toggle Button example + + + Viewgroup example + + + Webview example + + + + + Nested checkbox with delayed state change + + - )} - + + ); } } @@ -1037,152 +922,140 @@ class AccessibilityRoleAndStateExample extends React.Component<{}> { class AccessibilityActionsExample extends React.Component<{}> { render(): React.Node { return ( - - {theme => ( - - - { - switch (event.nativeEvent.actionName) { - case 'activate': - Alert.alert('Alert', 'View is clicked'); - break; - } - }}> - Click me - - + + + { + switch (event.nativeEvent.actionName) { + case 'activate': + Alert.alert('Alert', 'View is clicked'); + break; + } + }}> + Click me + + - - { - switch (event.nativeEvent.actionName) { - case 'cut': - Alert.alert('Alert', 'cut action success'); - break; - case 'copy': - Alert.alert('Alert', 'copy action success'); - break; - case 'paste': - Alert.alert('Alert', 'paste action success'); - break; - } - }}> - - This view supports many actions. - - - + + { + switch (event.nativeEvent.actionName) { + case 'cut': + Alert.alert('Alert', 'cut action success'); + break; + case 'copy': + Alert.alert('Alert', 'copy action success'); + break; + case 'paste': + Alert.alert('Alert', 'paste action success'); + break; + } + }}> + This view supports many actions. + + - - { - switch (event.nativeEvent.actionName) { - case 'increment': - Alert.alert('Alert', 'increment action success'); - break; - case 'decrement': - Alert.alert('Alert', 'decrement action success'); - break; - } - }}> - Slider - - + + { + switch (event.nativeEvent.actionName) { + case 'increment': + Alert.alert('Alert', 'increment action success'); + break; + case 'decrement': + Alert.alert('Alert', 'decrement action success'); + break; + } + }}> + Slider + + - - { - switch (event.nativeEvent.actionName) { - case 'cut': - Alert.alert('Alert', 'cut action success'); - break; - case 'copy': - Alert.alert('Alert', 'copy action success'); - break; - case 'paste': - Alert.alert('Alert', 'paste action success'); - break; - } - }} - onPress={() => Alert.alert('Button has been pressed!')} - accessibilityRole="button"> - - - Click me - - - - + + { + switch (event.nativeEvent.actionName) { + case 'cut': + Alert.alert('Alert', 'cut action success'); + break; + case 'copy': + Alert.alert('Alert', 'copy action success'); + break; + case 'paste': + Alert.alert('Alert', 'paste action success'); + break; + } + }} + onPress={() => Alert.alert('Button has been pressed!')} + accessibilityRole="button"> + + Click me + + + - -