From 9ac7324976b67d7a3ca4d6a80ed51f12c9a7c87d Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 21 Sep 2026 14:43:12 -0400 Subject: [PATCH] Render the internal SafeAreaView from the safe area insets prop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The core surfaces that cannot depend on `react-native-safe-area-context` — LogBox, the element inspector, `InputAccessoryView` — get their safe area padding from a private component that until now wrapped the native `RCTSafeAreaView`. It applies the prop instead, in JavaScript. The insets come from the synchronous inset event, which is processed in the frame that mounts the view, so the first frame is already padded. Two consequences, both visible in the updated LogBox snapshots: these surfaces now apply safe area padding on Android too, where they previously fell back to a plain `View`, and they re-render when insets arrive rather than being padded natively. The native implementations are untouched here — the deprecated public `SafeAreaView` still uses them. This only moves the internal component onto the prop, so the two can be compared against each other before the native side is removed. --- .../TextInput/InputAccessoryView.js | 2 +- .../LogBox/LogBoxNotificationContainer.js | 2 +- .../LogBox/UI/LogBoxInspectorFooterButton.js | 2 +- .../LogBox/UI/LogBoxInspectorHeader.js | 2 +- .../LogBoxNotificationContainer-test.js.snap | 72 +++++++++++-------- .../components/safeareaview/SafeAreaView.js | 70 ++++++++++++++++++ .../SafeAreaView_INTERNAL_DO_NOT_USE.js | 31 -------- .../devmenu/elementinspector/Inspector.js | 2 +- .../elementinspector/InspectorPanel.js | 2 +- 9 files changed, 118 insertions(+), 67 deletions(-) create mode 100644 packages/react-native/src/private/components/safeareaview/SafeAreaView.js delete mode 100644 packages/react-native/src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE.js diff --git a/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js b/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js index 5ded8e707aed..acb44026a6e5 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/LogBox/LogBoxNotificationContainer.js b/packages/react-native/Libraries/LogBox/LogBoxNotificationContainer.js index 1042525f9b39..f9d765423046 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 3db512ed3d43..a896030ab1c1 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 a5b5329d440e..6ab735b04308 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js @@ -11,7 +11,7 @@ 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'; 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 6799302e9474..7c65b0a79669 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,16 +1,20 @@ // 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`; @@ -119,16 +123,20 @@ 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/src/private/components/safeareaview/SafeAreaView.js b/packages/react-native/src/private/components/safeareaview/SafeAreaView.js new file mode 100644 index 000000000000..0ea40f01bbf6 --- /dev/null +++ b/packages/react-native/src/private/components/safeareaview/SafeAreaView.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @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 I18nManager from '../../../../Libraries/ReactNative/I18nManager'; +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. + */ +component SafeAreaView( + ref?: React.RefSetter, + ...props: ViewProps +) { + const {style, experimental_onSafeAreaInsetsChange, ...otherProps} = props; + const [insets, setInsets] = useState(null); + + const handleSafeAreaInsetsChange = useCallback( + (event: SafeAreaInsetsChangeEvent) => { + setInsets(event.nativeEvent.insets); + experimental_onSafeAreaInsetsChange?.(event); + }, + [experimental_onSafeAreaInsetsChange], + ); + + const paddingStyle = useMemo(() => { + if (insets == null) { + return null; + } + // Insets are physical edges, but Yoga remaps paddingLeft/paddingRight to + // start/end when I18nManager's swapLeftAndRightInRTL is on, which would + // pad the mirror-image edge in RTL. Swap the values so the physical edge + // keeps its inset. + const {isRTL, doLeftAndRightSwapInRTL} = I18nManager.getConstants(); + const swap = isRTL && doLeftAndRightSwapInRTL; + return { + paddingTop: insets.top, + paddingRight: swap ? insets.left : insets.right, + paddingBottom: insets.bottom, + paddingLeft: swap ? insets.right : 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 e6b5cdd5f522..000000000000 --- 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/src/private/devsupport/devmenu/elementinspector/Inspector.js b/packages/react-native/src/private/devsupport/devmenu/elementinspector/Inspector.js index 4bc6a2010067..1c32af0364a6 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 9e5d04f30038..d5756ac7badc 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 =