Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
21 changes: 21 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,26 @@ 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, ...) or the
* position of this view in the window 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.
*
* 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,120 @@
/**
* 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('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>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ const directEventTypes = {
topLayout: {
registrationName: 'onLayout',
},
topSafeAreaInsetsChange: {
registrationName: 'onSafeAreaInsetsChange',
},
};

const validAttributesForNonEventProps = {
Expand Down Expand Up @@ -402,6 +405,7 @@ const validAttributesForNonEventProps = {
// Props for bubbling and direct events
const validAttributesForEventProps = {
onLayout: true,
onSafeAreaInsetsChange: true,

// PanResponder handlers
onMoveShouldSetResponder: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ const directEventTypes = {
topLayout: {
registrationName: 'onLayout',
},
topSafeAreaInsetsChange: {
registrationName: 'onSafeAreaInsetsChange',
},
onGestureHandlerEvent: DynamicallyInjectedByGestureHandler({
registrationName: 'onGestureHandlerEvent',
}),
Expand Down Expand Up @@ -380,6 +383,7 @@ const validAttributesForNonEventProps = {
// Props for bubbling and direct events
const validAttributesForEventProps = ConditionallyIgnoredEventHandlers({
onLayout: true,
onSafeAreaInsetsChange: true,
onMagicTap: true,

// Accessibility
Expand Down
21 changes: 21 additions & 0 deletions packages/react-native/Libraries/Types/CoreEventTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ export type LayoutChangeEvent = NativeSyntheticEvent<
}>,
>;

export type SafeAreaInsets = Readonly<{
top: number,
right: number,
bottom: number,
left: number,
}>;

export type SafeAreaInsetsChangeEvent = NativeSyntheticEvent<
Readonly<{
/**
* The part of the view that is covered by the system UI, in the view's own
* coordinate space.
*/
insets: SafeAreaInsets,
/**
* The frame of the view in window coordinates.
*/
frame: LayoutRectangle,
}>,
>;

/**
* @deprecated Use `TextLayoutEvent` instead.
*/
Expand Down
Loading
Loading