From 4dd66d96cecc4071ba1a3726f99beb0424cf1215 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 29 Oct 2026 13:27:00 -0700 Subject: [PATCH 1/2] Render the internal SafeAreaView from the safe area insets prop (#58112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: The private `SafeAreaView` used by LogBox, the element inspector and `InputAccessoryView` now pads its children from the `experimental_onSafeAreaInsetsChange` prop instead of wrapping the native `RCTSafeAreaView`. It also works on Android, where it was a plain `View` before. The callers that still used the deprecated public `SafeAreaView` move off it: RNTester's platform test results view and the helloworld app use the insets prop, typed with `SafeAreaInsets`, which is now exported alongside `SafeAreaInsetsChangeEvent`. LogBox's inspector header drops its Android-only `View` fallback. The native view is left in place for the deprecated public `SafeAreaView`; https://github.com/react/react-native/issues/58113 removes it. ## Changelog: [ANDROID] [FIXED] - LogBox, the element inspector and `InputAccessoryView` now apply safe area padding on Android [GENERAL] [ADDED] - Export the `SafeAreaInsets` type Pull Request resolved: https://github.com/react/react-native/pull/58112 Test Plan: ``` yarn test packages/react-native/Libraries/LogBox # 101 passed, 3 snapshots updated yarn flow-check # 0 errors ``` LogBox notification, LogBox inspector and element inspector on iOS and Android, plus `InputAccessoryView` on iOS: | | LogBox notification | LogBox inspector | Element inspector | | |---|---|---|---|---| | iOS | | | |
`InputAccessoryView` | | Android | | | |
Element inspector, panel at the top | --- 👉 1. https://github.com/react/react-native/issues/58112 — Render the internal SafeAreaView from the safe area insets prop 2. https://github.com/react/react-native/issues/58113 — Remove the native SafeAreaView and the deprecated public export Differential Revision: D126071736 Pulled By: Abbondanzo --- .../TextInput/InputAccessoryView.js | 2 +- .../__tests__/ViewSafeAreaInsets-itest.js | 39 ++++++++++ .../LogBox/UI/LogBoxInspectorFooterButton.js | 2 +- .../LogBox/UI/LogBoxInspectorHeader.js | 14 ++-- .../LogBoxNotificationContainer-test.js.snap | 72 +++++++++++-------- packages/react-native/ReactNativeApi.d.ts | 5 +- packages/react-native/index.js.flow | 1 + .../SafeAreaView_INTERNAL_DO_NOT_USE.js | 69 ++++++++++++++---- .../__tests__/SafeAreaView-itest.js | 3 +- .../elementinspector/InspectorPanel.js | 2 +- .../RNTesterPlatformTestResultView.js | 42 ++++++++--- private/helloworld/App.tsx | 19 ++++- 12 files changed, 196 insertions(+), 74 deletions(-) rename packages/react-native/{Libraries/Components/SafeAreaView => src/private/components/safeareaview}/__tests__/SafeAreaView-itest.js (91%) diff --git a/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js b/packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js index 5ded8e707aed..c9e3f4c0b97c 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_INTERNAL_DO_NOT_USE'; import StyleSheet, { type ColorValue, type ViewStyleProp, diff --git a/packages/react-native/Libraries/Components/View/__tests__/ViewSafeAreaInsets-itest.js b/packages/react-native/Libraries/Components/View/__tests__/ViewSafeAreaInsets-itest.js index bfbd1a24515f..007b51149526 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/ViewSafeAreaInsets-itest.js +++ b/packages/react-native/Libraries/Components/View/__tests__/ViewSafeAreaInsets-itest.js @@ -12,6 +12,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import type {HostInstance} from 'react-native'; +import SafeAreaView from '../../../../src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE'; import * as Fantom from '@react-native/fantom'; import * as React from 'react'; import {createRef} from 'react'; @@ -116,3 +117,41 @@ describe('experimental_onSafeAreaInsetsChange', () => { ).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, + }); + + expect( + root + .getRenderedOutput({ + props: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'], + }) + .toJSX(), + ).toEqual( + , + ); + }); +}); diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorFooterButton.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorFooterButton.js index 3db512ed3d43..3a3d5da97792 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_INTERNAL_DO_NOT_USE'; 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..06569cab0d4c 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_INTERNAL_DO_NOT_USE'; 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 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/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index c0f8309f0ac5..990cc271b158 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<> + * @generated SignedSource<> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -5980,7 +5980,8 @@ export { RootViewStyleProvider, // d5909148 Runnable, // 594dd93a Runnables, // 4367c557 - SafeAreaInsetsChangeEvent, // cedb5309 + SafeAreaInsets, // 64d7a605 + SafeAreaInsetsChangeEvent, // 4ffd35e5 SafeAreaView, // dda99293 SafeAreaViewInstance, // 2b5d4391 ScaledSize, // 07e417c7 diff --git a/packages/react-native/index.js.flow b/packages/react-native/index.js.flow index 113127b308b2..1bf4e01d9788 100644 --- a/packages/react-native/index.js.flow +++ b/packages/react-native/index.js.flow @@ -486,6 +486,7 @@ export type { NativeTouchEvent, NativeUIEvent, ResponderSyntheticEvent, + SafeAreaInsets, SafeAreaInsetsChangeEvent, ScrollEvent, TargetedEvent, 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 index e6b5cdd5f522..0ea40f01bbf6 100644 --- 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 @@ -9,23 +9,62 @@ */ 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 UIManager from '../../../../Libraries/ReactNative/UIManager'; -import Platform from '../../../../Libraries/Utilities/Platform'; +import I18nManager from '../../../../Libraries/ReactNative/I18nManager'; import * as React from 'react'; +import {useCallback, useMemo, useState} 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, -}); +/** + * 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 exported; +export default SafeAreaView; 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 91% 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 0fcae9f2e282..cb84b97d23e7 100644 --- a/packages/react-native/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-itest.js +++ b/packages/react-native/src/private/components/safeareaview/__tests__/SafeAreaView-itest.js @@ -10,9 +10,10 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; +import SafeAreaView from '../SafeAreaView_INTERNAL_DO_NOT_USE'; import * as Fantom from '@react-native/fantom'; import * as React from 'react'; -import {SafeAreaView, Text} from 'react-native'; +import {Text} from 'react-native'; describe('', () => { it('renders with children', () => { 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..b4195f431486 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_INTERNAL_DO_NOT_USE'; import * as React from 'react'; const ScrollView = diff --git a/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js b/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js index c0f0e2b06282..6821dca645b7 100644 --- a/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js +++ b/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js @@ -12,7 +12,12 @@ import type { PlatformTestResult, PlatformTestResultStatus, } from './RNTesterPlatformTestTypes'; -import type {ListRenderItemInfo, TextStyle} from 'react-native'; +import type { + ListRenderItemInfo, + SafeAreaInsets, + SafeAreaInsetsChangeEvent, + TextStyle, +} from 'react-native'; import RNTesterPlatformTestMinimizedResultView from './RNTesterPlatformTestMinimizedResultView'; import RNTesterPlatformTestResultsText from './RNTesterPlatformTestResultsText'; @@ -24,7 +29,6 @@ import { KeyboardAvoidingView, Modal, Platform, - SafeAreaView, StyleSheet, Switch, Text, @@ -87,7 +91,7 @@ function FilterModalButton(props: FilterModalProps) { animationType="fade" presentationStyle="overFullScreen" transparent={true}> - + @@ -123,7 +127,7 @@ function FilterModalButton(props: FilterModalProps) { - + ); @@ -230,6 +234,13 @@ export default function RNTesterPlatformTestResultView( ); const [resultsExpanded, setResultsExpanded] = useState(false); + const [safeAreaInsets, setSafeAreaInsets] = useState(null); + const handleSafeAreaInsetsChange = useCallback( + (event: SafeAreaInsetsChangeEvent) => { + setSafeAreaInsets(event.nativeEvent.insets); + }, + [], + ); const handleReset = useCallback(() => { setFilterFailStatus(false); @@ -266,12 +277,21 @@ export default function RNTesterPlatformTestResultView( animationType="slide" onRequestClose={handleMaximizedPress} visible={resultsExpanded}> - + Results @@ -308,7 +328,7 @@ export default function RNTesterPlatformTestResultView( - + ); diff --git a/private/helloworld/App.tsx b/private/helloworld/App.tsx index e692a7dd84d1..19c0a8358ff8 100644 --- a/private/helloworld/App.tsx +++ b/private/helloworld/App.tsx @@ -8,8 +8,9 @@ */ import * as React from 'react'; +import {useState} from 'react'; import { - SafeAreaView, + type SafeAreaInsets, ScrollView, StatusBar, StyleSheet, @@ -20,9 +21,21 @@ import { function App(): React.ReactNode { const isDarkMode = useColorScheme() === 'dark'; + const [insets, setInsets] = useState(null); return ( - + + setInsets(event.nativeEvent.insets) + }> @@ -31,7 +44,7 @@ function App(): React.ReactNode { - + ); } From 4db69fdf6c6c4e54f6aa87e4901e83d69645391f Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Thu, 29 Oct 2026 14:30:39 -0700 Subject: [PATCH 2/2] Apply safe area insets RTL-aware in RNTester and helloworld (#59645) Summary: Pull Request resolved: https://github.com/react/react-native/pull/59645 RNTester's platform test results view and the helloworld app pad themselves from `experimental_onSafeAreaInsetsChange`, mapping `insets.left`/`insets.right` straight to `paddingLeft`/`paddingRight`. Insets are physical edges, but Yoga treats `paddingLeft`/`paddingRight` as start/end when `swapLeftAndRightInRTL` is on, so in RTL a right-edge cutout was left unpadded while the left edge got its padding. Both callers now swap the left and right insets in that case, as the internal `SafeAreaView` already does. Also updates the LogBox inspector, inspector footer and inspector header Fantom snapshots for the internal `SafeAreaView`'s wrapper view. Changelog: [Internal] Reviewed By: andrewdacenko Differential Revision: D128337176 --- .../LogBoxInspector-itest.js.snap | 438 ++++++++++-------- .../LogBoxInspectorFooter-itest.js.snap | 297 +++++++----- .../LogBoxInspectorHeader-itest.js.snap | 40 +- .../RNTesterPlatformTestResultView.js | 22 +- private/helloworld/App.tsx | 8 +- private/helloworld/__tests__/App.test.tsx | 24 + 6 files changed, 473 insertions(+), 356 deletions(-) diff --git a/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspector-itest.js.snap b/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspector-itest.js.snap index f1fe5f55a826..a59895fb00fe 100644 --- a/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspector-itest.js.snap +++ b/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspector-itest.js.snap @@ -8,67 +8,69 @@ exports[`LogBoxInspector should render fatal with selectedIndex 2 1`] = ` > - - - - - Log 3 of 3 - - - + + + + > + Log 3 of 3 + + + + - - Dismiss - + + Dismiss + + - - Minimize - + + Minimize + + - - Copy - + + Copy + + @@ -197,67 +214,69 @@ exports[`LogBoxInspector should render warning with selectedIndex 0 1`] = ` > - - - - - Log 1 of 3 - - - + + + + > + Log 1 of 3 + + + + - - Dismiss - + + Dismiss + + - - Minimize - + + Minimize + + - - Copy - + + Copy + + diff --git a/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorFooter-itest.js.snap b/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorFooter-itest.js.snap index e9dfe66c817d..3d47dcfdde25 100644 --- a/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorFooter-itest.js.snap +++ b/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorFooter-itest.js.snap @@ -32,58 +32,73 @@ exports[`LogBoxInspectorFooter should render two buttons for error 1`] = ` flexDirection="row" > - - Dismiss - + + Dismiss + + - - Minimize - + + Minimize + + - - Copy - + + Copy + + `; @@ -94,58 +109,73 @@ exports[`LogBoxInspectorFooter should render two buttons for fatal 1`] = ` flexDirection="row" > - - Dismiss - + + Dismiss + + - - Minimize - + + Minimize + + - - Copy - + + Copy + + `; @@ -156,58 +186,73 @@ exports[`LogBoxInspectorFooter should render two buttons for warning 1`] = ` flexDirection="row" > - - Dismiss - + + Dismiss + + - - Minimize - + + Minimize + + - - Copy - + + Copy + + `; diff --git a/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorHeader-itest.js.snap b/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorHeader-itest.js.snap index b6136c251f63..2529f432c71c 100644 --- a/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorHeader-itest.js.snap +++ b/packages/react-native/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxInspectorHeader-itest.js.snap @@ -1,10 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`LogBoxInspectorHeader should render both buttons for two total 1`] = ` - - + - + `; exports[`LogBoxInspectorHeader should render no buttons for one total 1`] = ` - - + - + `; exports[`LogBoxInspectorHeader should render syntax error header 1`] = ` - - + Failed to compile - + `; exports[`LogBoxInspectorHeader should render two buttons for three or more total 1`] = ` - - + - + `; diff --git a/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js b/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js index 6821dca645b7..8cc6cf06d90f 100644 --- a/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js +++ b/packages/rn-tester/js/examples/Experimental/PlatformTest/RNTesterPlatformTestResultView.js @@ -26,6 +26,7 @@ import {memo, useCallback, useMemo, useState} from 'react'; import { Button, FlatList, + I18nManager, KeyboardAvoidingView, Modal, Platform, @@ -241,6 +242,20 @@ export default function RNTesterPlatformTestResultView( }, [], ); + const safeAreaPaddingStyle = useMemo(() => { + if (safeAreaInsets == null) { + return null; + } + // Insets are physical edges, but Yoga treats paddingLeft/paddingRight as + // start/end when swapLeftAndRightInRTL is on. + const swap = I18nManager.isRTL && I18nManager.doLeftAndRightSwapInRTL; + return { + paddingTop: safeAreaInsets.top, + paddingRight: swap ? safeAreaInsets.left : safeAreaInsets.right, + paddingBottom: safeAreaInsets.bottom, + paddingLeft: swap ? safeAreaInsets.right : safeAreaInsets.left, + }; + }, [safeAreaInsets]); const handleReset = useCallback(() => { setFilterFailStatus(false); @@ -285,12 +300,7 @@ export default function RNTesterPlatformTestResultView( height: '100%', flexDirection: 'column', }, - safeAreaInsets != null && { - paddingTop: safeAreaInsets.top, - paddingRight: safeAreaInsets.right, - paddingBottom: safeAreaInsets.bottom, - paddingLeft: safeAreaInsets.left, - }, + safeAreaPaddingStyle, ]}> diff --git a/private/helloworld/App.tsx b/private/helloworld/App.tsx index 19c0a8358ff8..0aa51a55b775 100644 --- a/private/helloworld/App.tsx +++ b/private/helloworld/App.tsx @@ -10,6 +10,7 @@ import * as React from 'react'; import {useState} from 'react'; import { + I18nManager, type SafeAreaInsets, ScrollView, StatusBar, @@ -22,15 +23,18 @@ import { function App(): React.ReactNode { const isDarkMode = useColorScheme() === 'dark'; const [insets, setInsets] = useState(null); + // Insets are physical edges, but Yoga treats paddingLeft/paddingRight as + // start/end when swapLeftAndRightInRTL is on. + const swap = I18nManager.isRTL && I18nManager.doLeftAndRightSwapInRTL; return ( diff --git a/private/helloworld/__tests__/App.test.tsx b/private/helloworld/__tests__/App.test.tsx index 399fc98c4b41..0f2e3604b18d 100644 --- a/private/helloworld/__tests__/App.test.tsx +++ b/private/helloworld/__tests__/App.test.tsx @@ -9,6 +9,7 @@ import App from '../App'; import React from 'react'; +import {I18nManager, View} from 'react-native'; import ReactTestRenderer from 'react-test-renderer'; test('renders correctly', async () => { @@ -16,3 +17,26 @@ test('renders correctly', async () => { ReactTestRenderer.create(); }); }); + +test('pads the physical edges with the safe area insets in RTL', async () => { + jest.replaceProperty(I18nManager, 'isRTL', true); + + let renderer: ReactTestRenderer.ReactTestRenderer | undefined; + await ReactTestRenderer.act(() => { + renderer = ReactTestRenderer.create(); + }); + const getRootView = () => renderer!.root.findAllByType(View)[0]; + + await ReactTestRenderer.act(() => { + getRootView().props.experimental_onSafeAreaInsetsChange({ + nativeEvent: {insets: {top: 44, right: 0, bottom: 34, left: 59}}, + }); + }); + + expect(getRootView().props.style).toEqual({ + paddingTop: 44, + paddingRight: 59, + paddingBottom: 34, + paddingLeft: 0, + }); +});