diff --git a/packages/eslint-plugin-react-native/utils.js b/packages/eslint-plugin-react-native/utils.js index 728cc26d6ab..4bb8726be2c 100644 --- a/packages/eslint-plugin-react-native/utils.js +++ b/packages/eslint-plugin-react-native/utils.js @@ -109,10 +109,6 @@ const publicAPIMapping = { 'RefreshControlPropsIOS', ], }, - 'Libraries/Components/SafeAreaView/SafeAreaView': { - default: 'SafeAreaView', - types: null, - }, 'Libraries/Components/ScrollView/ScrollView': { default: 'ScrollView', types: [ diff --git a/packages/react-native-babel-preset/src/configs/lazy-imports.js b/packages/react-native-babel-preset/src/configs/lazy-imports.js index 95cb75e262c..23833aa2c14 100644 --- a/packages/react-native-babel-preset/src/configs/lazy-imports.js +++ b/packages/react-native-babel-preset/src/configs/lazy-imports.js @@ -29,7 +29,6 @@ module.exports = new Set([ 'Pressable', 'ProgressBarAndroid', 'ProgressViewIOS', - 'SafeAreaView', 'ScrollView', 'SectionList', 'Slider', diff --git a/packages/react-native/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js b/packages/react-native/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js deleted file mode 100644 index feb40f24ece..00000000000 --- a/packages/react-native/Libraries/Components/SafeAreaView/RCTSafeAreaViewNativeComponent.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * 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 - */ - -export * from '../../../src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent'; -export {default} from '../../../src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent'; diff --git a/packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js b/packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js deleted file mode 100644 index 2b141c8c849..00000000000 --- a/packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 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 type {HostInstance} from '../../../src/private/types/HostInstance'; -import type {ViewProps} from '../View/ViewPropTypes'; - -import Platform from '../../Utilities/Platform'; -import View from '../View/View'; -import * as React from 'react'; - -export type SafeAreaViewInstance = HostInstance; - -/** - * Renders content within the safe area boundaries of a device. Currently only applicable to iOS devices with iOS version 11 or later. Automatically applies padding to reflect the portion of the view not covered by navigation bars, tab bars, toolbars, and other ancestor views. - * - * @see https://reactnative.dev/docs/safeareaview - * @deprecated Use `react-native-safe-area-context` instead. - * @platform ios - */ -const SafeAreaView: component( - ref?: React.RefSetter, - ...props: ViewProps -) = Platform.select({ - ios: require('./RCTSafeAreaViewNativeComponent').default, - default: View, -}); - -export default SafeAreaView; diff --git a/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js b/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js index e00db0a1fa3..c11bd828f04 100644 --- a/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js +++ b/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js @@ -8,7 +8,7 @@ * @format */ -import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView'; +import SafeAreaView from '../../../src/private/components/safeareaview/SafeAreaView'; import StyleSheet, { type ColorValue, type ViewStyleProp, diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js index 5afdaf3f415..6e851856a8f 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -23,6 +23,7 @@ import type { LayoutRectangle, MouseEvent, PointerEvent, + SafeAreaInsetsChangeEvent, } from '../../Types/CoreEventTypes'; import type { AccessibilityActionEvent, @@ -63,6 +64,30 @@ type DirectEventProps = Readonly<{ */ onLayout?: ?(event: LayoutChangeEvent) => unknown, + /** + * Invoked when the part of this view that is covered by the system UI + * (status bar, navigation bar, home indicator, display cutouts, ...) + * changes, with: + * + * `{nativeEvent: {insets: {top, right, bottom, left}, frame: {x, y, width, height}}}` + * + * `insets` are relative to this view: an inset is only non-zero for the part + * of the view that actually overlaps the system UI. `frame` is the position + * of the view at the time of the event, relative to its enclosing view + * controller on iOS and to the window on Android; it does not trigger the + * event on its own, so it can be stale while the view moves without its + * insets changing. + * + * The event is dispatched synchronously, so the rendering it schedules is + * applied in the same frame the insets changed in. + * + * Setting this prop makes the view observe safe area changes; views without + * it are unaffected. + * + * @experimental + */ + onSafeAreaInsetsChange?: ?(event: SafeAreaInsetsChangeEvent) => unknown, + /** * When `accessible` is `true`, the system will invoke this function when the * user performs the magic tap gesture. diff --git a/packages/react-native/Libraries/Components/View/__tests__/ViewSafeAreaInsets-itest.js b/packages/react-native/Libraries/Components/View/__tests__/ViewSafeAreaInsets-itest.js new file mode 100644 index 00000000000..877a57931ab --- /dev/null +++ b/packages/react-native/Libraries/Components/View/__tests__/ViewSafeAreaInsets-itest.js @@ -0,0 +1,142 @@ +/** + * 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 '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {HostInstance} from 'react-native/src/private/types/HostInstance'; + +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {createRef} from 'react'; +import {View} from 'react-native'; +import SafeAreaView from 'react-native/src/private/components/safeareaview/SafeAreaView'; + +const INSETS = {top: 44, right: 0, bottom: 34, left: 0}; +const FRAME = {x: 0, y: 0, width: 390, height: 844}; + +describe('onSafeAreaInsetsChange', () => { + it('delivers the insets and the frame of the view', () => { + const root = Fantom.createRoot(); + const nodeRef = createRef(); + const onSafeAreaInsetsChange = jest.fn(); + + Fantom.runTask(() => { + root.render( + { + onSafeAreaInsetsChange(event.nativeEvent); + }} + />, + ); + }); + + Fantom.dispatchNativeEvent(nodeRef, 'safeAreaInsetsChange', { + insets: INSETS, + frame: FRAME, + }); + + expect(onSafeAreaInsetsChange).toHaveBeenCalledTimes(1); + const [event] = onSafeAreaInsetsChange.mock.lastCall; + expect(event.insets).toEqual(INSETS); + expect(event.frame).toEqual(FRAME); + }); + + it('is not delivered to views that did not opt in', () => { + const root = Fantom.createRoot(); + const nodeRef = createRef(); + + Fantom.runTask(() => { + root.render(); + }); + + // The prop is what makes the view observe the safe area, so a view without + // it is never the target of the event. + expect( + root.getRenderedOutput({props: ['onSafeAreaInsetsChange']}).toJSX(), + ).toEqual(); + }); + + it('prevents the view from being flattened', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + // A layout-only view would ordinarily be flattened away; observing the + // safe area requires a host view to observe with. + {}}> + + , + ); + }); + + expect( + root.getRenderedOutput({props: ['onSafeAreaInsetsChange']}).toJSX(), + ).toEqual( + + + , + ); + }); + + it('is reflected in the props of the view when set', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + {}} />, + ); + }); + + expect( + root.getRenderedOutput({props: ['onSafeAreaInsetsChange']}).toJSX(), + ).toEqual(); + }); +}); + +describe('', () => { + it('applies the insets it receives as padding', () => { + const root = Fantom.createRoot(); + const nodeRef = createRef(); + + Fantom.runTask(() => { + root.render(); + }); + + expect( + root + .getRenderedOutput({ + props: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'], + }) + .toJSX(), + ).toEqual(); + + Fantom.dispatchNativeEvent(nodeRef, 'safeAreaInsetsChange', { + insets: INSETS, + frame: FRAME, + }); + + expect( + root + .getRenderedOutput({ + props: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'], + }) + .toJSX(), + ).toEqual( + , + ); + }); +}); diff --git a/packages/react-native/Libraries/LogBox/LogBoxNotificationContainer.js b/packages/react-native/Libraries/LogBox/LogBoxNotificationContainer.js index 33593b2a2af..9f7d3b6aa9b 100644 --- a/packages/react-native/Libraries/LogBox/LogBoxNotificationContainer.js +++ b/packages/react-native/Libraries/LogBox/LogBoxNotificationContainer.js @@ -8,7 +8,7 @@ * @format */ -import SafeAreaView from '../../src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE'; +import SafeAreaView from '../../src/private/components/safeareaview/SafeAreaView'; import View from '../Components/View/View'; import StyleSheet from '../StyleSheet/StyleSheet'; import BackHandler from '../Utilities/BackHandler'; diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorFooterButton.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorFooterButton.js index 3db512ed3d4..a896030ab1c 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorFooterButton.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorFooterButton.js @@ -8,7 +8,7 @@ * @format */ -import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView'; +import SafeAreaView from '../../../src/private/components/safeareaview/SafeAreaView'; import View from '../../Components/View/View'; import StyleSheet from '../../StyleSheet/StyleSheet'; import Text from '../../Text/Text'; diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js index a5b5329d440..aebec6d63fa 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js @@ -8,10 +8,9 @@ * @format */ -import type {ViewProps} from '../../Components/View/ViewPropTypes'; import type {LogLevel} from '../Data/LogBoxLog'; -import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView'; +import SafeAreaView from '../../../src/private/components/safeareaview/SafeAreaView'; import View from '../../Components/View/View'; import StyleSheet from '../../StyleSheet/StyleSheet'; import Text from '../../Text/Text'; @@ -27,13 +26,10 @@ type Props = Readonly<{ level: LogLevel, }>; -const LogBoxInspectorHeaderSafeArea: React.ComponentType = - Platform.OS === 'android' ? View : SafeAreaView; - export default function LogBoxInspectorHeader(props: Props): React.Node { if (props.level === 'syntax') { return ( - + - + ); } @@ -56,7 +52,7 @@ export default function LogBoxInspectorHeader(props: Props): React.Node { const titleText = `Log ${props.selectedIndex + 1} of ${props.total}`; return ( - + props.onSelectIndex(nextIndex)} /> - + ); } diff --git a/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxNotificationContainer-test.js.snap b/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxNotificationContainer-test.js.snap index 82a662152af..4cdf7db240e 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxNotificationContainer-test.js.snap +++ b/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxNotificationContainer-test.js.snap @@ -1,14 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`LogBoxNotificationContainer should render both an error and warning notification 1`] = ` - - + `; exports[`LogBoxNotificationContainer should render null with no logs 1`] = `null`; @@ -113,14 +117,18 @@ exports[`LogBoxNotificationContainer should render selected fatal error even whe exports[`LogBoxNotificationContainer should render selected syntax error even when disabled 1`] = `null`; exports[`LogBoxNotificationContainer should render the latest error notification 1`] = ` - - + `; exports[`LogBoxNotificationContainer should render the latest warning notification 1`] = ` - - + `; diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index 0f93c41709c..a5b4fa3cc13 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -204,6 +204,9 @@ const directEventTypes = { topLayout: { registrationName: 'onLayout', }, + topSafeAreaInsetsChange: { + registrationName: 'onSafeAreaInsetsChange', + }, }; const validAttributesForNonEventProps = { @@ -402,6 +405,7 @@ const validAttributesForNonEventProps = { // Props for bubbling and direct events const validAttributesForEventProps = { onLayout: true, + onSafeAreaInsetsChange: true, // PanResponder handlers onMoveShouldSetResponder: true, diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js index d22a6864219..acabd7911c5 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js @@ -179,6 +179,9 @@ const directEventTypes = { topLayout: { registrationName: 'onLayout', }, + topSafeAreaInsetsChange: { + registrationName: 'onSafeAreaInsetsChange', + }, onGestureHandlerEvent: DynamicallyInjectedByGestureHandler({ registrationName: 'onGestureHandlerEvent', }), @@ -380,6 +383,7 @@ const validAttributesForNonEventProps = { // Props for bubbling and direct events const validAttributesForEventProps = ConditionallyIgnoredEventHandlers({ onLayout: true, + onSafeAreaInsetsChange: true, onMagicTap: true, // Accessibility diff --git a/packages/react-native/Libraries/Types/CoreEventTypes.js b/packages/react-native/Libraries/Types/CoreEventTypes.js index dff10cb2760..15aff5e610c 100644 --- a/packages/react-native/Libraries/Types/CoreEventTypes.js +++ b/packages/react-native/Libraries/Types/CoreEventTypes.js @@ -76,6 +76,29 @@ export type LayoutChangeEvent = NativeSyntheticEvent< }>, >; +export type SafeAreaInsets = Readonly<{ + top: number, + right: number, + bottom: number, + left: number, +}>; + +export type SafeAreaInsetsChangeEvent = NativeSyntheticEvent< + Readonly<{ + /** + * The part of the view that is covered by the system UI, in the view's own + * coordinate space. + */ + insets: SafeAreaInsets, + /** + * The frame of the view at the time of the event. Relative to the + * enclosing view controller on iOS and to the window on Android; only + * updated when the insets change. + */ + frame: LayoutRectangle, + }>, +>; + /** * @deprecated Use `TextLayoutEvent` instead. */ diff --git a/packages/react-native/Libraries/Utilities/Dimensions.js b/packages/react-native/Libraries/Utilities/Dimensions.js index 85a6fc5dc49..fb86412115a 100644 --- a/packages/react-native/Libraries/Utilities/Dimensions.js +++ b/packages/react-native/Libraries/Utilities/Dimensions.js @@ -16,10 +16,16 @@ import NativeDeviceInfo, { type DimensionsPayload, type DisplayMetrics, type DisplayMetricsAndroid, + type WindowSafeAreaInsets, } from './NativeDeviceInfo'; import invariant from 'invariant'; -export type {DimensionsPayload, DisplayMetrics, DisplayMetricsAndroid}; +export type { + DimensionsPayload, + DisplayMetrics, + DisplayMetricsAndroid, + WindowSafeAreaInsets, +}; /** @deprecated Use DisplayMetrics */ export type ScaledSize = DisplayMetrics; @@ -72,11 +78,21 @@ class Dimensions { let {screen, window} = dims; const {windowPhysicalPixels} = dims; if (windowPhysicalPixels) { + const {scale, safeAreaInsets} = windowPhysicalPixels; window = { - width: windowPhysicalPixels.width / windowPhysicalPixels.scale, - height: windowPhysicalPixels.height / windowPhysicalPixels.scale, - scale: windowPhysicalPixels.scale, + width: windowPhysicalPixels.width / scale, + height: windowPhysicalPixels.height / scale, + scale, fontScale: windowPhysicalPixels.fontScale, + safeAreaInsets: + safeAreaInsets == null + ? undefined + : { + top: safeAreaInsets.top / scale, + right: safeAreaInsets.right / scale, + bottom: safeAreaInsets.bottom / scale, + left: safeAreaInsets.left / scale, + }, }; } const {screenPhysicalPixels} = dims; diff --git a/packages/react-native/Libraries/Utilities/__tests__/Dimensions-itest.js b/packages/react-native/Libraries/Utilities/__tests__/Dimensions-itest.js index e4481e8f8be..513b925dd63 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/Dimensions-itest.js +++ b/packages/react-native/Libraries/Utilities/__tests__/Dimensions-itest.js @@ -30,6 +30,26 @@ describe('Dimensions', () => { expect(Dimensions.get('window').fontScale).toEqual(3); }); + it('should scale window safe area insets from physical pixels', () => { + Dimensions.set({ + windowPhysicalPixels: { + width: 400, + height: 800, + scale: 2, + densityDpi: 2, + fontScale: 3, + safeAreaInsets: {top: 96, right: 0, bottom: 48, left: 0}, + }, + }); + + expect(Dimensions.get('window').safeAreaInsets).toEqual({ + top: 48, + right: 0, + bottom: 24, + left: 0, + }); + }); + it('should set screen dimensions on Android', () => { // $FlowFixMe[incompatible-type] - `Platform.OS` needs to be read-only. Platform.OS = 'android'; diff --git a/packages/react-native/Package.swift b/packages/react-native/Package.swift index c1e3dfc00f4..3caae2edde6 100644 --- a/packages/react-native/Package.swift +++ b/packages/react-native/Package.swift @@ -472,7 +472,6 @@ let reactFabric = RNTarget( "components/inputaccessory", "components/modal", "components/rncore", - "components/safeareaview", "components/switch", "components/text", "components/textinput", @@ -509,12 +508,6 @@ let reactFabricSwitch = RNTarget( dependencies: [.reactNativeDependencies, .reactCore, .reactJsiExecutor, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .yoga, .reactRendererDebug, .reactGraphics, .reactFabric, .reactTurboModuleBridging] ) -let reactFabricSafeAreaView = RNTarget( - name: .reactFabricSafeAreaView, - path: "ReactCommon/react/renderer/components/safeareaview", - dependencies: [.reactNativeDependencies, .reactCore, .reactJsiExecutor, .reactTurboModuleCore, .jsi, .logger, .reactDebug, .reactFeatureFlags, .reactUtils, .reactRuntimeScheduler, .reactCxxReact, .yoga, .reactRendererDebug, .reactGraphics, .reactFabric, .reactTurboModuleBridging] -) - let reactFabricTextLayoutManager = RNTarget( name: .reactFabricTextLayoutManager, path: "ReactCommon/react/renderer/textlayoutmanager", @@ -567,7 +560,7 @@ let reactRCTFabric = RNTarget( name: .reactRCTFabric, path: "React/Fabric", searchPaths: [virtualViewPath], - dependencies: [.reactNativeDependencies, .reactCore, .reactRCTImage, .yoga, .reactRCTText, .jsi, .reactFabricInputAccessory, .reactFabricModal, .reactFabricSafeAreaView, .reactFabricSwitch, .reactFabricText, .reactFabricTextInput, .reactFabricUnimplementedView, .reactFabricTextLayoutManager, .reactGraphics, .reactImageManager, .reactDebug, .reactUtils, .reactPerformanceTimeline, .reactRendererDebug, .reactRendererConsistency, .reactRuntimeScheduler, .reactRCTAnimation, .reactJsInspector, .reactJsInspectorNetwork, .reactJsInspectorTracing, .reactFabric, .reactFabricImage, .rctSwiftUIWrapper] + dependencies: [.reactNativeDependencies, .reactCore, .reactRCTImage, .yoga, .reactRCTText, .jsi, .reactFabricInputAccessory, .reactFabricModal, .reactFabricSwitch, .reactFabricText, .reactFabricTextInput, .reactFabricUnimplementedView, .reactFabricTextLayoutManager, .reactGraphics, .reactImageManager, .reactDebug, .reactUtils, .reactPerformanceTimeline, .reactRendererDebug, .reactRendererConsistency, .reactRuntimeScheduler, .reactRCTAnimation, .reactJsInspector, .reactJsInspectorNetwork, .reactJsInspectorTracing, .reactFabric, .reactFabricImage, .rctSwiftUIWrapper] ) /// React-ImageManagerApple.podspec @@ -690,7 +683,6 @@ let targets = [ reactFabricImage, reactFabricInputAccessory, reactFabricModal, - reactFabricSafeAreaView, reactFabricSwitch, reactFabricTextLayoutManager, reactFabricText, @@ -875,7 +867,6 @@ extension String { static let reactFabricImage = "React-FabricImage" static let reactFabricInputAccessory = "React-FabricInputAccessory" static let reactFabricModal = "React-FabricModal" - static let reactFabricSafeAreaView = "React-FabricSafeAreaView" static let reactFabricSwitch = "React-FabricSwitch" static let reactFabricTextLayoutManager = "React-FabricTextLayoutManager" static let reactFabricText = "React-FabricText" diff --git a/packages/react-native/React/Base/RCTBridge.mm b/packages/react-native/React/Base/RCTBridge.mm index d7379488d37..9a40172155e 100644 --- a/packages/react-native/React/Base/RCTBridge.mm +++ b/packages/react-native/React/Base/RCTBridge.mm @@ -55,7 +55,6 @@ @"RCTDebuggingOverlayManager", @"RCTModalHostViewManager", @"RCTRefreshControlManager", - @"RCTSafeAreaViewManager", @"RCTScrollContentViewManager", @"RCTScrollViewManager", @"RCTSwitchManager", diff --git a/packages/react-native/React/CoreModules/RCTDeviceInfo.mm b/packages/react-native/React/CoreModules/RCTDeviceInfo.mm index 1761214fc3b..7d2c7c5215e 100644 --- a/packages/react-native/React/CoreModules/RCTDeviceInfo.mm +++ b/packages/react-native/React/CoreModules/RCTDeviceInfo.mm @@ -201,12 +201,19 @@ static BOOL RCTIsIPhoneNotched() // We fallback to screen size if a key window is not found. CGSize windowSize = mainWindow != nil ? mainWindow.bounds.size : screenSize; + UIEdgeInsets safeAreaInsets = mainWindow != nil ? mainWindow.safeAreaInsets : UIEdgeInsetsZero; - NSDictionary *dimsWindow = @{ + NSDictionary *dimsWindow = @{ @"width" : @(windowSize.width), @"height" : @(windowSize.height), @"scale" : @(screen.scale), - @"fontScale" : @(fontScale) + @"fontScale" : @(fontScale), + @"safeAreaInsets" : @{ + @"top" : @(safeAreaInsets.top), + @"right" : @(safeAreaInsets.right), + @"bottom" : @(safeAreaInsets.bottom), + @"left" : @(safeAreaInsets.left) + } }; NSDictionary *dimsScreen = @{ diff --git a/packages/react-native/React/Fabric/AppleEventBeat.cpp b/packages/react-native/React/Fabric/AppleEventBeat.cpp deleted file mode 100644 index 4a3d533a0cd..00000000000 --- a/packages/react-native/React/Fabric/AppleEventBeat.cpp +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 index 256e0f0983a..528145e2379 100644 --- a/packages/react-native/React/Fabric/AppleEventBeat.h +++ b/packages/react-native/React/Fabric/AppleEventBeat.h @@ -7,6 +7,8 @@ #pragma once +#include + #include #include #include @@ -19,6 +21,11 @@ class RuntimeScheduler; * Event beat associated with JavaScript runtime. * The beat is called on `RuntimeExecutor`'s thread induced by the UI thread * event loop. + * + * A synchronous request made while Core Animation is laying out the current + * frame (the run loop observer that induces the beat has already run at that + * point) is additionally induced from the display phase of the same commit + * cycle, so that its effects are mounted before the frame is presented. */ class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate { public: @@ -27,13 +34,20 @@ class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate { std::unique_ptr uiRunLoopObserver, RuntimeScheduler &RuntimeScheduler); + ~AppleEventBeat() override; + + void requestSynchronous() const override; + #pragma mark - RunLoopObserver::Delegate void activityDidChange(const RunLoopObserver::Delegate *delegate, RunLoopObserver::Activity activity) const noexcept override; private: + class DisplayPhaseFlusher; + std::unique_ptr uiRunLoopObserver_; + std::unique_ptr displayPhaseFlusher_; }; } // namespace facebook::react diff --git a/packages/react-native/React/Fabric/AppleEventBeat.mm b/packages/react-native/React/Fabric/AppleEventBeat.mm new file mode 100644 index 00000000000..2212302df6f --- /dev/null +++ b/packages/react-native/React/Fabric/AppleEventBeat.mm @@ -0,0 +1,141 @@ +/* + * 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" + +#import +#import + +#include + +/* + * A zero-sized layer whose only purpose is to run a callback during the + * display phase of a Core Animation commit. Core Animation processes a commit + * as layout → display → (repeat until stable) → commit, so a layer marked as + * needing display during the layout phase has its `display` called after the + * whole layout pass but before the transaction is committed. + */ +@interface RCTEventBeatFlusherLayer : CALayer +@property (nonatomic, copy, nullable) void (^onDisplay)(void); +@end + +@implementation RCTEventBeatFlusherLayer + +- (void)display +{ + if (self.onDisplay != nil) { + self.onDisplay(); + } +} + +// The layer is not a visual element; never participate in animations. +- (id)actionForKey:(NSString *)event +{ + return nil; +} + +@end + +namespace facebook::react { + +/* + * Owns the flusher layer and keeps it attached to the key window's layer so + * it participates in the current Core Animation commit cycle. + */ +class AppleEventBeat::DisplayPhaseFlusher { + public: + DisplayPhaseFlusher(std::function callback, std::weak_ptr weakOwner) + { + layer_ = [RCTEventBeatFlusherLayer new]; + layer_.frame = CGRectZero; + auto sharedCallback = std::make_shared>(std::move(callback)); + layer_.onDisplay = ^{ + // The owner (indirectly) retains the event beat; if it is gone, so is + // the beat the callback points into. + auto owner = weakOwner.lock(); + if (!owner) { + return; + } + (*sharedCallback)(); + }; + } + + ~DisplayPhaseFlusher() + { + // The beat can be destroyed on any thread; layer mutations belong on the + // main thread. The block only retains the layer, and a display happening + // before this executes is made safe by the owner check above. + RCTEventBeatFlusherLayer *layer = layer_; + RCTExecuteOnMainQueue(^{ + layer.onDisplay = nil; + [layer removeFromSuperlayer]; + }); + } + + /* + * Schedules the callback to run in the display phase of the current (or + * next) Core Animation commit cycle. Main thread only. + */ + void schedule() const + { + CALayer *hostLayer = RCTKeyWindow().layer; + if (hostLayer == nil) { + // No window to participate in a commit with; the run loop observer will + // process the request on the next turn instead. + return; + } + if (layer_.superlayer != hostLayer) { + [layer_ removeFromSuperlayer]; + [hostLayer addSublayer:layer_]; + } + [layer_ setNeedsDisplay]; + } + + private: + RCTEventBeatFlusherLayer *layer_; +}; + +AppleEventBeat::AppleEventBeat( + std::shared_ptr ownerBox, + std::unique_ptr uiRunLoopObserver, + RuntimeScheduler& runtimeScheduler) + : EventBeat(std::move(ownerBox), runtimeScheduler), + uiRunLoopObserver_(std::move(uiRunLoopObserver)), + displayPhaseFlusher_( + std::make_unique([this]() { induce(); }, ownerBox_->owner)) +{ + uiRunLoopObserver_->setDelegate(this); + uiRunLoopObserver_->enable(); +} + +AppleEventBeat::~AppleEventBeat() = default; + +void AppleEventBeat::requestSynchronous() const +{ + EventBeat::requestSynchronous(); + + // The run loop observer that ordinarily induces the beat runs before Core + // Animation commits the frame. A synchronous request made while Core + // Animation is already laying out (e.g. an event emitted from + // `layoutSubviews`) would therefore only be processed on the next frame. + // Scheduling an induce in the display phase of the current commit cycle + // processes it before this frame is presented. Multiple requests within one + // cycle coalesce into a single induce. + if (RCTIsMainQueue()) { + displayPhaseFlusher_->schedule(); + } +} + +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/Mounting/ComponentViews/RCTFabricComponentsPlugins.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h index 6b8a6ff6d6d..d03b01686f5 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h @@ -34,7 +34,6 @@ Class RCTDebuggingOverlayCls(void) __attribute__((used Class RCTInputAccessoryCls(void) __attribute__((used)); Class RCTParagraphCls(void) __attribute__((used)); Class RCTPullToRefreshViewCls(void) __attribute__((used)); -Class RCTSafeAreaViewCls(void) __attribute__((used)); Class RCTScrollViewCls(void) __attribute__((used)); Class RCTSwitchCls(void) __attribute__((used)); Class RCTTextInputCls(void) __attribute__((used)); diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm index 442c2e6e396..fe974664467 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.mm @@ -23,7 +23,6 @@ {"InputAccessoryView", RCTInputAccessoryCls}, {"Paragraph", RCTParagraphCls}, {"PullToRefreshView", RCTPullToRefreshViewCls}, - {"SafeAreaView", RCTSafeAreaViewCls}, {"ScrollView", RCTScrollViewCls}, {"Switch", RCTSwitchCls}, {"TextInput", RCTTextInputCls}, diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.h deleted file mode 100644 index 542b88cf14f..00000000000 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.h +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 - -NS_ASSUME_NONNULL_BEGIN - -/** - * UIView class for root component. - */ -@interface RCTSafeAreaViewComponentView : RCTViewComponentView - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.mm deleted file mode 100644 index 4c05633d8cb..00000000000 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/SafeAreaView/RCTSafeAreaViewComponentView.mm +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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 "RCTSafeAreaViewComponentView.h" - -#import -#import -#import -#import "RCTConversions.h" -#import "RCTFabricComponentsPlugins.h" - -using namespace facebook::react; - -@implementation RCTSafeAreaViewComponentView { - SafeAreaViewShadowNode::ConcreteState::Shared _state; -} - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - _props = SafeAreaViewShadowNode::defaultSharedProps(); - } - - return self; -} - -- (void)safeAreaInsetsDidChange -{ - [super safeAreaInsetsDidChange]; - - [self _updateStateIfNecessary]; -} - -- (void)_updateStateIfNecessary -{ - if (!_state) { - return; - } - - UIEdgeInsets insets = self.safeAreaInsets; - insets.left = RCTRoundPixelValue(insets.left); - insets.top = RCTRoundPixelValue(insets.top); - insets.right = RCTRoundPixelValue(insets.right); - insets.bottom = RCTRoundPixelValue(insets.bottom); - - auto newPadding = RCTEdgeInsetsFromUIEdgeInsets(insets); - auto threshold = 1.0 / RCTScreenScale() + 0.01; // Size of a pixel plus some small threshold. - - _state->updateState( - [=](const SafeAreaViewShadowNode::ConcreteState::Data &oldData) - -> SafeAreaViewShadowNode::ConcreteState::SharedData { - auto oldPadding = oldData.padding; - auto deltaPadding = newPadding - oldPadding; - - if (std::abs(deltaPadding.left) < threshold && std::abs(deltaPadding.top) < threshold && - std::abs(deltaPadding.right) < threshold && std::abs(deltaPadding.bottom) < threshold) { - return nullptr; - } - - auto newData = oldData; - newData.padding = newPadding; - return std::make_shared(newData); - }); -} - -#pragma mark - RCTComponentViewProtocol - -+ (ComponentDescriptorProvider)componentDescriptorProvider -{ - return concreteComponentDescriptorProvider(); -} - -- (void)updateState:(const facebook::react::State::Shared &)state - oldState:(const facebook::react::State::Shared &)oldState -{ - _state = std::static_pointer_cast(state); -} - -- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask -{ - [super finalizeUpdates:updateMask]; - [self _updateStateIfNecessary]; -} - -- (void)prepareForRecycle -{ - [super prepareForRecycle]; - _state.reset(); -} - -@end - -Class RCTSafeAreaViewCls(void) -{ - return RCTSafeAreaViewComponentView.class; -} 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 b46c5e6cd33..277718515ad 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -25,6 +25,7 @@ #import #import #import +#import #import #import #import @@ -122,6 +123,11 @@ @implementation RCTViewComponentView { NSMutableSet *_accessibilityOrderNativeIDs; RCTSwiftUIContainerViewWrapper *_swiftUIWrapper; BOOL _focusable; + // The insets sent with the last `onSafeAreaInsetsChange` event, or nil if + // none was sent yet. A pointer because almost no view observes the safe + // area: the views that do pay for a small box, every other view only for + // the pointer. + NSValue *_lastSentSafeAreaInsets; } #ifdef RCT_DYNAMIC_FRAMEWORKS @@ -438,6 +444,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & -newViewProps.hitSlop.right}; } + // `onSafeAreaInsetsChange`. Scheduled whenever the prop is set, not only on + // its transitions: recycled views keep their last props, so `oldViewProps` + // of a freshly reused view is not a reliable baseline. + if (newViewProps.onSafeAreaInsetsChange) { + [self setNeedsLayout]; + } else if (oldViewProps.onSafeAreaInsetsChange) { + _lastSentSafeAreaInsets = nil; + } + // `overflow` if (oldViewProps.getClipsContentToBounds() != newViewProps.getClipsContentToBounds()) { self.currentContainerView.clipsToBounds = newViewProps.getClipsContentToBounds(); @@ -720,6 +735,105 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics } } +#pragma mark - Safe area insets + +// The view controller the view is hosted in, which is the coordinate space +// `frame` is reported in. Modals and other view controllers are positioned +// independently of the window, so the window is not a usable reference. +static UIViewController *RCTParentViewControllerOfView(UIView *view) +{ + UIResponder *responder = view.nextResponder; + while (responder != nil) { + if ([responder isKindOfClass:[UIViewController class]]) { + return (UIViewController *)responder; + } + responder = responder.nextResponder; + } + return nil; +} + +static BOOL RCTEdgeInsetsEqualWithThreshold(UIEdgeInsets lhs, UIEdgeInsets rhs, CGFloat threshold) +{ + return ABS(lhs.left - rhs.left) <= threshold && ABS(lhs.top - rhs.top) <= threshold && + ABS(lhs.right - rhs.right) <= threshold && ABS(lhs.bottom - rhs.bottom) <= threshold; +} + +// The event is only ever emitted from `layoutSubviews`; everything that might +// have changed the insets merely marks the view as needing layout. This defers +// the emit out of arbitrary call contexts — in particular out of +// `updateProps`, which runs inside the mounting transaction where +// synchronously re-entering React is not safe — while keeping it in the same +// frame: the layout pass runs before the frame is displayed. +- (void)_safeAreaInsetsMayHaveChanged +{ + if (!_eventEmitter) { + return; + } + + // The view has not been mounted or laid out yet, so the insets we would + // compute are not the ones the view ends up with. + if (self.window == nil || CGSizeEqualToSize(self.bounds.size, CGSizeZero)) { + return; + } + + // Only a change of the insets triggers an event. The frame is part of the + // payload but not of the trigger: a view that moves without its overlap with + // the system UI changing stays silent, which is what makes observing views + // safe to place inside scroll views. + UIEdgeInsets insets = self.safeAreaInsets; + if (_lastSentSafeAreaInsets != nil && + RCTEdgeInsetsEqualWithThreshold(insets, _lastSentSafeAreaInsets.UIEdgeInsetsValue, 1.0 / RCTScreenScale())) { + return; + } + + UIView *referenceView = RCTParentViewControllerOfView(self).view ?: self.window; + CGRect frame = [self convertRect:self.bounds toView:referenceView]; + + _lastSentSafeAreaInsets = [NSValue valueWithUIEdgeInsets:insets]; + + static_cast(*_eventEmitter) + .onSafeAreaInsetsChange( + EdgeInsets{ + .left = (Float)insets.left, + .top = (Float)insets.top, + .right = (Float)insets.right, + .bottom = (Float)insets.bottom}, + RCTRectFromCGRect(frame)); +} + +// The prop is checked here rather than inside the helper so that views which +// do not use it only pay for a branch on a prop they already have in hand. +- (BOOL)_observesSafeAreaInsets +{ + return static_cast(*_props).onSafeAreaInsetsChange; +} + +- (void)safeAreaInsetsDidChange +{ + [super safeAreaInsetsDidChange]; + if ([self _observesSafeAreaInsets]) { + [self setNeedsLayout]; + } +} + +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + if ([self _observesSafeAreaInsets]) { + [self setNeedsLayout]; + } +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + // Both the insets and the frame depend on where the view sits in the window, + // so moving or resizing it changes them without UIKit notifying us. + if ([self _observesSafeAreaInsets]) { + [self _safeAreaInsetsMayHaveChanged]; + } +} + - (BOOL)isJSResponder { return _isJSResponder; @@ -775,6 +889,7 @@ - (void)prepareForRecycle _filterLayer = nil; [self clearExistingBackgroundImageLayers]; + _lastSentSafeAreaInsets = nil; _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil; _eventEmitter.reset(); _isJSResponder = NO; diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 4d0e582d353..4211692b73e 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3242,6 +3242,7 @@ public abstract class com/facebook/react/uimanager/BaseViewManager : com/faceboo public fun setMoveShouldSetResponder (Landroid/view/View;Z)V public fun setMoveShouldSetResponderCapture (Landroid/view/View;Z)V public fun setNativeId (Landroid/view/View;Ljava/lang/String;)V + public fun setOnSafeAreaInsetsChange (Landroid/view/View;Z)V public fun setOpacity (Landroid/view/View;F)V public fun setOutlineColor (Landroid/view/View;Ljava/lang/Integer;)V public fun setOutlineOffset (Landroid/view/View;F)V @@ -4579,6 +4580,7 @@ public final class com/facebook/react/uimanager/ViewProps { public static final field NONE Ljava/lang/String; public static final field NUMBER_OF_LINES Ljava/lang/String; public static final field ON Ljava/lang/String; + public static final field ON_SAFE_AREA_INSETS_CHANGE Ljava/lang/String; public static final field OPACITY Ljava/lang/String; public static final field OUTLINE_COLOR Ljava/lang/String; public static final field OUTLINE_OFFSET Ljava/lang/String; @@ -5131,14 +5133,6 @@ public abstract interface class com/facebook/react/viewmanagers/ModalHostViewMan public abstract fun setVisible (Landroid/view/View;Z)V } -public class com/facebook/react/viewmanagers/SafeAreaViewManagerDelegate : com/facebook/react/uimanager/BaseViewManagerDelegate { - public fun (Lcom/facebook/react/uimanager/BaseViewManager;)V - public fun setProperty (Landroid/view/View;Ljava/lang/String;Ljava/lang/Object;)V -} - -public abstract interface class com/facebook/react/viewmanagers/SafeAreaViewManagerInterface : com/facebook/react/uimanager/ViewManagerWithGeneratedInterface { -} - public class com/facebook/react/viewmanagers/UnimplementedNativeViewManagerDelegate : com/facebook/react/uimanager/BaseViewManagerDelegate { public fun (Lcom/facebook/react/uimanager/BaseViewManager;)V public fun setProperty (Landroid/view/View;Ljava/lang/String;Ljava/lang/Object;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt index 5f5f5a4308e..ca401d761ee 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.kt @@ -23,6 +23,7 @@ import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.DisplayMetricsHolder.getScreenDisplayMetrics import com.facebook.react.uimanager.DisplayMetricsHolder.initDisplayMetricsIfNotInitialized +import com.facebook.react.uimanager.internal.SafeAreaInsetsObserver import com.facebook.react.views.view.isEdgeToEdgeFeatureFlagOn /** Module that exposes Android Constants to JS. */ @@ -71,7 +72,9 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) : WritableNativeMap().apply { putMap( "windowPhysicalPixels", - getPhysicalPixelsWritableMap(getWindowDisplayMetrics()), + getPhysicalPixelsWritableMap(getWindowDisplayMetrics()).apply { + getWindowSafeAreaInsetsWritableMap()?.let { putMap("safeAreaInsets", it) } + }, ) putMap( "screenPhysicalPixels", @@ -79,6 +82,21 @@ internal class DeviceInfoModule(reactContext: ReactApplicationContext) : ) } + /** + * The part of the window that is covered by the system UI, in physical pixels. Uses the same + * computation as the `onSafeAreaInsetsChange` view prop, applied to the window's decor view. + */ + private fun getWindowSafeAreaInsetsWritableMap(): WritableMap? { + val decorView = reactApplicationContext.currentActivity?.window?.decorView ?: return null + val insets = SafeAreaInsetsObserver.getSafeAreaInsets(decorView) ?: return null + return WritableNativeMap().apply { + putInt("top", insets.top) + putInt("right", insets.right) + putInt("bottom", insets.bottom) + putInt("left", insets.left) + } + } + private fun getPhysicalPixelsWritableMap( displayMetrics: DisplayMetrics, ): WritableMap = diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt index f7ac78bd6ea..f7f1a5eab9d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt @@ -50,7 +50,6 @@ import com.facebook.react.views.drawer.ReactDrawerLayoutManager import com.facebook.react.views.image.ReactImageManager import com.facebook.react.views.modal.ReactModalHostManager import com.facebook.react.views.progressbar.ReactProgressBarViewManager -import com.facebook.react.views.safeareaview.ReactSafeAreaViewManager import com.facebook.react.views.scroll.ReactHorizontalScrollContainerViewManager import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager import com.facebook.react.views.scroll.ReactNestedScrollViewManager @@ -147,7 +146,6 @@ constructor(private val config: MainPackageConfig? = null) : if (ReactNativeFeatureFlags.useNestedScrollViewAndroid()) ReactNestedScrollViewManager() else ReactScrollViewManager(), ReactSwitchManager(), - ReactSafeAreaViewManager(), SwipeRefreshLayoutManager(), // Native equivalents ReactImageManager(), @@ -176,8 +174,6 @@ constructor(private val config: MainPackageConfig? = null) : ModuleSpec.viewManagerSpec { ReactHorizontalScrollContainerViewManager() }, ReactProgressBarViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { ReactProgressBarViewManager() }, - ReactSafeAreaViewManager.REACT_CLASS to - ModuleSpec.viewManagerSpec { ReactSafeAreaViewManager() }, ReactScrollViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { if (ReactNativeFeatureFlags.useNestedScrollViewAndroid()) ReactNestedScrollViewManager() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index d2747ebda57..e3c937145c3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -37,6 +37,7 @@ import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.uimanager.events.FocusEvent; import com.facebook.react.uimanager.events.PointerEventHelper; +import com.facebook.react.uimanager.internal.SafeAreaInsetsObserver; import com.facebook.react.uimanager.style.OutlineStyle; import com.facebook.react.uimanager.util.ReactFindViewUtil; import java.util.ArrayList; @@ -74,6 +75,10 @@ public BaseViewManager(@Nullable ReactApplicationContext reactContext) { @Override protected @Nullable T prepareToRecycleView(@NonNull ThemedReactContext reactContext, T view) { + // Stops safe area observation and clears its tag; the next user of the + // view re-enables it through the prop if needed. + SafeAreaInsetsObserver.setEnabled(view, false); + // Reset tags view.setTag(null); view.setTag(R.id.pointer_events, null); @@ -293,6 +298,15 @@ public void setRenderToHardwareTexture(@NonNull T view, boolean useHWTexture) { view.setTag(R.id.use_hardware_layer, useHWTexture); } + /** + * Views only observe safe area insets while a JavaScript handler is attached, so views that do + * not use the prop are not affected. + */ + @ReactProp(name = ViewProps.ON_SAFE_AREA_INSETS_CHANGE, defaultBoolean = false) + public void setOnSafeAreaInsetsChange(@NonNull T view, boolean onSafeAreaInsetsChange) { + SafeAreaInsetsObserver.setEnabled(view, onSafeAreaInsetsChange); + } + @ReactProp(name = ViewProps.TEST_ID) public void setTestId(@NonNull T view, @Nullable String testId) { view.setTag(R.id.react_test_id, testId); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt index 07d7e132e7d..2fd42a68b38 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt @@ -152,6 +152,7 @@ public object ViewProps { public const val SHADOW_COLOR: String = "shadowColor" public const val Z_INDEX: String = "zIndex" public const val RENDER_TO_HARDWARE_TEXTURE: String = "renderToHardwareTextureAndroid" + public const val ON_SAFE_AREA_INSETS_CHANGE: String = "onSafeAreaInsetsChange" public const val ACCESSIBILITY_LABEL: String = "accessibilityLabel" public const val ACCESSIBILITY_COLLECTION: String = "accessibilityCollection" public const val ACCESSIBILITY_COLLECTION_ITEM: String = "accessibilityCollectionItem" diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/SafeAreaInsetsChangeEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/SafeAreaInsetsChangeEvent.kt new file mode 100644 index 00000000000..fdfa4329633 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/SafeAreaInsetsChangeEvent.kt @@ -0,0 +1,65 @@ +/* + * 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. + */ + +package com.facebook.react.uimanager.events + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.PixelUtil.pxToDp + +/** + * Emitted when the part of a view that is covered by the system UI, or the position of that view in + * the window, changes. + * + * Dispatched synchronously so that the layout depending on the insets is mounted in the frame the + * insets changed in, rather than the one after it. + */ +internal class SafeAreaInsetsChangeEvent( + surfaceId: Int, + viewTag: Int, + private val insetTop: Int, + private val insetRight: Int, + private val insetBottom: Int, + private val insetLeft: Int, + private val frameX: Int, + private val frameY: Int, + private val frameWidth: Int, + private val frameHeight: Int, +) : Event(surfaceId, viewTag) { + + override fun getEventName(): String = EVENT_NAME + + override fun getEventData(): WritableMap = + Arguments.createMap().apply { + putMap( + "insets", + Arguments.createMap().apply { + putDouble("top", insetTop.toDp()) + putDouble("right", insetRight.toDp()) + putDouble("bottom", insetBottom.toDp()) + putDouble("left", insetLeft.toDp()) + }, + ) + putMap( + "frame", + Arguments.createMap().apply { + putDouble("x", frameX.toDp()) + putDouble("y", frameY.toDp()) + putDouble("width", frameWidth.toDp()) + putDouble("height", frameHeight.toDp()) + }, + ) + } + + override fun experimental_isSynchronous(): Boolean = true + + internal companion object { + const val EVENT_NAME: String = "topSafeAreaInsetsChange" + + private fun Int.toDp(): Double = toFloat().pxToDp().toDouble() + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/internal/SafeAreaInsetsObserver.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/internal/SafeAreaInsetsObserver.kt new file mode 100644 index 00000000000..a91d7fb9084 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/internal/SafeAreaInsetsObserver.kt @@ -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. + */ + +package com.facebook.react.uimanager.internal + +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import androidx.core.graphics.Insets +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.facebook.react.R +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.common.UIManagerType +import com.facebook.react.uimanager.common.ViewUtil +import com.facebook.react.uimanager.events.SafeAreaInsetsChangeEvent +import kotlin.math.max +import kotlin.math.min + +/** + * Observes the part of a view that is covered by the system UI, and emits + * [SafeAreaInsetsChangeEvent] whenever it, or the position of the view in the window, changes. + * + * One observer is attached per view that sets the `onSafeAreaInsetsChange` prop. Views without the + * prop never get an observer, and so pay nothing for this. + */ +internal class SafeAreaInsetsObserver private constructor(private val view: View) : + ViewTreeObserver.OnPreDrawListener, View.OnAttachStateChangeListener { + + private var lastInsets: Insets? = null + private var isListening = false + + private fun start() { + view.addOnAttachStateChangeListener(this) + if (view.isAttachedToWindow) { + onViewAttachedToWindow(view) + } + } + + private fun stop() { + view.removeOnAttachStateChangeListener(this) + stopListening() + lastInsets = null + } + + private fun startListening() { + if (!isListening) { + isListening = true + view.viewTreeObserver.addOnPreDrawListener(this) + } + } + + private fun stopListening() { + if (isListening) { + isListening = false + view.viewTreeObserver.removeOnPreDrawListener(this) + } + } + + override fun onViewAttachedToWindow(v: View) { + // The insets and the frame both depend on where the view ends up in the window, which is only + // known once it has been laid out. A pre-draw listener is the cheapest hook that catches every + // change: window insets, layout, and scrolling ancestors alike. + startListening() + maybeEmit() + } + + override fun onViewDetachedFromWindow(v: View) { + stopListening() + } + + override fun onPreDraw(): Boolean { + maybeEmit() + return true + } + + private fun maybeEmit() { + // Only a change of the insets triggers an event. The frame is part of the + // payload but not of the trigger: a view that moves (scrolling, layout) + // without its overlap with the system UI changing stays silent. This is + // what makes observing views safe to place inside scroll views — and it + // prevents feedback loops, since the synchronous render caused by an event + // produces a new frame, which runs this pre-draw listener again. + val insets = getSafeAreaInsets(view) ?: return + if (insets == lastInsets) { + return + } + val frame = getFrame(view) ?: return + val eventDispatcher = + UIManagerHelper.getEventDispatcher(UIManagerHelper.getReactContext(view)) ?: return + // Recorded only once the event is actually dispatched, so a failed lookup + // above does not permanently swallow this inset value. + lastInsets = insets + eventDispatcher.dispatchEvent( + SafeAreaInsetsChangeEvent( + surfaceId = UIManagerHelper.getSurfaceId(view), + viewTag = view.id, + insetTop = insets.top, + insetRight = insets.right, + insetBottom = insets.bottom, + insetLeft = insets.left, + frameX = frame.left, + frameY = frame.top, + frameWidth = frame.width(), + frameHeight = frame.height(), + ), + ) + } + + companion object { + /** + * Starts or stops observing safe area insets for [view]. Safe to call repeatedly with the same + * value. + */ + @JvmStatic + fun setEnabled(view: View, enabled: Boolean) { + if (enabled && ViewUtil.getUIManagerType(view.id) != UIManagerType.FABRIC) { + // The event is only registered with, and dispatched synchronously by, + // the Fabric renderer. + return + } + val existing = view.getTag(R.id.safe_area_insets_observer) as? SafeAreaInsetsObserver + if (enabled == (existing != null)) { + return + } + if (enabled) { + val observer = SafeAreaInsetsObserver(view) + view.setTag(R.id.safe_area_insets_observer, observer) + observer.start() + } else { + view.setTag(R.id.safe_area_insets_observer, null) + existing?.stop() + } + } + + /** + * The insets of the window that overlap [view], in the view's own coordinate space. A view that + * does not reach under the system UI has no insets. + * + * Also used with the window's decor view to report window-level safe area insets through the + * `Dimensions` module. + */ + @JvmStatic + fun getSafeAreaInsets(view: View): Insets? { + // The view has not been laid out yet. + if (view.width == 0 || view.height == 0) { + return null + } + val rootView = view.rootView + val windowInsets = + ViewCompat.getRootWindowInsets(rootView)?.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(), + ) ?: return null + + val visibleRect = Rect() + if (!view.getGlobalVisibleRect(visibleRect)) { + // The view is fully clipped by an ancestor (e.g. scrolled out of a + // scroll view); the rect is undefined in that case, and a view that is + // not visible has no meaningful insets. + return null + } + return Insets.of( + max(windowInsets.left - visibleRect.left, 0), + max(windowInsets.top - visibleRect.top, 0), + max(min(visibleRect.left + view.width - rootView.width, 0) + windowInsets.right, 0), + max(min(visibleRect.top + view.height - rootView.height, 0) + windowInsets.bottom, 0), + ) + } + + /** The frame of [view] in the coordinate space of the window. */ + private fun getFrame(view: View): Rect? { + val rootView = view.rootView as? ViewGroup ?: return null + if (view.parent == null) { + return null + } + val frame = Rect() + view.getDrawingRect(frame) + try { + rootView.offsetDescendantRectToMyCoords(view, frame) + } catch (e: IllegalArgumentException) { + // Thrown when the view is not a descendant of its own root view, which can happen while it + // is being unmounted. + return null + } + return frame + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaView.kt deleted file mode 100644 index 034879ca96e..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaView.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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. - */ - -package com.facebook.react.views.safeareaview - -import android.view.ViewGroup -import androidx.annotation.UiThread -import androidx.core.graphics.Insets -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsCompat.CONSUMED -import com.facebook.react.bridge.GuardedRunnable -import com.facebook.react.bridge.WritableNativeMap -import com.facebook.react.common.build.ReactBuildConfig -import com.facebook.react.uimanager.PixelUtil.pxToDp -import com.facebook.react.uimanager.StateWrapper -import com.facebook.react.uimanager.ThemedReactContext - -internal class ReactSafeAreaView(val reactContext: ThemedReactContext) : ViewGroup(reactContext) { - internal var stateWrapper: StateWrapper? = null - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - - ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsets -> - val insets = - windowInsets.getInsets( - WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(), - ) - updateState(insets) - CONSUMED - } - requestApplyInsets() - } - - override fun onLayout(p0: Boolean, p1: Int, p2: Int, p3: Int, p4: Int): Unit = Unit - - override fun shouldDelayChildPressedState(): Boolean = false - - @Suppress("DEPRECATION") - @UiThread - private fun updateState(insets: Insets) { - val sw = stateWrapper - if (sw != null) { - WritableNativeMap().apply { - putDouble("left", insets.left.toFloat().pxToDp().toDouble()) - putDouble("top", insets.top.toFloat().pxToDp().toDouble()) - putDouble("bottom", insets.bottom.toFloat().pxToDp().toDouble()) - putDouble("right", insets.right.toFloat().pxToDp().toDouble()) - - sw.updateState(this) - } - } else if (!ReactBuildConfig.UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE) { - // paper - reactContext.runOnNativeModulesQueueThread( - object : GuardedRunnable(reactContext) { - override fun runGuarded() { - this@ReactSafeAreaView.reactContext.reactApplicationContext - .getNativeModule(com.facebook.react.uimanager.UIManagerModule::class.java) - ?.updateInsetsPadding(id, insets.top, insets.left, insets.bottom, insets.right) - } - }, - ) - } - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaViewManager.kt deleted file mode 100644 index 4e0c7154b88..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/safeareaview/ReactSafeAreaViewManager.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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. - */ - -package com.facebook.react.views.safeareaview - -import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.uimanager.ReactStylesDiffMap -import com.facebook.react.uimanager.StateWrapper -import com.facebook.react.uimanager.ThemedReactContext -import com.facebook.react.uimanager.ViewGroupManager -import com.facebook.react.uimanager.ViewManagerDelegate -import com.facebook.react.viewmanagers.SafeAreaViewManagerDelegate -import com.facebook.react.viewmanagers.SafeAreaViewManagerInterface - -/** View manager for [ReactSafeAreaView] components. */ -@ReactModule(name = ReactSafeAreaViewManager.REACT_CLASS) -internal class ReactSafeAreaViewManager : - ViewGroupManager(), SafeAreaViewManagerInterface { - - private val delegate: ViewManagerDelegate = SafeAreaViewManagerDelegate(this) - - override fun getDelegate(): ViewManagerDelegate = delegate - - override fun createViewInstance(context: ThemedReactContext): ReactSafeAreaView = - ReactSafeAreaView(context) - - override fun getName(): String = REACT_CLASS - - @Suppress("DEPRECATION") - override fun createShadowNodeInstance(): com.facebook.react.uimanager.LayoutShadowNode = - com.facebook.react.uimanager.LayoutShadowNode() - - @Suppress("DEPRECATION") - override fun getShadowNodeClass(): Class = - com.facebook.react.uimanager.LayoutShadowNode::class.java - - override fun updateState( - view: ReactSafeAreaView, - props: ReactStylesDiffMap, - stateWrapper: StateWrapper, - ): Any? { - view.stateWrapper = stateWrapper - return null - } - - internal companion object { - const val REACT_CLASS: String = "RCTSafeAreaView" - } -} diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index 9fad7aff310..7c80bfc59d1 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -121,7 +121,6 @@ add_react_common_subdir(react/renderer/components/text) add_react_common_subdir(react/renderer/components/unimplementedview) add_react_common_subdir(react/renderer/components/modal) add_react_common_subdir(react/renderer/components/scrollview) -add_react_common_subdir(react/renderer/components/safeareaview) add_react_common_subdir(react/renderer/leakchecker) add_react_common_subdir(react/renderer/observers/events) add_react_common_subdir(react/renderer/observers/intersection) @@ -254,7 +253,6 @@ add_library(reactnative $ $ $ - $ $ $ $ @@ -356,7 +354,6 @@ target_include_directories(reactnative $ $ $ - $ $ $ $ diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt index bdef71c0d4a..09d419ed144 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CMakeLists.txt @@ -54,7 +54,6 @@ target_link_libraries( rrc_modal rrc_progressbar rrc_root - rrc_safeareaview rrc_scrollview rrc_switch rrc_text diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp index 57c1e0e8d86..5275fab0e17 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -58,8 +57,6 @@ void addCoreComponents( concreteComponentDescriptorProvider()); providerRegistry->add( concreteComponentDescriptorProvider()); - providerRegistry->add( - concreteComponentDescriptorProvider()); providerRegistry->add( concreteComponentDescriptorProvider()); providerRegistry->add( diff --git a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index 0e51a358eb7..a4820e5d8da 100644 --- a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -82,4 +82,7 @@ + + + diff --git a/packages/react-native/ReactCommon/React-FabricComponents.podspec b/packages/react-native/ReactCommon/React-FabricComponents.podspec index 3113cbcda62..33a61a5844b 100644 --- a/packages/react-native/ReactCommon/React-FabricComponents.podspec +++ b/packages/react-native/ReactCommon/React-FabricComponents.podspec @@ -94,13 +94,6 @@ Pod::Spec.new do |s| sss.header_dir = "react/renderer/components/modal" end - ss.subspec "safeareaview" do |sss| - sss.source_files = podspec_sources("react/renderer/components/safeareaview/**/*.{m,mm,cpp,h}", "react/renderer/components/safeareaview/**/*.h") - # Exclude tests to avoid conflicts with the react-native-safe-area-context package - sss.exclude_files = "react/renderer/components/safeareaview/tests" - sss.header_dir = "react/renderer/components/safeareaview" - end - ss.subspec "text" do |sss| sss.source_files = podspec_sources(["react/renderer/components/text/*.{m,mm,cpp,h}", "react/renderer/components/text/platform/cxx/**/*.{m,mm,cpp,h}"], diff --git a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/components/safeareaview/CMakeLists.txt deleted file mode 100644 index c32e40803c6..00000000000 --- a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -# 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. - -cmake_minimum_required(VERSION 3.13) -set(CMAKE_VERBOSE_MAKEFILE on) - -include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) - -file(GLOB rrc_safeareaview_SRCS CONFIGURE_DEPENDS *.cpp) - -add_library( - rrc_safeareaview - STATIC - ${rrc_safeareaview_SRCS} -) - -target_include_directories(rrc_safeareaview PUBLIC .) - -target_link_libraries( - rrc_safeareaview - glog - fbjni - folly_runtime - glog_init - react_codegen_rncore - react_debug - react_renderer_componentregistry - react_renderer_core - react_renderer_debug - react_renderer_graphics - react_renderer_uimanager - reactnativejni - rrc_view - yoga -) - -target_compile_reactnative_options(rrc_safeareaview PRIVATE) -target_compile_options(rrc_safeareaview PRIVATE -Wpedantic) diff --git a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewComponentDescriptor.h deleted file mode 100644 index a03c7c968d2..00000000000 --- a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewComponentDescriptor.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { - -/* - * Descriptor for component. - */ -class SafeAreaViewComponentDescriptor final : public ConcreteComponentDescriptor { - using ConcreteComponentDescriptor::ConcreteComponentDescriptor; - void adopt(ShadowNode &shadowNode) const override - { - auto &layoutableShadowNode = static_cast(shadowNode); - auto &stateData = static_cast(*shadowNode.getState()).getData(); - layoutableShadowNode.setPadding(stateData.padding); - - ConcreteComponentDescriptor::adopt(shadowNode); - } -}; - -} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewShadowNode.cpp deleted file mode 100644 index 52a347fba9e..00000000000 --- a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewShadowNode.cpp +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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 "SafeAreaViewShadowNode.h" - -namespace facebook::react { - -// NOLINTNEXTLINE(modernize-avoid-c-arrays) -const char SafeAreaViewComponentName[] = "SafeAreaView"; - -} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewShadowNode.h deleted file mode 100644 index ed073709833..00000000000 --- a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewShadowNode.h +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { - -// NOLINTNEXTLINE(modernize-avoid-c-arrays) -extern const char SafeAreaViewComponentName[]; - -/* - * `ShadowNode` for component. - */ -class SafeAreaViewShadowNode final - : public ConcreteViewShadowNode { - using ConcreteViewShadowNode::ConcreteViewShadowNode; -}; - -} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewState.cpp b/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewState.cpp deleted file mode 100644 index 44b7721404b..00000000000 --- a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewState.cpp +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 "SafeAreaViewState.h" - -namespace facebook::react { - -#ifdef ANDROID -folly::dynamic SafeAreaViewState::getDynamic() const { - return folly::dynamic::object("left", padding.left)("top", padding.top)( - "right", padding.right)("bottom", padding.bottom); -} -#endif - -} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewState.h b/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewState.h deleted file mode 100644 index 7ca383fdb51..00000000000 --- a/packages/react-native/ReactCommon/react/renderer/components/safeareaview/SafeAreaViewState.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 - -#ifdef ANDROID -#include -#endif - -namespace facebook::react { - -/* - * State for component. - */ -class SafeAreaViewState final { - public: -#ifdef ANDROID - SafeAreaViewState() = default; - - SafeAreaViewState(const SafeAreaViewState & /*previousState*/, folly::dynamic data) - : padding( - EdgeInsets{ - (Float)data["left"].getDouble(), - (Float)data["top"].getDouble(), - (Float)data["right"].getDouble(), - (Float)data["bottom"].getDouble(), - }) {}; - - folly::dynamic getDynamic() const; -#endif - - EdgeInsets padding{}; -}; - -} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp index 4e981efd3f8..ef52ed25f75 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp @@ -32,6 +32,42 @@ void BaseViewEventEmitter::onAccessibilityEscape() const { dispatchEvent("accessibilityEscape"); } +#pragma mark - Safe area + +void BaseViewEventEmitter::onSafeAreaInsetsChange( + const EdgeInsets& insets, + const Rect& frame) const { + // Dispatched synchronously and as a discrete event so that React processes it + // before the current frame is presented. Both the thread this is called from + // (the UI thread) and the JavaScript thread are blocked until React has + // finished rendering. + experimental_flushSync([this, &insets, &frame]() { + dispatchEvent( + "safeAreaInsetsChange", + [insets, frame](jsi::Runtime& runtime) { + auto payload = jsi::Object(runtime); + { + auto insetsPayload = jsi::Object(runtime); + insetsPayload.setProperty(runtime, "top", insets.top); + insetsPayload.setProperty(runtime, "right", insets.right); + insetsPayload.setProperty(runtime, "bottom", insets.bottom); + insetsPayload.setProperty(runtime, "left", insets.left); + payload.setProperty(runtime, "insets", insetsPayload); + } + { + auto framePayload = jsi::Object(runtime); + framePayload.setProperty(runtime, "x", frame.origin.x); + framePayload.setProperty(runtime, "y", frame.origin.y); + framePayload.setProperty(runtime, "width", frame.size.width); + framePayload.setProperty(runtime, "height", frame.size.height); + payload.setProperty(runtime, "frame", framePayload); + } + return payload; + }, + RawEvent::Category::Discrete); + }); +} + #pragma mark - Layout void BaseViewEventEmitter::onLayout(const LayoutMetrics& layoutMetrics) const { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.h index e556d1e9754..b38a8e8e592 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.h @@ -12,6 +12,7 @@ #include #include +#include #include "TouchEventEmitter.h" @@ -32,6 +33,19 @@ class BaseViewEventEmitter : public TouchEventEmitter { void onLayout(const LayoutMetrics &layoutMetrics) const; +#pragma mark - Safe area + + /* + * Emits `onSafeAreaInsetsChange` with the portion of the view that is covered + * by the system UI (status bar, home indicator, display cutouts, ...) and the + * frame of the view at the time of the event. + * + * The event is dispatched synchronously, blocking the thread it is called + * from until React has re-rendered, so that the layout that depends on the + * insets is mounted in the same frame the insets changed in. + */ + void onSafeAreaInsetsChange(const EdgeInsets &insets, const Rect &frame) const; + #pragma mark - Focus void onFocus() const; void onBlur() const; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp index 4f8e4265d89..a313de1a618 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -288,6 +288,12 @@ BaseViewProps::BaseViewProps( "onLayout", sourceProps.onLayout, {})), + onSafeAreaInsetsChange(convertRawProp( + context, + rawProps, + "onSafeAreaInsetsChange", + sourceProps.onSafeAreaInsetsChange, + {})), events(convertRawProp(context, rawProps, sourceProps.events, {})), collapsable(convertRawProp( context, @@ -355,6 +361,7 @@ void BaseViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(isolation); RAW_SET_PROP_SWITCH_CASE_BASIC(hitSlop); RAW_SET_PROP_SWITCH_CASE_BASIC(onLayout); + RAW_SET_PROP_SWITCH_CASE_BASIC(onSafeAreaInsetsChange); RAW_SET_PROP_SWITCH_CASE_BASIC(collapsable); RAW_SET_PROP_SWITCH_CASE_BASIC(collapsableChildren); RAW_SET_PROP_SWITCH_CASE_BASIC(removeClippedSubviews); @@ -591,6 +598,10 @@ SharedDebugStringConvertibleList BaseViewProps::getDebugProps() const { "backgroundImage", backgroundImage, defaultBaseViewProps.backgroundImage), + debugStringConvertibleItem( + "onSafeAreaInsetsChange", + onSafeAreaInsetsChange, + defaultBaseViewProps.onSafeAreaInsetsChange), }; } #endif diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h index 7554ba7cad8..5759c582c37 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -101,6 +101,7 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { PointerEventsMode pointerEvents{}; EdgeInsets hitSlop{}; bool onLayout{}; + bool onSafeAreaInsetsChange{}; ViewEvents events{}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index a166a90546c..9567c79c817 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -55,6 +55,8 @@ void ViewShadowNode::initialize() noexcept { viewProps.accessibilityViewIsModal || viewProps.importantForAccessibility != ImportantForAccessibility::Auto || viewProps.removeClippedSubviews || viewProps.cursor != Cursor::Auto || + // Observing the safe area requires a host view to observe with. + viewProps.onSafeAreaInsetsChange || !viewProps.filter.empty() || viewProps.mixBlendMode != BlendMode::Normal || viewProps.isolation == Isolation::Isolate || diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp index 32289a2c43f..3611166c683 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp @@ -563,6 +563,10 @@ folly::dynamic HostPlatformViewProps::getDiffProps( result["onLayout"] = onLayout; } + if (onSafeAreaInsetsChange != oldProps->onSafeAreaInsetsChange) { + result["onSafeAreaInsetsChange"] = onSafeAreaInsetsChange; + } + if (zIndex != oldProps->zIndex) { result["zIndex"] = zIndex.has_value() ? zIndex.value() : folly::dynamic(nullptr); diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp index cdb05f4719f..839e2f19f90 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.cpp @@ -53,7 +53,14 @@ void EventBeat::induce() const { isEventBeatRequested_ = false; if (isBeatCallbackScheduled_) { - return; + // An asynchronous beat is already scheduled but has not run yet. A + // synchronous request must not be stranded behind it (it would silently + // lose its this-frame guarantee, and the leftover flag would make an + // unrelated later beat blocking), so it proceeds and processes the queue + // now; the already scheduled beat will simply find an empty queue. + if (!isSynchronousRequested_) { + return; + } } isBeatCallbackScheduled_ = true; diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h index 3740b457e78..b8da33cc9ac 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventBeat.h @@ -111,6 +111,16 @@ class EventBeat { */ virtual void requestSynchronous() const; + /* + * Induces the next beat to happen as soon as possible. + * Receiver might ignore the call if a beat was not requested. + * + * Ordinarily called by the platform once per frame; also callable by a + * consumer right after `requestSynchronous` to process the queue immediately + * at the call site instead of at the next frame boundary. + */ + void induce() const; + /* * The callback will be executed once a consumer (for example EventQueue) * calls either `EventBeat::request` or `EventBeat::requestSynchronous`. The @@ -128,12 +138,6 @@ class EventBeat { void unstable_setInduceCallback(std::function callback); protected: - /* - * Induces the next beat to happen as soon as possible. - * Receiver might ignore the call if a beat was not requested. - */ - void induce() const; - BeatCallback beatCallback_; std::function induceCallback_; std::shared_ptr ownerBox_; diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/EventBeatTest.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/EventBeatTest.cpp new file mode 100644 index 00000000000..1dda2fd47fd --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/tests/EventBeatTest.cpp @@ -0,0 +1,168 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include + +#include "StubQueue.h" + +namespace facebook::react { + +class EventBeatTestFeatureFlags : public ReactNativeFeatureFlagsDefaults { + public: + bool enableBridgelessArchitecture() override { + return true; + } +}; + +class EventBeatTest : public testing::Test { + protected: + void SetUp() override { + ReactNativeFeatureFlags::dangerouslyReset(); + ReactNativeFeatureFlags::override( + std::make_unique()); + + runtime_ = facebook::hermes::makeHermesRuntime( + ::hermes::vm::RuntimeConfig::Builder().build()); + stubQueue_ = std::make_unique(); + + RuntimeExecutor runtimeExecutor = + [this]( + std::function&& callback) { + stubQueue_->runOnQueue([this, callback = std::move(callback)]() { + callback(*runtime_); + }); + }; + + runtimeScheduler_ = std::make_unique(runtimeExecutor); + + ownerBox_ = std::make_shared(); + owner_ = std::make_shared(0); + ownerBox_->owner = owner_; + eventBeat_ = std::make_unique(ownerBox_, *runtimeScheduler_); + } + + void TearDown() override { + ReactNativeFeatureFlags::dangerouslyReset(); + } + + std::unique_ptr runtime_; + std::unique_ptr stubQueue_; + std::unique_ptr runtimeScheduler_; + std::shared_ptr ownerBox_; + std::shared_ptr owner_; + std::unique_ptr eventBeat_; +}; + +TEST_F(EventBeatTest, induceWithoutRequestIsNoop) { + int beatCount = 0; + eventBeat_->setBeatCallback([&beatCount](jsi::Runtime& /*runtime*/) { + beatCount++; + }); + + eventBeat_->induce(); + + EXPECT_EQ(beatCount, 0); + EXPECT_EQ(stubQueue_->size(), 0); +} + +TEST_F(EventBeatTest, synchronousRequestIsProcessedAtInduce) { + int beatCount = 0; + eventBeat_->setBeatCallback([&beatCount](jsi::Runtime& /*runtime*/) { + beatCount++; + }); + + eventBeat_->requestSynchronous(); + EXPECT_EQ(beatCount, 0); + + // Platform implementations induce the beat at a point where the effects of + // synchronous events can still make the current frame (the display phase on + // Apple, before the draw on Android). The beat callback runs synchronously + // before `induce` returns, with both threads blocked. + std::thread driver([this]() { + stubQueue_->waitForTask(); + stubQueue_->tick(); + }); + eventBeat_->induce(); + driver.join(); + + EXPECT_EQ(beatCount, 1); + + // The request was consumed: another induce does nothing. + eventBeat_->induce(); + EXPECT_EQ(beatCount, 1); + EXPECT_EQ(stubQueue_->size(), 0); +} + +TEST_F(EventBeatTest, requestMadeDuringBeatIsProcessedByASubsequentInduce) { + int beatCount = 0; + eventBeat_->setBeatCallback([&](jsi::Runtime& /*runtime*/) { + beatCount++; + if (beatCount == 1) { + // A synchronous request made from within the beat (e.g. an event whose + // handler causes another synchronous event). Platform implementations + // defer the induce for it (the display phase flusher on Apple, the next + // pre-draw on Android) rather than inducing from within the beat. + eventBeat_->requestSynchronous(); + } + }); + + eventBeat_->requestSynchronous(); + std::thread driver([this]() { + stubQueue_->waitForTask(); + stubQueue_->tick(); + }); + eventBeat_->induce(); + driver.join(); + + EXPECT_EQ(beatCount, 1); + + // The request made during the beat is not lost: the next induce processes + // it. + std::thread driver2([this]() { + stubQueue_->waitForTask(); + stubQueue_->tick(); + }); + eventBeat_->induce(); + driver2.join(); + + EXPECT_EQ(beatCount, 2); +} + +TEST_F(EventBeatTest, synchronousRequestIsNotStrandedBehindScheduledBeat) { + int beatCount = 0; + eventBeat_->setBeatCallback([&beatCount](jsi::Runtime& /*runtime*/) { + beatCount++; + }); + + // An asynchronous beat is scheduled but has not run yet. + eventBeat_->request(); + eventBeat_->induce(); + EXPECT_EQ(beatCount, 0); + + // A synchronous request arriving now must still be processed by its induce + // instead of being silently deferred behind the scheduled beat. + eventBeat_->requestSynchronous(); + std::thread driver([this]() { + stubQueue_->waitForTask(); + stubQueue_->tick(); + stubQueue_->waitForTask(); + stubQueue_->tick(); + }); + eventBeat_->induce(); + driver.join(); + + EXPECT_EQ(beatCount, 2); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h b/packages/react-native/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h index c750cf97152..05cbf8e7370 100644 --- a/packages/react-native/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h +++ b/packages/react-native/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h @@ -12,9 +12,18 @@ namespace facebook::react { -using DisplayMetrics = NativeDeviceInfoDisplayMetrics; +using WindowSafeAreaInsets = NativeDeviceInfoWindowSafeAreaInsets; -using DisplayMetricsAndroid = NativeDeviceInfoDisplayMetricsAndroid; +using DisplayMetrics = + NativeDeviceInfoDisplayMetrics>; + +using DisplayMetricsAndroid = NativeDeviceInfoDisplayMetricsAndroid< + double, + double, + double, + double, + double, + std::optional>; using DimensionsPayload = NativeDeviceInfoDimensionsPayload< std::optional, @@ -25,6 +34,9 @@ using DimensionsPayload = NativeDeviceInfoDimensionsPayload< using DeviceInfoConstants = NativeDeviceInfoDeviceInfoConstants, std::optional>; +template <> +struct Bridging : NativeDeviceInfoWindowSafeAreaInsetsBridging {}; + template <> struct Bridging : NativeDeviceInfoDisplayMetricsBridging {}; diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 5220c72e26e..040238f66bd 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4406ddb15814262a7ccc2b56e9625d67>> + * @generated SignedSource<> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -357,12 +357,6 @@ declare const requireNativeComponent_default: ( uiViewClassName: string, ) => HostComponent declare const RootTagContext: React.Context -declare const SafeAreaView: typeof SafeAreaView_default -declare const SafeAreaView_default: ( - props: ViewProps & { - ref?: React.Ref - }, -) => React.ReactNode declare const ScrollView: typeof ScrollViewWrapper & ScrollViewComponentStatics declare const ScrollViewContext_default: React.Context declare const ScrollViewWrapper: ( @@ -1867,10 +1861,14 @@ declare type DirectEventProps = { readonly onAccessibilityTap?: () => unknown readonly onLayout?: (event: LayoutChangeEvent) => unknown readonly onMagicTap?: () => unknown + readonly onSafeAreaInsetsChange?: ( + event: SafeAreaInsetsChangeEvent, + ) => unknown } declare type DisplayMetrics = { fontScale: number height: number + readonly safeAreaInsets?: WindowSafeAreaInsets scale: number width: number } @@ -1878,6 +1876,7 @@ declare type DisplayMetricsAndroid = { densityDpi: number fontScale: number height: number + readonly safeAreaInsets?: WindowSafeAreaInsets scale: number width: number } @@ -4208,8 +4207,16 @@ declare type Runnable = ( declare type Runnables = { [appKey: string]: Runnable } -declare type SafeAreaView = typeof SafeAreaView -declare type SafeAreaViewInstance = HostInstance +declare type SafeAreaInsets = { + readonly bottom: number + readonly left: number + readonly right: number + readonly top: number +} +declare type SafeAreaInsetsChangeEvent = NativeSyntheticEvent<{ + readonly frame: LayoutRectangle + readonly insets: SafeAreaInsets +}> declare type ScaledSize = DisplayMetrics declare type ScheduleLocalNotificationDetails = PresentLocalNotificationDetails & { @@ -5650,6 +5657,12 @@ declare type WebPlatform = { get isTV(): boolean get Version(): string } +declare type WindowSafeAreaInsets = { + bottom: number + left: number + right: number + top: number +} declare type WindowsPlatform = { OS: "windows" select: (spec: PlatformSelectSpec) => T @@ -5716,16 +5729,16 @@ export { AccessibilityValue, // cf8bcb74 ActionSheetIOS, // b558559e ActionSheetIOSOptions, // 1756eb5a - ActivityIndicator, // e43c68bb + ActivityIndicator, // cb3baebf ActivityIndicatorInstance, // a82dd4e7 - ActivityIndicatorProps, // 619842eb + ActivityIndicatorProps, // c9fb2776 Alert, // a398a509 AlertButton, // bf1a3b60 AlertButtonStyle, // ec9fb242 AlertOptions, // 8a116d2a AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // d74ec583 + Animated, // ef96069a AppConfig, // 35c0ca70 AppRegistry, // 1e8c5a00 AppState, // 12012be5 @@ -5752,16 +5765,16 @@ export { DevMenu, // 21b8b7a9 DevSettings, // e55b91dc DeviceEventEmitter, // bdbdd014 - DeviceInfo, // 521bfb71 - DeviceInfoConstants, // 279e7858 + DeviceInfo, // db26f5e4 + DeviceInfoConstants, // 148c17ed DimensionValue, // b163a381 - Dimensions, // 980ef68c - DimensionsPayload, // 653bc26c - DisplayMetrics, // 1dc35cef - DisplayMetricsAndroid, // 872e62eb - DrawerLayoutAndroid, // 7843b7b5 + Dimensions, // 6e9b417f + DimensionsPayload, // 8251f7f2 + DisplayMetrics, // 5801c350 + DisplayMetricsAndroid, // 5ad0b971 + DrawerLayoutAndroid, // 9789fb75 DrawerLayoutAndroidInstance, // c0694352 - DrawerLayoutAndroidProps, // 0c1d6155 + DrawerLayoutAndroidProps, // 2a3b9a3e DrawerSlideEvent, // c4ab8fba DropShadowValue, // e9df2606 DynamicColorIOS, // d96c228c @@ -5777,9 +5790,9 @@ export { EventSubscription, // b8d084aa ExtendedExceptionData, // 5a6ccf5a FilterFunction, // bf24c0e3 - FlatList, // e1b005c7 - FlatListInstance, // 2d1d8e45 - FlatListProps, // 94fa2dc7 + FlatList, // f7dfb7bf + FlatListInstance, // 4cde6870 + FlatListProps, // eb05aa31 FocusEvent, // 850f1517 FontVariant, // 7c7558bb GestureResponderEvent, // 14d3e77a @@ -5790,17 +5803,17 @@ export { IEventEmitter, // fbef6131 IOSKeyboardEvent, // e67bfe3a IgnorePattern, // ec6f6ece - Image, // 87b793a4 - ImageBackground, // abfd6c1d - ImageBackgroundInstance, // 50bc4cb3 - ImageBackgroundProps, // e04f173a + Image, // 4845ad61 + ImageBackground, // 601e01e7 + ImageBackgroundInstance, // 3bb44053 + ImageBackgroundProps, // 46039e37 ImageErrorEvent, // 978933f4 ImageInstance, // 9a100753 ImageLoadEvent, // 77f0b718 ImageProgressEventIOS, // 445331a4 - ImageProps, // c50cfe0d + ImageProps, // b95ac16f ImagePropsAndroid, // ee00e1d5 - ImagePropsBase, // fa3f8e21 + ImagePropsBase, // 54b7d251 ImagePropsIOS, // 9e19c85d ImageRequireSource, // 681d683b ImageResizeMode, // d51106e2 @@ -5818,9 +5831,9 @@ export { KeyEvent, // 20fa4267 KeyUpEvent, // 57f832c5 Keyboard, // 49414c97 - KeyboardAvoidingView, // fbf69b90 - KeyboardAvoidingViewInstance, // f6c63fc9 - KeyboardAvoidingViewProps, // 8609524c + KeyboardAvoidingView, // 3f0523c3 + KeyboardAvoidingViewInstance, // 668649df + KeyboardAvoidingViewProps, // b3f0f986 KeyboardEvent, // c3f895d4 KeyboardEventEasing, // af4091c8 KeyboardEventName, // 59299ad6 @@ -5845,10 +5858,10 @@ export { MeasureInWindowOnSuccessCallback, // a285f598 MeasureLayoutOnSuccessCallback, // 3592502a MeasureOnSuccessCallback, // 82824e59 - Modal, // 252d448a + Modal, // 1fcfee47 ModalBaseProps, // c294cc46 ModalInstance, // d466ce77 - ModalProps, // 757e7cec + ModalProps, // 84f2e72f ModalPropsAndroid, // 515fb173 ModalPropsIOS, // 664ecb7e ModeChangeEvent, // f64bf69d @@ -5884,15 +5897,15 @@ export { PointerEvent, // ff599afe PressabilityConfig, // fea539a5 PressabilityEventHandlers, // c222648b - Pressable, // ed5391ac + Pressable, // 771ba8f0 PressableAndroidRippleConfig, // ee32eaca PressableInstance, // eebfe911 - PressableProps, // d6bfa55b + PressableProps, // b16dba85 PressableStateCallbackType, // 9af36561 ProcessedColorValue, // 33f74304 - ProgressBarAndroid, // 2dd6dbfd + ProgressBarAndroid, // 7e4aae87 ProgressBarAndroidInstance, // ab545ef1 - ProgressBarAndroidProps, // 07fa83a1 + ProgressBarAndroidProps, // 4a37f451 PublicRootInstance, // 8040afd7 PublicTextInstance, // 6937c7bf PushNotificationEventName, // 84e7e150 @@ -5900,9 +5913,9 @@ export { PushNotificationPermissions, // c2e7ae4f Rationale, // 5df1b1c1 ReactNativeVersion, // abd76827 - RefreshControl, // 947cb880 - RefreshControlInstance, // 55f1814a - RefreshControlProps, // eb2b1cbe + RefreshControl, // f51d7fa6 + RefreshControlInstance, // e3123c95 + RefreshControlProps, // 3b735108 RefreshControlPropsAndroid, // 8ac931ca RefreshControlPropsIOS, // 72a36381 Registry, // 6c39216d @@ -5914,24 +5927,22 @@ export { RootViewStyleProvider, // 8792d506 Runnable, // 594dd93a Runnables, // 4367c557 - SafeAreaView, // 9ce03f75 - SafeAreaViewInstance, // 21dba39c - ScaledSize, // 07e417c7 + ScaledSize, // 5e5527f8 ScrollEvent, // d7abdd0a - ScrollResponderType, // ba188eae + ScrollResponderType, // 6ffba565 ScrollToLocationParamsType, // d7ecdad1 - ScrollView, // 066a8597 - ScrollViewImperativeMethods, // 904c66fd - ScrollViewInstance, // ccf4f341 - ScrollViewProps, // b62913d1 + ScrollView, // ab8c8f76 + ScrollViewImperativeMethods, // 440145ad + ScrollViewInstance, // 17fa4b20 + ScrollViewProps, // 48286a63 ScrollViewPropsAndroid, // 02f3df2e ScrollViewPropsIOS, // 807cb4f6 ScrollViewScrollToOptions, // 3313411e SectionBase, // 9f13db00 - SectionList, // ee3e7972 + SectionList, // f865a7b9 SectionListData, // 1a4de01a - SectionListInstance, // c9b991fe - SectionListProps, // 0e933318 + SectionListInstance, // e953047b + SectionListProps, // 2866c531 SectionListRenderItem, // 715b2086 SectionListRenderItemInfo, // 4a48a922 Separators, // 6a45f7e3 @@ -5950,17 +5961,17 @@ export { StyleProp, // fa0e9b4a StyleSheet, // 3c21ec63 SubmitBehavior, // c4ddf490 - Switch, // f495bab3 + Switch, // d891ccdc SwitchChangeEvent, // 899635b1 SwitchInstance, // 3c50eec5 - SwitchProps, // cadfd0c4 + SwitchProps, // c4f737fb Systrace, // 626d178c TVViewPropsIOS, // 330ce7b5 TargetedEvent, // 16e98910 TaskProvider, // 266dedf2 Text, // 09e783b9 TextContentType, // 239b3ecc - TextInput, // a2cc82d1 + TextInput, // 169cba98 TextInputAndroidProps, // 9ebbc103 TextInputBlurEvent, // b77af40e TextInputChangeEvent, // f55eef98 @@ -5970,7 +5981,7 @@ export { TextInputIOSProps, // fb3c9327 TextInputInstance, // 5a0c0e0d TextInputKeyPressEvent, // 546c5d07 - TextInputProps, // dd91ebaa + TextInputProps, // 5cbc9768 TextInputSelectionChangeEvent, // e58f2abc TextInputSubmitEditingEvent, // 6bcb2aa5 TextInstance, // 05463a96 @@ -5995,19 +6006,19 @@ export { UIManager, // afbcdf05 UTFSequence, // ad625158 Vibration, // 31e4bbf8 - View, // 3e72139a + View, // 54df4cf8 ViewInstance, // ffde5573 - ViewProps, // 763271bb + ViewProps, // 16a288a1 ViewPropsAndroid, // 55e81851 ViewPropsIOS, // 58ee19bf ViewStyle, // e45056b1 VirtualViewMode, // 6be59722 VirtualizedList, // 68c7345e VirtualizedListInstance, // 423ee7c0 - VirtualizedListProps, // f51f4a42 + VirtualizedListProps, // 214bc0cd VirtualizedSectionList, // 9fd9cd61 VirtualizedSectionListInstance, // 12b706d5 - VirtualizedSectionListProps, // 8210f30a + VirtualizedSectionListProps, // fd63943b WrapperComponentProvider, // 9ef54e61 codegenNativeCommands, // 628a7c0a codegenNativeComponent, // 32a1bca6 @@ -6020,5 +6031,5 @@ export { useAnimatedValueXY, // 04c70878 useColorScheme, // d585efdb usePressability, // 782138ed - useWindowDimensions, // bb4b683f + useWindowDimensions, // 63544292 } diff --git a/packages/react-native/__typetests__/index.tsx b/packages/react-native/__typetests__/index.tsx index 54ea8b7694b..c18254841f4 100644 --- a/packages/react-native/__typetests__/index.tsx +++ b/packages/react-native/__typetests__/index.tsx @@ -115,7 +115,6 @@ import { UIManager, View, ViewStyle, - SafeAreaView, VirtualizedList, findNodeHandle, requireNativeComponent, @@ -1576,31 +1575,6 @@ class BridgedComponentTest extends React.Component { } } -const SafeAreaViewTest = () => { - const viewRef = React.createRef>(); - - return ( - <> - ; - ; - { - ref?.focus(); - ref?.blur(); - ref?.measure( - (x, y, width, height, pageX, pageY): number => - x + y + width + height + pageX + pageY, - ); - ref?.measureInWindow( - (x, y, width, height): number => x + y + width + height, - ); - ref?.setNativeProps({focusable: false}); - }} - /> - - ); -}; - const SwitchRefTest = () => { const switchRef = React.createRef>(); diff --git a/packages/react-native/index.js b/packages/react-native/index.js index 80fb9723e4f..82484615e12 100644 --- a/packages/react-native/index.js +++ b/packages/react-native/index.js @@ -118,20 +118,6 @@ module.exports = { return require('./Libraries/Components/RefreshControl/RefreshControl') .default; }, - /** - * @deprecated SafeAreaView has been deprecated and will be removed in a future release. - * Please use 'react-native-safe-area-context' instead. - * See https://github.com/AppAndFlow/react-native-safe-area-context - */ - get SafeAreaView() { - warnOnce( - 'safe-area-view-deprecated', - 'SafeAreaView has been deprecated and will be removed in a future release. ' + - "Please use 'react-native-safe-area-context' instead. " + - 'See https://github.com/AppAndFlow/react-native-safe-area-context', - ); - return require('./Libraries/Components/SafeAreaView/SafeAreaView').default; - }, get ScrollView() { return require('./Libraries/Components/ScrollView/ScrollView').default; }, diff --git a/packages/react-native/index.js.flow b/packages/react-native/index.js.flow index 51e4a54e055..94c34b11ef5 100644 --- a/packages/react-native/index.js.flow +++ b/packages/react-native/index.js.flow @@ -108,9 +108,6 @@ export type { } from './Libraries/Components/RefreshControl/RefreshControl'; export {default as RefreshControl} from './Libraries/Components/RefreshControl/RefreshControl'; -export type {SafeAreaViewInstance} from './Libraries/Components/SafeAreaView/SafeAreaView'; -export {default as SafeAreaView} from './Libraries/Components/SafeAreaView/SafeAreaView'; - export type { ScrollViewImperativeMethods, ScrollViewInstance, diff --git a/packages/react-native/scripts/ios-prebuild/headers-config.js b/packages/react-native/scripts/ios-prebuild/headers-config.js index b5534b4054e..8afffa0baf0 100644 --- a/packages/react-native/scripts/ios-prebuild/headers-config.js +++ b/packages/react-native/scripts/ios-prebuild/headers-config.js @@ -326,13 +326,6 @@ const PodspecExceptions /*: {[key: string]: PodSpecConfiguration} */ = { headerDir: 'react/renderer/components/modal', }, - { - name: 'safeareaview', - headerPatterns: ['react/renderer/components/safeareaview/**/*.h'], - excludePatterns: ['react/renderer/components/safeareaview/tests'], - headerDir: 'react/renderer/components/safeareaview', - }, - { name: 'scrollview', headerPatterns: [ diff --git a/packages/react-native/src/private/components/safeareaview/SafeAreaView.js b/packages/react-native/src/private/components/safeareaview/SafeAreaView.js new file mode 100644 index 00000000000..631da1e73ff --- /dev/null +++ b/packages/react-native/src/private/components/safeareaview/SafeAreaView.js @@ -0,0 +1,75 @@ +/** + * 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 type {ViewProps} from '../../../../Libraries/Components/View/ViewPropTypes'; +import type { + SafeAreaInsets, + SafeAreaInsetsChangeEvent, +} from '../../../../Libraries/Types/CoreEventTypes'; +import type {HostInstance} from '../../types/HostInstance'; + +import View from '../../../../Libraries/Components/View/View'; +import Dimensions from '../../../../Libraries/Utilities/Dimensions'; +import * as React from 'react'; +import {useCallback, useMemo, useState} from 'react'; + +/** + * Renders its children within the safe area of the device, by applying the part + * of the view that is covered by the system UI as padding. + * + * This is the internal counterpart of `react-native-safe-area-context`, for the + * few surfaces React Native renders itself (LogBox, the element inspector, ...) + * which cannot take a dependency on it. Everything else should use the library. + */ +component SafeAreaView( + ref?: React.RefSetter, + ...props: ViewProps +) { + const {style, onSafeAreaInsetsChange, ...otherProps} = props; + // Seeded with the window insets so the first frame is already padded; the + // synchronous event then keeps them correct relative to this view. The seed + // is only exact for views aligned with the window edges, which the internal + // surfaces using this component (LogBox, the element inspector) are. + const [insets, setInsets] = useState( + () => Dimensions.get('window').safeAreaInsets, + ); + + const handleSafeAreaInsetsChange = useCallback( + (event: SafeAreaInsetsChangeEvent) => { + setInsets(event.nativeEvent.insets); + onSafeAreaInsetsChange?.(event); + }, + [onSafeAreaInsetsChange], + ); + + const paddingStyle = useMemo( + () => + insets == null + ? null + : { + paddingTop: insets.top, + paddingRight: insets.right, + paddingBottom: insets.bottom, + paddingLeft: insets.left, + }, + [insets], + ); + + return ( + + ); +} + +export default SafeAreaView; diff --git a/packages/react-native/src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE.js b/packages/react-native/src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE.js deleted file mode 100644 index e6b5cdd5f52..00000000000 --- a/packages/react-native/src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * 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 type {ViewProps} from '../../../../Libraries/Components/View/ViewPropTypes'; - -import View from '../../../../Libraries/Components/View/View'; -import UIManager from '../../../../Libraries/ReactNative/UIManager'; -import Platform from '../../../../Libraries/Utilities/Platform'; -import * as React from 'react'; - -const exported: component( - ref?: React.RefSetter>, - ...ViewProps -) = Platform.select({ - ios: require('../../../../src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent') - .default, - android: UIManager.hasViewManagerConfig('RCTSafeAreaView') - ? require('../../../../src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent') - .default - : View, - default: View, -}); - -export default exported; diff --git a/packages/react-native/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-itest.js b/packages/react-native/src/private/components/safeareaview/__tests__/SafeAreaView-itest.js similarity index 89% rename from packages/react-native/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-itest.js rename to packages/react-native/src/private/components/safeareaview/__tests__/SafeAreaView-itest.js index 0fcae9f2e28..aee21a7fc8a 100644 --- a/packages/react-native/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-itest.js +++ b/packages/react-native/src/private/components/safeareaview/__tests__/SafeAreaView-itest.js @@ -12,7 +12,8 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import * as Fantom from '@react-native/fantom'; import * as React from 'react'; -import {SafeAreaView, Text} from 'react-native'; +import SafeAreaView from 'react-native/src/private/components/safeareaview/SafeAreaView'; +import {Text} from 'react-native'; describe('', () => { it('renders with children', () => { diff --git a/packages/react-native/src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent.js b/packages/react-native/src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent.js deleted file mode 100644 index 44493701109..00000000000 --- a/packages/react-native/src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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 type {ViewProps} from '../../../../../Libraries/Components/View/ViewPropTypes'; -import type {HostComponent} from '../../../types/HostComponent'; - -import codegenNativeComponent from '../../../../../Libraries/Utilities/codegenNativeComponent'; - -type RCTSafeAreaViewNativeProps = Readonly<{ - ...ViewProps, - - // No props -}>; - -export default codegenNativeComponent( - 'SafeAreaView', - { - paperComponentName: 'RCTSafeAreaView', - interfaceOnly: true, - }, -) as HostComponent; diff --git a/packages/react-native/src/private/devsupport/devmenu/elementinspector/Inspector.js b/packages/react-native/src/private/devsupport/devmenu/elementinspector/Inspector.js index 58ec505cad2..652d4d87703 100644 --- a/packages/react-native/src/private/devsupport/devmenu/elementinspector/Inspector.js +++ b/packages/react-native/src/private/devsupport/devmenu/elementinspector/Inspector.js @@ -18,7 +18,7 @@ import type { import type {ViewStyleProp} from '../../../../../Libraries/StyleSheet/StyleSheet'; import type {ReactDevToolsAgent} from '../../../../../Libraries/Types/ReactDevToolsTypes'; -import SafeAreaView from '../../../components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE'; +import SafeAreaView from '../../../components/safeareaview/SafeAreaView'; import * as React from 'react'; const View = require('../../../../../Libraries/Components/View/View').default; diff --git a/packages/react-native/src/private/devsupport/devmenu/elementinspector/InspectorPanel.js b/packages/react-native/src/private/devsupport/devmenu/elementinspector/InspectorPanel.js index 9e5d04f3003..d5756ac7bad 100644 --- a/packages/react-native/src/private/devsupport/devmenu/elementinspector/InspectorPanel.js +++ b/packages/react-native/src/private/devsupport/devmenu/elementinspector/InspectorPanel.js @@ -12,7 +12,7 @@ import type {ElementsHierarchy, InspectedElement} from './Inspector'; -import SafeAreaView from '../../../../../Libraries/Components/SafeAreaView/SafeAreaView'; +import SafeAreaView from '../../../components/safeareaview/SafeAreaView'; import * as React from 'react'; const ScrollView = diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js index df2f70c62de..8f53910fcae 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js @@ -12,12 +12,20 @@ import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry'; +export type WindowSafeAreaInsets = { + top: number, + right: number, + bottom: number, + left: number, +}; + export type DisplayMetricsAndroid = { width: number, height: number, scale: number, fontScale: number, densityDpi: number, + readonly safeAreaInsets?: WindowSafeAreaInsets, }; export type DisplayMetrics = { @@ -25,6 +33,7 @@ export type DisplayMetrics = { height: number, scale: number, fontScale: number, + readonly safeAreaInsets?: WindowSafeAreaInsets, }; export type DimensionsPayload = { diff --git a/packages/react-native/types_DEPRECATED/Libraries/Components/SafeAreaView/SafeAreaView.d.ts b/packages/react-native/types_DEPRECATED/Libraries/Components/SafeAreaView/SafeAreaView.d.ts deleted file mode 100644 index 590928373da..00000000000 --- a/packages/react-native/types_DEPRECATED/Libraries/Components/SafeAreaView/SafeAreaView.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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 - */ - -import type * as React from 'react'; -import {Constructor} from '../../../private/Utilities'; -import {HostInstance} from '../../../public/ReactNativeTypes'; -import {ViewProps} from '../View/ViewPropTypes'; - -/** - * @deprecated - * Use `react-native-safe-area-context` instead. This component is deprecated and will be removed in a future release. - * - * Renders nested content and automatically applies paddings reflect the portion of the view - * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. - * Moreover, and most importantly, Safe Area's paddings reflect physical limitation of the screen, - * such as rounded corners or camera notches (aka sensor housing area on iPhone X). - */ -declare class SafeAreaViewComponent extends React.Component {} - -declare const SafeAreaViewBase: Constructor & - typeof SafeAreaViewComponent; - -/** - * @deprecated - * Use `react-native-safe-area-context` instead. This component is deprecated and will be removed in a future release. - */ -export class SafeAreaView extends SafeAreaViewBase {} diff --git a/packages/react-native/types_DEPRECATED/index.d.ts b/packages/react-native/types_DEPRECATED/index.d.ts index b5541260cb6..219f5ae9152 100644 --- a/packages/react-native/types_DEPRECATED/index.d.ts +++ b/packages/react-native/types_DEPRECATED/index.d.ts @@ -89,7 +89,6 @@ export * from './Libraries/Components/LayoutConformance/LayoutConformance'; export * from './Libraries/Components/Pressable/Pressable'; export * from './Libraries/Components/ProgressBarAndroid/ProgressBarAndroid'; export * from './Libraries/Components/RefreshControl/RefreshControl'; -export * from './Libraries/Components/SafeAreaView/SafeAreaView'; export * from './Libraries/Components/ScrollView/ScrollView'; export * from './Libraries/Components/StatusBar/StatusBar'; export * from './Libraries/Components/Switch/Switch'; diff --git a/packages/rn-tester/Podfile.lock b/packages/rn-tester/Podfile.lock index 030d0acba63..b2361fd52a2 100644 --- a/packages/rn-tester/Podfile.lock +++ b/packages/rn-tester/Podfile.lock @@ -21,6 +21,7 @@ PODS: - RCTTypeSafety - React-bridging - React-Core + - React-cxxstableapi - React-debug - React-Fabric - React-featureflags @@ -49,6 +50,7 @@ PODS: - RCTTypeSafety - React-bridging - React-Core + - React-cxxstableapi - React-debug - React-Fabric - React-featureflags @@ -1404,7 +1406,6 @@ PODS: - React-FabricComponents/components/iostextinput (= 1000.0.0) - React-FabricComponents/components/modal (= 1000.0.0) - React-FabricComponents/components/rncore (= 1000.0.0) - - React-FabricComponents/components/safeareaview (= 1000.0.0) - React-FabricComponents/components/switch (= 1000.0.0) - React-FabricComponents/components/text (= 1000.0.0) - React-FabricComponents/components/textinput (= 1000.0.0) @@ -1530,33 +1531,6 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/safeareaview (1000.0.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-cxxreact - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-jsi - - React-jsiexecutor - - React-logger - - React-RCTFBReactNativeSpec - - React-rendererdebug - - React-runtimescheduler - - React-utils - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - React-FabricComponents/components/switch (1000.0.0): - boost - DoubleConversion @@ -2159,6 +2133,7 @@ PODS: - React-RCTFabric - React-RCTFBReactNativeSpec - React-RCTImage + - React-RCTLinking - React-RCTNetwork - React-RCTRuntime - React-rendererdebug @@ -2658,6 +2633,7 @@ PODS: - RCTTypeSafety - React-bridging - React-Core + - React-cxxstableapi - React-debug - React-Fabric - React-featureflags @@ -2957,93 +2933,93 @@ SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 - FBLazyVector: af92c6d1747ebb2387d117d9dd73d7e4903841a8 + FBLazyVector: 5d79fe29588711e614cf02add90e73380f861a1d fmt: 530618a01105dae0fa3a2f27c81ae11fa8f67eac glog: e56ede4028c4b7418e6b1195a36b1656bb35e225 - hermes-engine: c80355e32d44390d09935e3e31d569b19ff4b205 - MyNativeView: 02b46285748cdd4731b86f6d91840d6ba2e5741f - NativeCxxModuleExample: 3dee4ec2dece1caf6a820efde7f01efc5a034880 + hermes-engine: 4286c95931498cdf677eb49780ae8472f789fe69 + MyNativeView: 76ad67d331a2511f761380be28f6eff6f367e5f9 + NativeCxxModuleExample: 46d816ef02757e3b002f2f095c1b80254b4b8f80 OCMock: 589f2c84dacb1f5aaf6e4cec1f292551fe748e74 RCT-Folly: 36c4f904fb6cd0219dcb76b94e9502d2a72fab0b - RCTDeprecation: 3808e36294137f9ee5668f4df2e73dc079cd1dcf - RCTRequired: 25edbba9d281a349b2c6ea96164b24ddbaab9682 - RCTSwiftUI: ddb3e80be1d459ffbf69250f96ac110ba16936a1 - RCTSwiftUIWrapper: 80ba3daf3dce113d442e53822e32c14e290eb6a7 - RCTTypeSafety: 656f2b1b5978bedc692d17c142117cf77e27a965 - React: 9fcf4514ef577f653607ef701030619c4d446b8a - React-bridging: e5ca72ed0c4c42237c89d47395caae032324a85d - React-callinvoker: 9dbc5bf0c1770b852c286f27a8ff3ac2c41d2bae - React-Core: 07dd2589d382d42ebea21c94b63ae9b2b9d1c0db - React-CoreModules: 3fff481436a5bf1fe2107c77306b1b22060e6d86 - React-cxxreact: f73e41399049552c9008973772a8a9e71af4ca57 - React-cxxstableapi: 717808de4bba60c59802263e36a93da2167a7a79 - React-debug: c136ab6a9df808ec43092cf683d27035ebb48a26 - React-defaultsnativemodule: ddd0b272cf22ca179066f737c41af2b91f826271 - React-domnativemodule: 91aa634005208e9e068871b5161548faddb76d35 - React-Fabric: f94d3624837f799c26b15b4b014c667fc641e8ad - React-FabricComponents: aca79528e806729b41c4020b722febdd63385194 - React-FabricImage: 803f4a7988bbdad297bff982684cea9aeea3c901 - React-featureflags: 1cd13109ac3f2bdda0145f7c9bce6e70e237b661 - React-featureflagsnativemodule: a8591c1cece2cb28efd7a98632a035f52c344bb9 - React-graphics: bfe5977ad2800e2169a82a0afdea595bb04dd9ba - React-hermes: 52e0641c625bcdd88591dcad239515e7da86170d - React-idlecallbacksnativemodule: a76ec7bbeabde1b997eee3082a8986477effb352 - React-ImageManager: dbb3cba90eb68cd3f406354fcbc9d1a974d12346 - React-intersectionobservernativemodule: 1486d1730e131e8004a70d68bf1a725b79ef5c45 - React-jserrorhandler: 2178b775435a2d46765c1a95cff896f51d6ae297 - React-jsi: eab4393bbf978faa0d9d93ae5d13c20937fab320 - React-jsiexecutor: d07aaba9188314c08751a4f60ee863ef8bee1ecb - React-jsinspector: 085d6437ce83b5f0ad095909ee26d03da661e127 - React-jsinspectorcdp: 17244d8639ca280967956ca1c7da0dd1a4fc6454 - React-jsinspectornetwork: abcd141858a9228dd9fb5b7c2e5395c23ee9bbb4 - React-jsinspectortracing: ce38fda4fed441cc148ba7777bf357261a813ba3 - React-jsitooling: 54447648b4229d55f65dabba53518e56c8bf1a06 - React-jsitracing: c270fb95d74e14e264d33b3a2cf8ca78901cc66b - React-logger: d20ed7b124ed68131054d598cd1ea0bca74e68ee - React-Mapbuffer: 1bda74e2bfd17505fcd7ce21c95842f5d5cdfc78 - React-microtasksnativemodule: cec2334ddab49a8e38dcfb6967860b6002629233 - React-mutationobservernativemodule: dbbdf506d21c3c69653d837498e9d068147054e1 - React-NativeModulesApple: 15fbbad6cff9bef2da812843ba31b743b0f8f8bf - React-networking: 65716f04019b6a5f6ca8013307c3eefea47b6463 - React-oscompat: d4c113e294eaa6bedf7c18d03df8ea503a98129d - React-perflogger: c12ba5379b631513afe6ad4a87c7d64549668c76 - React-performancecdpmetrics: b150de1bd90ca5d536ec187ef9df340a88b9e2be - React-performancetimeline: 4ef48781f3fcafa44bdecab7c119e454bffdae3c - React-RCTActionSheet: 413cbb93858a32b5c18405e792262caec6cf158e - React-RCTAnimatedModuleProvider: dcbb65d677cf6ac1cda7912650ad33c4253916e6 - React-RCTAnimation: 164e50bb4cc07bbc5a5ec1f19b8bc9392268b39f - React-RCTAppDelegate: 3981a52f920a199573a1450764417236043a6717 - React-RCTBlob: 11c79b6e5447c2c1cc1b7633d7ee4d712a465cce - React-RCTFabric: 691f78aeca063781ac816cdd50594a02715f2bc0 - React-RCTFBReactNativeSpec: 329d23f7222c216da3b07dd02ff00b3b98fda1cd - React-RCTImage: bb0eb46db1e24ad906cb31c21e21b514db5b8a55 - React-RCTLinking: 6fb1ec3188dbf07f87b29afe172064a46d331446 - React-RCTNetwork: f424e280b3f9edd4d6692296c69d3dd5de92f78a - React-RCTPushNotification: 49c1acca24d107235c15a61af9ed856ef9e4c956 - React-RCTRuntime: feb1d8054fd1128af1622a6ec477ed5585dd01cc - React-RCTSettings: a932ad87f8dc6601deb45220a9bfa7da4a44df03 - React-RCTTest: 35876bf3e89cbbb357d52292f0e525996e5ce60c - React-RCTText: 16b0e933027bd04a5fc594461cb7691cd7eaaa56 - React-RCTVibration: fa7447c8f2435d18eb07e507e088c77cc6dbf61c - React-rendererconsistency: 7953edf876fa2626cfe7513f8a96dcb87e4be3ae - React-renderercss: 2ff59d46f74a73403fc90b7f70c24031b79173b4 - React-rendererdebug: f18285ef1772f907eb15f83134d33ef98916c261 - React-RuntimeApple: 412d7771e11d3ee5ad48e3d73ad4ab184e14f31b - React-RuntimeCore: c424d1a4f7354dfe5c9a06997865ccf78f22c244 - React-runtimeexecutor: 47ac7048f643a0bef6a0a86a5e136867d66a0768 - React-RuntimeHermes: 17ab25d968387190328b1e337024cf46fa3e5eb4 - React-runtimescheduler: 57f201dffa80893b709accd1722ab5efbedf7699 - React-timing: 208133eb7843c1b4292ab9b3c0c55ad2eedc709c - React-utils: 702979802d8aa217752fd73e95b96bad2646f5d9 - React-viewtransitionnativemodule: 2e1b09e22fe19f3e4286a5768d75570086d46baf - React-webperformancenativemodule: 86b068ca5c8c69583974ab63853b7acc7b5af653 - ReactAppDependencyProvider: 56ca45ab1d086e3093f7ab9c19a8dc8eac5c099b + RCTDeprecation: aa7b5fda9f4dc8176dfbffde52fc399b7cb0d66d + RCTRequired: 2a980df5a542b6d0b64e78c6936462f95fc430a3 + RCTSwiftUI: 397eb2e12aca0fbba9da92bd518c130dfba030a0 + RCTSwiftUIWrapper: 02b57c0c261580d196b3c5d3d0784d4322bbf38b + RCTTypeSafety: d5244b080345cb99cf1207ffc2663c440c4a1bce + React: 25d7d5f063d2a8eaef9042093e0faa46bf1f7e6a + React-bridging: 028651330aeaadb3d044fb61316f7eb5edce7bf3 + React-callinvoker: 3253cef92176d723d215eb830976c4407d63fb70 + React-Core: 65ff734234313813450260bef130b659fcdfed78 + React-CoreModules: a0a3fb59076ebb3539a9cc0ec0558514fad6e09e + React-cxxreact: 1fbf501f0cc725d0912895d484969cc32085e90e + React-cxxstableapi: 5d7aed0bd083272e5d6a1197a2f5874dd6fa70dc + React-debug: 789f22790445ecc0277c21d180edad938ec1355c + React-defaultsnativemodule: 6f00bdaffd77ae45c0f9187be99c2354bdc060ba + React-domnativemodule: 4640ecd16add71af43e9e3834c0b6bad15c02d79 + React-Fabric: 65cc7a8651cd1608eb5b72bd0a8173bb8ea85c4b + React-FabricComponents: 894739a446bf0c2f1fd9aafa5aa47e548d912193 + React-FabricImage: 201ec790670908145cac74fd53caaf82f4677a03 + React-featureflags: 3ed89aa5e31c82957789466f1ec513510474b883 + React-featureflagsnativemodule: 3a460f2f4b01d9b059752d6784ff89422e1312a1 + React-graphics: 4f2918d549c3021da06e691d80af6758110741bc + React-hermes: e86d5add0fe90f56ac56f355f44ac81e0663395d + React-idlecallbacksnativemodule: 76d42dba14091f465112f3379c5e90c84ee7753b + React-ImageManager: 88233d24184a1364e1b13a616b4217d5ee0fa3bd + React-intersectionobservernativemodule: 3ad91fdfc383d64fe21ee91eed9098e58a2cb961 + React-jserrorhandler: c24b8a5fb30744e5f1d094f359cc0dc931b68bbd + React-jsi: b67fa79eb1a6e7eded9e1b2831ad1ce2f4ff51d6 + React-jsiexecutor: 48b0de69ab68652c9f95da2af59f69b1b3888201 + React-jsinspector: b50f4776f583a0a5f91b66345bf40ef0149c7d8f + React-jsinspectorcdp: 15de4e8fc0c159d8f83f783b7e2a2d6ed41cf7d7 + React-jsinspectornetwork: 06349b14c851a93754eda9f17cfb31a57de85b99 + React-jsinspectortracing: 54e8369f65f2e61c7c0b11831363983358ebee55 + React-jsitooling: 96300f0cc959a8f3b72155e8f735b98fcfba9d01 + React-jsitracing: fe3e0404edbbf0867eb9e8ad9df649220def9a9b + React-logger: 4654f131be56a554b060449ca5ca1af4f975655e + React-Mapbuffer: f57e0b0cea17dbbf53a4697f0d0eaf907c90b4a7 + React-microtasksnativemodule: 5ca87525e9a4778e569f38d395486c3949d8a1e3 + React-mutationobservernativemodule: d470e649b8e21455d384283d8e05e240d2772be0 + React-NativeModulesApple: cce1fbedaa2bae30d4306bea2950252b2ae75208 + React-networking: 0e61dc8918e54fafe4a5ba3e83b62b7c0a13029b + React-oscompat: babc721c938bb36d5988dde6fac18fc42c4f09d6 + React-perflogger: db5b36c94a8a05d51b6fc8bac2de59b4e9964d6a + React-performancecdpmetrics: f0a5b7f17cd5ca1e1d033d61ebc77c3a05c2961c + React-performancetimeline: be0cc9335a36b1a48162129986a681746ac36bbc + React-RCTActionSheet: 9423c8a357e9e9a2ba434529b6881b33b7dc6917 + React-RCTAnimatedModuleProvider: a372e9d0ababf0a984e4c48898936b241c129b58 + React-RCTAnimation: a8e67fc89a82c00d40d548acc78bab16a3f78bd6 + React-RCTAppDelegate: fdd815e3a012c28262a8c84e45846696970b43a6 + React-RCTBlob: 43830b34671989e1e0c19d96961399fe0abc92d9 + React-RCTFabric: c22a620f5645cd79831bfae33124e41a6ef5b302 + React-RCTFBReactNativeSpec: 901e4d0b8182cfd2d63a640842653c8a2d53da22 + React-RCTImage: 319e721c28309655d303f369d2b6ec284cb1de00 + React-RCTLinking: 982c6844d2a9c8e4a01b915fe480e91a60947a35 + React-RCTNetwork: 9ada882f28a7107f30185c05a44713f01f6e279b + React-RCTPushNotification: 61da7ed557d9ad1448e45b7fce7f48f1aee65b27 + React-RCTRuntime: 6448878228bf91d3513cfaffdc8836f3dfec7a1e + React-RCTSettings: 74544098c12355948ea40cbbdd7a3557eac803ca + React-RCTTest: 4ffecb22c2c57fd0548e5dfae931c9c574dfdc87 + React-RCTText: 69e4792fa769c15487a05728d65f494542c2897e + React-RCTVibration: a114b43d998f57bb26177dc6087a8d5b5b76a248 + React-rendererconsistency: 164e9964829e232bf29f23a1ac635819e1485599 + React-renderercss: 88723c2efbae59cc2cda99ff3997de57ef710199 + React-rendererdebug: 503695e1360649a6fadcd03891adab9abb850e85 + React-RuntimeApple: dd2222a9f41e6bfa75b8ec02180deb92e6f3b728 + React-RuntimeCore: aa168dde5e7c8846fbe16da01505bb3f2fee5806 + React-runtimeexecutor: 54cb2c716d1e7dfd6da9aaece1a651dd768b29af + React-RuntimeHermes: 0aac9be6e30c284e74bbf24e4ddf2ccbe89fd235 + React-runtimescheduler: ac6c3d2ac6b90d07bf8b0b7213b4fa00b89d8c23 + React-timing: 9855773faaf566414d387d1bee4455c2493573c1 + React-utils: 40b2f2e33f6625f6020a527f7c9c64f64734ab92 + React-viewtransitionnativemodule: e9f3e208cedb56dd2ed46415c356fae158c1512b + React-webperformancenativemodule: 512187e6c30271b9161ac5bd12bb9944f7c02d0a + ReactAppDependencyProvider: 6608c009714b19748c04ce0f489ad19379ff108c ReactCodegen: 19447115b4e0b8eec131ae256e7000a9a0910dc4 - ReactCommon: 1376b66d6ef10d128c76c2d59fef49e706832ca6 - ReactCommon-Samples: b74ec150a8210d62be6cc899d28a24e7b795edd3 - ScreenshotManager: c005d764f11c09fb4a5640a3157024dddce859e9 + ReactCommon: 65e914376dd3b0f688d93ff37eb0fb0b843c2536 + ReactCommon-Samples: 9d9b24a340e15a0907a1ad116b9e8c455934a4ba + ScreenshotManager: bfc4f9d013f4f8c209cf153eb6ce7f331472b352 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: a9afa834491f5b80bc3b10d2d91e8bfb15ab1100 + Yoga: 6e8bc37a3d02e9ea3b566a5c04bbb168146d0ba3 PODFILE CHECKSUM: 59f1e7d58cffbfafbeb3df31151743dd4c9a52e8 diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj index 8248d57b2b2..f23b821e256 100644 --- a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj +++ b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj @@ -508,14 +508,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTester/Pods-RNTester-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTester/Pods-RNTester-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RNTester/Pods-RNTester-frameworks.sh\"\n"; @@ -551,14 +547,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTesterIntegrationTests/Pods-RNTesterIntegrationTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTesterIntegrationTests/Pods-RNTesterIntegrationTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RNTesterIntegrationTests/Pods-RNTesterIntegrationTests-resources.sh\"\n"; @@ -572,14 +564,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests-resources.sh\"\n"; @@ -627,14 +615,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTesterIntegrationTests/Pods-RNTesterIntegrationTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTesterIntegrationTests/Pods-RNTesterIntegrationTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RNTesterIntegrationTests/Pods-RNTesterIntegrationTests-frameworks.sh\"\n"; @@ -670,14 +654,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTester/Pods-RNTester-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTester/Pods-RNTester-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RNTester/Pods-RNTester-resources.sh\"\n"; @@ -713,14 +693,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests-frameworks.sh\"\n"; @@ -953,7 +929,10 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.1; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_CFLAGS = "$(inherited)"; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", @@ -961,11 +940,13 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", ); OTHER_LDFLAGS = ( "-ObjC", "-lc++", ); + PODFILE_DIR = "$(SRCROOT)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -1046,7 +1027,10 @@ ); IPHONEOS_DEPLOYMENT_TARGET = 15.1; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_CFLAGS = "$(inherited)"; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", @@ -1054,11 +1038,13 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", ); OTHER_LDFLAGS = ( "-ObjC", "-lc++", ); + PODFILE_DIR = "$(SRCROOT)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js b/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js index b71ce8beb50..7dfc1b71fc8 100644 --- a/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js +++ b/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js @@ -17,6 +17,7 @@ import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; import RNTesterPlatformTestMinimizedResultView from './RNTesterPlatformTestMinimizedResultView'; import RNTesterPlatformTestResultsText from './RNTesterPlatformTestResultsText'; +import SafeAreaView from 'react-native/src/private/components/safeareaview/SafeAreaView'; import * as React from 'react'; import {memo, useCallback, useMemo, useState} from 'react'; import { @@ -25,7 +26,6 @@ import { KeyboardAvoidingView, Modal, Platform, - SafeAreaView, StyleSheet, Switch, Text, diff --git a/packages/rn-tester/js/examples/SafeAreaInsets/SafeAreaInsetsExample.js b/packages/rn-tester/js/examples/SafeAreaInsets/SafeAreaInsetsExample.js new file mode 100644 index 00000000000..799ac767333 --- /dev/null +++ b/packages/rn-tester/js/examples/SafeAreaInsets/SafeAreaInsetsExample.js @@ -0,0 +1,314 @@ +/** + * 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 + */ + +'use strict'; + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; +import type {SafeAreaInsetsChangeEvent} from 'react-native/Libraries/Types/CoreEventTypes'; + +import RNTesterText from '../../components/RNTesterText'; +import * as React from 'react'; +import {useCallback, useState} from 'react'; +import { + Button, + Modal, + ScrollView, + StyleSheet, + TextInput, + View, + useWindowDimensions, +} from 'react-native'; + +type Insets = SafeAreaInsetsChangeEvent['nativeEvent']['insets']; +type Frame = SafeAreaInsetsChangeEvent['nativeEvent']['frame']; + +function useSafeAreaInsets(): [ + ?Insets, + ?Frame, + (SafeAreaInsetsChangeEvent) => void, +] { + const [state, setState] = useState(null); + const onSafeAreaInsetsChange = useCallback( + (event: SafeAreaInsetsChangeEvent) => { + setState({ + insets: event.nativeEvent.insets, + frame: event.nativeEvent.frame, + }); + }, + [], + ); + return [state?.insets, state?.frame, onSafeAreaInsetsChange]; +} + +function InsetsReadoutExample(): React.Node { + const [insets, frame, onSafeAreaInsetsChange] = useSafeAreaInsets(); + + return ( + + + {insets == null + ? 'Waiting for insets…' + : `insets: {top: ${insets.top}, right: ${insets.right}, bottom: ${insets.bottom}, left: ${insets.left}}`} + + + {frame == null + ? '' + : `frame: {x: ${frame.x}, y: ${frame.y}, width: ${frame.width}, height: ${frame.height}}`} + + + This view does not reach under the system UI, so its insets are zero. + + + ); +} + +function FullScreenModalContent({onClose}: {onClose: () => void}): React.Node { + const [insets, , onSafeAreaInsetsChange] = useSafeAreaInsets(); + const [applied, setApplied] = useState(false); + + // The view observes the safe area but no event has been received yet. With + // synchronous dispatch this state is committed but never displayed: the + // event fires while this tree is being mounted and the insets are applied + // before the frame is presented. If a frame ever renders in this state, the + // dispatch was not synchronous. + const waitingForInsets = applied && insets == null; + + return ( + + + + {insets != null + ? `top: ${insets.top}, right: ${insets.right}, bottom: ${insets.bottom}, left: ${insets.left}` + : waitingForInsets + ? 'Observing the safe area, inset event not received yet — this state should never be visible.' + : 'Insets not applied: the content extends under the system UI.'} + + + Applying the insets and rotating the device both update the padding in + the same frame, without the content jumping. + + {!applied ? ( +