Skip to content

Commit 4287815

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 4287815

12 files changed

Lines changed: 196 additions & 74 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_INTERNAL_DO_NOT_USE';
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
@@ -12,6 +12,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
1212

1313
import type {HostInstance} from 'react-native';
1414

15+
import SafeAreaView from '../../../../src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE';
1516
import * as Fantom from '@react-native/fantom';
1617
import * as React from 'react';
1718
import {createRef} from 'react';
@@ -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/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_INTERNAL_DO_NOT_USE';
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_INTERNAL_DO_NOT_USE';
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
`;

‎packages/react-native/ReactNativeApi.d.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<2ceaa8e472a53fc667994edb63ccd995>>
7+
* @generated SignedSource<<1faa2820bf867aa599cdd390c5c90b00>>
88
*
99
* This file was generated by scripts/js-api/build-types/index.js.
1010
*/
@@ -5968,7 +5968,8 @@ export {
59685968
RootViewStyleProvider, // d5909148
59695969
Runnable, // 594dd93a
59705970
Runnables, // 4367c557
5971-
SafeAreaInsetsChangeEvent, // cedb5309
5971+
SafeAreaInsets, // 64d7a605
5972+
SafeAreaInsetsChangeEvent, // 4ffd35e5
59725973
SafeAreaView, // d917b6ec
59735974
SafeAreaViewInstance, // 2b5d4391
59745975
ScaledSize, // 07e417c7

‎packages/react-native/index.js.flow‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ export type {
485485
NativeTouchEvent,
486486
NativeUIEvent,
487487
ResponderSyntheticEvent,
488+
SafeAreaInsets,
488489
SafeAreaInsetsChangeEvent,
489490
ScrollEvent,
490491
TargetedEvent,

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

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,62 @@
99
*/
1010

1111
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';
1217

1318
import View from '../../../../Libraries/Components/View/View';
14-
import UIManager from '../../../../Libraries/ReactNative/UIManager';
15-
import Platform from '../../../../Libraries/Utilities/Platform';
19+
import I18nManager from '../../../../Libraries/ReactNative/I18nManager';
1620
import * as React from 'react';
21+
import {useCallback, useMemo, useState} from 'react';
1722

18-
const exported: component(
19-
ref?: React.RefSetter<React.ElementRef<typeof View>>,
20-
...ViewProps
21-
) = Platform.select({
22-
ios: require('../../../../src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent')
23-
.default,
24-
android: UIManager.hasViewManagerConfig('RCTSafeAreaView')
25-
? require('../../../../src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent')
26-
.default
27-
: View,
28-
default: View,
29-
});
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+
}
3069

31-
export default exported;
70+
export default SafeAreaView;

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
@@ -10,9 +10,10 @@
1010

1111
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
1212

13+
import SafeAreaView from '../SafeAreaView_INTERNAL_DO_NOT_USE';
1314
import * as Fantom from '@react-native/fantom';
1415
import * as React from 'react';
15-
import {SafeAreaView, Text} from 'react-native';
16+
import {Text} from 'react-native';
1617

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import type {ElementsHierarchy, InspectedElement} from './Inspector';
1414

15-
import SafeAreaView from '../../../../../Libraries/Components/SafeAreaView/SafeAreaView';
15+
import SafeAreaView from '../../../components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE';
1616
import * as React from 'react';
1717

1818
const ScrollView =

0 commit comments

Comments
 (0)