Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
575d3f1
Add an onSafeAreaInsetsChange view prop
janicduplessis Aug 14, 2026
92df10b
Only emit safe area insets from the layout pass
janicduplessis Aug 14, 2026
68feff4
Update C++ API snapshots
janicduplessis Aug 14, 2026
66de1bd
Report window safe area insets through Dimensions
janicduplessis Aug 14, 2026
eb2873e
Update C++ API snapshots for the generated DeviceInfo types
janicduplessis Aug 14, 2026
ffbcbd9
Fix ReactCxxPlatform DeviceInfoModule for the new generated DeviceInf…
janicduplessis Aug 14, 2026
00f8b95
Seed the internal SafeAreaView with the window insets from Dimensions
janicduplessis Aug 15, 2026
37a6af3
Process the safe area inset event at the call site, padding the first…
janicduplessis Aug 15, 2026
d670d3e
Make the modal example apply insets from a button press
janicduplessis Aug 15, 2026
bb47ed9
Render a marker in the example when observing without an inset event
janicduplessis Aug 15, 2026
f2bf245
Only emit safe area inset events when the insets change
janicduplessis Aug 16, 2026
29c63d6
Format the safe area example
janicduplessis Aug 16, 2026
cacbd00
Add tests for synchronous EventBeat processing
janicduplessis Aug 16, 2026
5b9f6a2
Add a keyboard probe to the safe area example
janicduplessis Aug 16, 2026
cae354e
Remove the native SafeAreaView implementations
janicduplessis Aug 16, 2026
875fc69
Induce the Apple event beat from the display phase on synchronous req…
janicduplessis Aug 16, 2026
8e24923
Address findings from independent code reviews
janicduplessis Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,19 @@
*/

import type {HostInstance} from '../../../src/private/types/HostInstance';
import type {ViewProps} from '../View/ViewPropTypes';

import Platform from '../../Utilities/Platform';
import View from '../View/View';
import * as React from 'react';
import SafeAreaView_INTERNAL_DO_NOT_USE from '../../../src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE';

export type SafeAreaViewInstance = HostInstance;

/**
* Renders content within the safe area boundaries of a device. Currently only applicable to iOS devices with iOS version 11 or later. Automatically applies padding to reflect the portion of the view not covered by navigation bars, tab bars, toolbars, and other ancestor views.
* Renders content within the safe area boundaries of a device, by applying the
* part of the view that is covered by the system UI as padding.
*
* @see https://reactnative.dev/docs/safeareaview
* @deprecated Use `react-native-safe-area-context` instead.
* @platform ios
*/
const SafeAreaView: component(
ref?: React.RefSetter<SafeAreaViewInstance>,
...props: ViewProps
) = Platform.select({
ios: require('./RCTSafeAreaViewNativeComponent').default,
default: View,
});
const SafeAreaView: typeof SafeAreaView_INTERNAL_DO_NOT_USE =
SafeAreaView_INTERNAL_DO_NOT_USE;

