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 (
-
+
-
+
);
}
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/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..8cc6cf06d90f 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';
@@ -21,10 +26,10 @@ import {memo, useCallback, useMemo, useState} from 'react';
import {
Button,
FlatList,
+ I18nManager,
KeyboardAvoidingView,
Modal,
Platform,
- SafeAreaView,
StyleSheet,
Switch,
Text,
@@ -87,7 +92,7 @@ function FilterModalButton(props: FilterModalProps) {
animationType="fade"
presentationStyle="overFullScreen"
transparent={true}>
-
+
@@ -123,7 +128,7 @@ function FilterModalButton(props: FilterModalProps) {
-
+
>
);
@@ -230,6 +235,27 @@ export default function RNTesterPlatformTestResultView(
);
const [resultsExpanded, setResultsExpanded] = useState(false);
+ const [safeAreaInsets, setSafeAreaInsets] = useState(null);
+ const handleSafeAreaInsetsChange = useCallback(
+ (event: SafeAreaInsetsChangeEvent) => {
+ setSafeAreaInsets(event.nativeEvent.insets);
+ },
+ [],
+ );
+ 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);
@@ -266,12 +292,16 @@ export default function RNTesterPlatformTestResultView(
animationType="slide"
onRequestClose={handleMaximizedPress}
visible={resultsExpanded}>
-
+
Results
@@ -308,7 +338,7 @@ export default function RNTesterPlatformTestResultView(
-
+
>
);
diff --git a/private/helloworld/App.tsx b/private/helloworld/App.tsx
index e692a7dd84d1..0aa51a55b775 100644
--- a/private/helloworld/App.tsx
+++ b/private/helloworld/App.tsx
@@ -8,8 +8,10 @@
*/
import * as React from 'react';
+import {useState} from 'react';
import {
- SafeAreaView,
+ I18nManager,
+ type SafeAreaInsets,
ScrollView,
StatusBar,
StyleSheet,
@@ -20,9 +22,24 @@ 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 (
-
+
+ setInsets(event.nativeEvent.insets)
+ }>
@@ -31,7 +48,7 @@ function App(): React.ReactNode {
-
+
);
}
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,
+ });
+});