Skip to content

Commit 11f37bd

Browse files
Render the internal SafeAreaView from the safe area insets prop
## 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`; #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 ## 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 | <img src="https://raw.githubusercontent.com/janicduplessis/react-native/cf6fa5bb55818587767eb6b47a10b8970864f932/ios-logbox-notification.png" width="200"> | <img src="https://raw.githubusercontent.com/janicduplessis/react-native/cf6fa5bb55818587767eb6b47a10b8970864f932/ios-logbox-inspector.png" width="200"> | <img src="https://raw.githubusercontent.com/janicduplessis/react-native/cf6fa5bb55818587767eb6b47a10b8970864f932/ios-inspector.png" width="200"> | <img src="https://raw.githubusercontent.com/janicduplessis/react-native/cf6fa5bb55818587767eb6b47a10b8970864f932/ios-iav-keyboard.png" width="200"><br>`InputAccessoryView` | | Android | <img src="https://raw.githubusercontent.com/janicduplessis/react-native/cf6fa5bb55818587767eb6b47a10b8970864f932/android-logbox-notification.png" width="200"> | <img src="https://raw.githubusercontent.com/janicduplessis/react-native/cf6fa5bb55818587767eb6b47a10b8970864f932/android-logbox-inspector.png" width="200"> | <img src="https://raw.githubusercontent.com/janicduplessis/react-native/cf6fa5bb55818587767eb6b47a10b8970864f932/android-inspector.png" width="200"> | <img src="https://raw.githubusercontent.com/janicduplessis/react-native/cf6fa5bb55818587767eb6b47a10b8970864f932/android-inspector-top.png" width="200"><br>Element inspector, panel at the top | --- 👉 1. #58112 — Render the internal SafeAreaView from the safe area insets prop 2. #58113 — Remove the native SafeAreaView and the deprecated public export Differential Revision: D126071736 Pull Request resolved: #58112
1 parent c483412 commit 11f37bd

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<<eda44383e13efcfb4f814115c4b44547>>
7+
* @generated SignedSource<<ed9c28d3d67e72e66c1a86081d61dee7>>
88
*
99
* This file was generated by scripts/js-api/build-types/index.js.
1010
*/
@@ -5980,7 +5980,8 @@ export {
59805980
RootViewStyleProvider, // d5909148
59815981
Runnable, // 594dd93a
59825982
Runnables, // 4367c557
5983-
SafeAreaInsetsChangeEvent, // cedb5309
5983+
SafeAreaInsets, // 64d7a605
5984+
SafeAreaInsetsChangeEvent, // 4ffd35e5
59845985
SafeAreaView, // dda99293
59855986
SafeAreaViewInstance, // 2b5d4391
59865987
ScaledSize, // 07e417c7

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ export type {
486486
NativeTouchEvent,
487487
NativeUIEvent,
488488
ResponderSyntheticEvent,
489+
SafeAreaInsets,
489490
SafeAreaInsetsChangeEvent,
490491
ScrollEvent,
491492
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)