export default SafeAreaView;
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions packages/react-native/Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
LayoutRectangle,
MouseEvent,
PointerEvent,
SafeAreaInsetsChangeEvent,
} from '../../Types/CoreEventTypes';
import type {
AccessibilityActionEvent,
Expand Down Expand Up @@ -63,6 +64,30 @@ type DirectEventProps = Readonly<{
*/
onLayout?: ?(event: LayoutChangeEvent) => unknown,

/**
* Invoked when the part of this view that is covered by the system UI
* (status bar, navigation bar, home indicator, display cutouts, ...)
* changes, with:
*
* `{nativeEvent: {insets: {top, right, bottom, left}, frame: {x, y, width, height}}}`
*
* `insets` are relative to this view: an inset is only non-zero for the part
* of the view that actually overlaps the system UI. `frame` is the position
* of the view at the time of the event, relative to its enclosing view
* controller on iOS and to the window on Android; it does not trigger the
* event on its own, so it can be stale while the view moves without its
* insets changing.
*
* The event is dispatched synchronously, so the rendering it schedules is
* applied in the same frame the insets changed in.
*
* Setting this prop makes the view observe safe area changes; views without
* it are unaffected.
*
* @experimental
*/
onSafeAreaInsetsChange?: ?(event: SafeAreaInsetsChangeEvent) => unknown,

/**
* When `accessible` is `true`, the system will invoke this function when the
* user performs the magic tap gesture.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

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

import type {HostInstance} from 'react-native/src/private/types/HostInstance';

import * as Fantom from '@react-native/fantom';
import * as React from 'react';
import {createRef} from 'react';
import {View} from 'react-native';
import SafeAreaView from 'react-native/src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE';

const INSETS = {top: 44, right: 0, bottom: 34, left: 0};
const FRAME = {x: 0, y: 0, width: 390, height: 844};

describe('onSafeAreaInsetsChange', () => {
it('delivers the insets and the frame of the view', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();
const onSafeAreaInsetsChange = jest.fn();

Fantom.runTask(() => {
root.render(
<View
collapsable={false}
ref={nodeRef}
onSafeAreaInsetsChange={event => {
onSafeAreaInsetsChange(event.nativeEvent);
}}
/>,
);
});

Fantom.dispatchNativeEvent(nodeRef, 'safeAreaInsetsChange', {
insets: INSETS,
frame: FRAME,
});

expect(onSafeAreaInsetsChange).toHaveBeenCalledTimes(1);
const [event] = onSafeAreaInsetsChange.mock.lastCall;
expect(event.insets).toEqual(INSETS);
expect(event.frame).toEqual(FRAME);
});

it('is not delivered to views that did not opt in', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();

Fantom.runTask(() => {
root.render(<View collapsable={false} ref={nodeRef} />);
});

// The prop is what makes the view observe the safe area, so a view without
// it is never the target of the event.
expect(
root.getRenderedOutput({props: ['onSafeAreaInsetsChange']}).toJSX(),
).toEqual(<rn-view />);
});

it('prevents the view from being flattened', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(
// A layout-only view would ordinarily be flattened away; observing the
// safe area requires a host view to observe with.
<View onSafeAreaInsetsChange={() => {}}>
<View collapsable={false} />
</View>,
);
});

expect(
root.getRenderedOutput({props: ['onSafeAreaInsetsChange']}).toJSX(),
).toEqual(
<rn-view onSafeAreaInsetsChange="true">
<rn-view />
</rn-view>,
);
});

it('is reflected in the props of the view when set', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(
<View collapsable={false} onSafeAreaInsetsChange={() => {}} />,
);
});

expect(
root.getRenderedOutput({props: ['onSafeAreaInsetsChange']}).toJSX(),
).toEqual(<rn-view onSafeAreaInsetsChange="true" />);
});
});

describe('<SafeAreaView>', () => {
it('applies the insets it receives as padding', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();

Fantom.runTask(() => {
root.render(<SafeAreaView collapsable={false} ref={nodeRef} />);
});

expect(
root
.getRenderedOutput({
props: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
})
.toJSX(),
).toEqual(<rn-view />);

Fantom.dispatchNativeEvent(nodeRef, 'safeAreaInsetsChange', {
insets: INSETS,
frame: FRAME,
});

expect(
root
.getRenderedOutput({
props: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
})
.toJSX(),
).toEqual(
<rn-view
paddingBottom="34"
paddingLeft="0"
paddingRight="0"
paddingTop="44"
/>,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -27,13 +26,10 @@ type Props = Readonly<{
level: LogLevel,
}>;

const LogBoxInspectorHeaderSafeArea: React.ComponentType<ViewProps> =
Platform.OS === 'android' ? View : SafeAreaView;

export default function LogBoxInspectorHeader(props: Props): React.Node {
if (props.level === 'syntax') {
return (
<LogBoxInspectorHeaderSafeArea style={styles[props.level]}>
<SafeAreaView style={styles[props.level]}>
<View style={styles.header}>
<View style={styles.title}>
<Text
Expand All @@ -44,7 +40,7 @@ export default function LogBoxInspectorHeader(props: Props): React.Node {
</Text>
</View>
</View>
</LogBoxInspectorHeaderSafeArea>
</SafeAreaView>
);
}

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

return (
<LogBoxInspectorHeaderSafeArea style={styles[props.level]}>
<SafeAreaView style={styles[props.level]}>
<View style={styles.header}>
<LogBoxInspectorHeaderButton
id="logbox_header_button_prev"
Expand All @@ -81,7 +77,7 @@ export default function LogBoxInspectorHeader(props: Props): React.Node {
onPress={() => props.onSelectIndex(nextIndex)}
/>
</View>
</LogBoxInspectorHeaderSafeArea>
</SafeAreaView>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`LogBoxNotificationContainer should render both an error and warning notification 1`] = `
<RCTSafeAreaView
<View
onSafeAreaInsetsChange={[Function]}
style={
Object {
"bottom": 20,
"left": 10,
"position": "absolute",
"right": 10,
}
Array [
Object {
"bottom": 20,
"left": 10,
"position": "absolute",
"right": 10,
},
null,
]
}
>
<View
Expand Down Expand Up @@ -101,7 +105,7 @@ exports[`LogBoxNotificationContainer should render both an error and warning not
totalLogCount={1}
/>
</View>
</RCTSafeAreaView>
</View>
`;

exports[`LogBoxNotificationContainer should render null with no logs 1`] = `null`;
Expand All @@ -113,14 +117,18 @@ 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`] = `
<RCTSafeAreaView
<View
onSafeAreaInsetsChange={[Function]}
style={
Object {
"bottom": 20,
"left": 10,
"position": "absolute",
"right": 10,
}
Array [
Object {
"bottom": 20,
"left": 10,
"position": "absolute",
"right": 10,
},
null,
]
}
>
<View
Expand Down Expand Up @@ -168,18 +176,22 @@ exports[`LogBoxNotificationContainer should render the latest error notification
totalLogCount={2}
/>
</View>
</RCTSafeAreaView>
</View>
`;

exports[`LogBoxNotificationContainer should render the latest warning notification 1`] = `
<RCTSafeAreaView
<View
onSafeAreaInsetsChange={[Function]}
style={
Object {
"bottom": 20,
"left": 10,
"position": "absolute",
"right": 10,
}
Array [
Object {
"bottom": 20,
"left": 10,
"position": "absolute",
"right": 10,
},
null,
]
}
>
<View
Expand Down Expand Up @@ -227,5 +239,5 @@ exports[`LogBoxNotificationContainer should render the latest warning notificati
totalLogCount={2}
/>
</View>
</RCTSafeAreaView>
</View>
`;
Loading
Loading