Skip to content

Commit f2a8a05

Browse files
Render the internal SafeAreaView from the safe area insets prop
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.
1 parent 1c6e0c7 commit f2a8a05

13 files changed

Lines changed: 195 additions & 88 deletions

File tree

‎packages/react-native/Libraries/Components/TextInput/InputAccessoryView.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @format
99
*/
1010

11-
import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView';
11+
import SafeAreaView from '../../../src/private/components/safeareaview/SafeAreaView';
1212
import StyleSheet, {
1313
type ColorValue,
1414
type ViewStyleProp,

‎packages/react-native/Libraries/Components/View/__tests__/ViewSafeAreaInsets-itest.js‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import * as Fantom from '@react-native/fantom';
1616
import * as React from 'react';
1717
import {createRef} from 'react';
1818
import {View} from 'react-native';
19+
import SafeAreaView from 'react-native/src/private/components/safeareaview/SafeAreaView';
1920

2021
const INSETS = {top: 44, right: 0, bottom: 34, left: 0};
2122

@@ -116,3 +117,41 @@ describe('experimental_onSafeAreaInsetsChange', () => {
116117
).toEqual(<rn-view experimental_onSafeAreaInsetsChange="true" />);
117118
});
118119
});
120+
121+
describe('<SafeAreaView>', () => {
122+
it('applies the insets it receives as padding', () => {
123+
const root = Fantom.createRoot();
124+
const nodeRef = createRef<HostInstance>();
125+
126+
Fantom.runTask(() => {
127+
root.render(<SafeAreaView collapsable={false} ref={nodeRef} />);
128+
});
129+
130+
expect(
131+
root
132+
.getRenderedOutput({
133+
props: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
134+
})
135+
.toJSX(),
136+
).toEqual(<rn-view />);
137+
138+
Fantom.dispatchNativeEvent(nodeRef, 'safeAreaInsetsChange', {
139+
insets: INSETS,
140+
});
141+
142+
expect(
143+
root
144+
.getRenderedOutput({
145+
props: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
146+
})
147+
.toJSX(),
148+
).toEqual(
149+
<rn-view
150+
paddingBottom="34"
151+
paddingLeft="0"
152+
paddingRight="0"
153+
paddingTop="44"
154+
/>,
155+
);
156+
});
157+
});

‎packages/react-native/Libraries/LogBox/LogBoxNotificationContainer.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @format
99
*/
1010

11-
import SafeAreaView from '../../src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE';
11+
import SafeAreaView from '../../src/private/components/safeareaview/SafeAreaView';
1212
import View from '../Components/View/View';
1313
import StyleSheet from '../StyleSheet/StyleSheet';
1414
import BackHandler from '../Utilities/BackHandler';

‎packages/react-native/Libraries/LogBox/UI/LogBoxInspectorFooterButton.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @format
99
*/
1010

11-
import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView';
11+
import SafeAreaView from '../../../src/private/components/safeareaview/SafeAreaView';
1212
import View from '../../Components/View/View';
1313
import StyleSheet from '../../StyleSheet/StyleSheet';
1414
import Text from '../../Text/Text';

‎packages/react-native/Libraries/LogBox/UI/LogBoxInspectorHeader.js‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88
* @format
99
*/
1010

11-
import type {ViewProps} from '../../Components/View/ViewPropTypes';
1211
import type {LogLevel} from '../Data/LogBoxLog';
1312

14-
import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView';
13+
import SafeAreaView from '../../../src/private/components/safeareaview/SafeAreaView';
1514
import View from '../../Components/View/View';
1615
import StyleSheet from '../../StyleSheet/StyleSheet';
1716
import Text from '../../Text/Text';
@@ -27,13 +26,10 @@ type Props = Readonly<{
2726
level: LogLevel,
2827
}>;
2928

