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 cb67856cb4b3..7d194d4db35b 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<<2ceaa8e472a53fc667994edb63ccd995>> + * @generated SignedSource<<1faa2820bf867aa599cdd390c5c90b00>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -5968,7 +5968,8 @@ export { RootViewStyleProvider, // d5909148 Runnable, // 594dd93a Runnables, // 4367c557 - SafeAreaInsetsChangeEvent, // cedb5309 + SafeAreaInsets, // 64d7a605 + SafeAreaInsetsChangeEvent, // 4ffd35e5 SafeAreaView, // d917b6ec SafeAreaViewInstance, // 2b5d4391 ScaledSize, // 07e417c7 diff --git a/packages/react-native/index.js.flow b/packages/react-native/index.js.flow index 3d015144404b..8dadcac64496 100644 --- a/packages/react-native/index.js.flow +++ b/packages/react-native/index.js.flow @@ -485,6 +485,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 { - + ); }