30-
const LogBoxInspectorHeaderSafeArea: React.ComponentType<ViewProps> =
31-
Platform.OS === 'android' ? View : SafeAreaView;
32-
3329
export default function LogBoxInspectorHeader(props: Props): React.Node {
3430
if (props.level === 'syntax') {
3531
return (
36-
<LogBoxInspectorHeaderSafeArea style={styles[props.level]}>
32+
<SafeAreaView style={styles[props.level]}>
3733
<View style={styles.header}>
3834
<View style={styles.title}>
3935
<Text
@@ -44,7 +40,7 @@ export default function LogBoxInspectorHeader(props: Props): React.Node {
4440
</Text>
4541
</View>
4642
</View>
47-
</LogBoxInspectorHeaderSafeArea>
43+
</SafeAreaView>
4844
);
4945
}
5046

@@ -56,7 +52,7 @@ export default function LogBoxInspectorHeader(props: Props): React.Node {
5652
const titleText = `Log ${props.selectedIndex + 1} of ${props.total}`;
5753

5854
return (
59-
<LogBoxInspectorHeaderSafeArea style={styles[props.level]}>
55+
<SafeAreaView style={styles[props.level]}>
6056
<View style={styles.header}>
6157
<LogBoxInspectorHeaderButton
6258
id="logbox_header_button_prev"
@@ -81,7 +77,7 @@ export default function LogBoxInspectorHeader(props: Props): React.Node {
8177
onPress={() => props.onSelectIndex(nextIndex)}
8278
/>
8379
</View>
84-
</LogBoxInspectorHeaderSafeArea>
80+
</SafeAreaView>
8581
);
8682
}
8783

‎packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBoxNotificationContainer-test.js.snap‎

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`LogBoxNotificationContainer should render both an error and warning notification 1`] = `
4-
<SafeAreaView
4+
<View
5+
experimental_onSafeAreaInsetsChange={[Function]}
56
style={
6-
Object {
7-
"alignItems": "center",
8-
"bottom": 20,
9-
"left": 10,
10-
"pointerEvents": "box-none",
11-
"position": "absolute",
12-
"right": 10,
13-
}
7+
Array [
8+
Object {
9+
"alignItems": "center",
10+
"bottom": 20,
11+
"left": 10,
12+
"pointerEvents": "box-none",
13+
"position": "absolute",
14+
"right": 10,
15+
},
16+
null,
17+
]
1418
}
1519
>
1620
<View
@@ -107,7 +111,7 @@ exports[`LogBoxNotificationContainer should render both an error and warning not
107111
totalLogCount={1}
108112
/>
109113
</View>
110-
</SafeAreaView>
114+
</View>
111115
`;
112116

113117
exports[`LogBoxNotificationContainer should render null with no logs 1`] = `null`;
@@ -119,16 +123,20 @@ exports[`LogBoxNotificationContainer should render selected fatal error even whe
119123
exports[`LogBoxNotificationContainer should render selected syntax error even when disabled 1`] = `null`;
120124

121125
exports[`LogBoxNotificationContainer should render the latest error notification 1`] = `
122-
<SafeAreaView
126+
<View
127+
experimental_onSafeAreaInsetsChange={[Function]}
123128
style={
124-
Object {
125-
"alignItems": "center",
126-
"bottom": 20,
127-
"left": 10,
128-
"pointerEvents": "box-none",
129-
"position": "absolute",
130-
"right": 10,
131-
}
129+
Array [
130+
Object {
131+
"alignItems": "center",
132+
"bottom": 20,
133+
"left": 10,
134+
"pointerEvents": "box-none",
135+
"position": "absolute",
136+
"right": 10,
137+
},
138+
null,
139+
]
132140
}
133141
>
134142
<View
@@ -178,20 +186,24 @@ exports[`LogBoxNotificationContainer should render the latest error notification
178186
totalLogCount={2}
179187
/>
180188
</View>
181-
</SafeAreaView>
189+
</View>
182190
`;
183191

184192
exports[`LogBoxNotificationContainer should render the latest warning notification 1`] = `
185-
<SafeAreaView
193+
<View
194+
experimental_onSafeAreaInsetsChange={[Function]}
186195
style={
187-
Object {
188-
"alignItems": "center",
189-
"bottom": 20,
190-
"left": 10,
191-
"pointerEvents": "box-none",
192-
"position": "absolute",
193-
"right": 10,
194-
}
196+
Array [
197+
Object {
198+
"alignItems": "center",
199+
"bottom": 20,
200+
"left": 10,
201+
"pointerEvents": "box-none",
202+
"position": "absolute",
203+
"right": 10,
204+
},
205+
null,
206+
]
195207
}
196208
>
197209
<View
@@ -241,5 +253,5 @@ exports[`LogBoxNotificationContainer should render the latest warning notificati
241253
totalLogCount={2}
242254
/>
243255
</View>
244-
</SafeAreaView>
256+
</View>
245257
`;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import type {ViewProps} from '../../../../Libraries/Components/View/ViewPropTypes';
12+
import type {
13+
SafeAreaInsets,
14+
SafeAreaInsetsChangeEvent,
15+
} from '../../../../Libraries/Types/CoreEventTypes';
16+
import type {HostInstance} from '../../types/HostInstance';
17+
18+
import View from '../../../../Libraries/Components/View/View';
19+
import I18nManager from '../../../../Libraries/ReactNative/I18nManager';
20+
import * as React from 'react';
21+
import {useCallback, useMemo, useState} from 'react';
22+
23+
/**
24+
* Renders its children within the safe area of the device, by applying the part
25+
* of the view that is covered by the system UI as padding.
26+
*/
27+
component SafeAreaView(
28+
ref?: React.RefSetter<HostInstance>,
29+
...props: ViewProps
30+
) {
31+
const {style, experimental_onSafeAreaInsetsChange, ...otherProps} = props;
32+
const [insets, setInsets] = useState<?SafeAreaInsets>(null);
33+
34+
const handleSafeAreaInsetsChange = useCallback(
35+
(event: SafeAreaInsetsChangeEvent) => {
36+
setInsets(event.nativeEvent.insets);
37+
experimental_onSafeAreaInsetsChange?.(event);
38+
},
39+
[experimental_onSafeAreaInsetsChange],
40+
);
41+
42+
const paddingStyle = useMemo(() => {
43+
if (insets == null) {
44+
return null;
45+
}
46+
// Insets are physical edges, but Yoga remaps paddingLeft/paddingRight to
47+
// start/end when I18nManager's swapLeftAndRightInRTL is on, which would
48+
// pad the mirror-image edge in RTL. Swap the values so the physical edge
49+
// keeps its inset.
50+
const {isRTL, doLeftAndRightSwapInRTL} = I18nManager.getConstants();
51+
const swap = isRTL && doLeftAndRightSwapInRTL;
52+
return {
53+
paddingTop: insets.top,
54+
paddingRight: swap ? insets.left : insets.right,
55+
paddingBottom: insets.bottom,
56+
paddingLeft: swap ? insets.right : insets.left,
57+
};
58+
}, [insets]);
59+
60+
return (
61+
<View
62+
{...otherProps}
63+
ref={ref}
64+
experimental_onSafeAreaInsetsChange={handleSafeAreaInsetsChange}
65+
style={[style, paddingStyle]}
66+
/>
67+
);
68+
}
69+
70+
export default SafeAreaView;

‎packages/react-native/src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE.js‎

Lines changed: 0 additions & 31 deletions
This file was deleted.

packages/react-native/Libraries/Components/SafeAreaView/__tests__/SafeAreaView-itest.js renamed to packages/react-native/src/private/components/safeareaview/__tests__/SafeAreaView-itest.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
1212

1313
import * as Fantom from '@react-native/fantom';
1414
import * as React from 'react';
15-
import {SafeAreaView, Text} from 'react-native';
15+
import {Text} from 'react-native';
16+
import SafeAreaView from 'react-native/src/private/components/safeareaview/SafeAreaView';
1617

1718
describe('<SafeAreaView>', () => {
1819
it('renders with children', () => {

‎packages/react-native/src/private/devsupport/devmenu/elementinspector/Inspector.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type {
1818
import type {ViewStyleProp} from '../../../../../Libraries/StyleSheet/StyleSheet';
1919
import type {ReactDevToolsAgent} from '../../../../../Libraries/Types/ReactDevToolsTypes';
2020

21-
import SafeAreaView from '../../../components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE';
21+
import SafeAreaView from '../../../components/safeareaview/SafeAreaView';
2222
import * as React from 'react';
2323

2424
const View = require('../../../../../Libraries/Components/View/View').default;

0 commit comments

Comments
 (0)