Skip to content

Commit 66912fe

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
SafeAreaProvider, safe-area hooks, and SafeAreaView edges/mode
Summary: Brings React Native core's safe-area API to functional parity with `react-native-safe-area-context`. This change adds the shared JS API and shared C++ layout logic, and wires up the Android native implementation (iOS follows in a subsequent change). New public API (exported from `react-native`): - `SafeAreaProvider` — measures the window safe-area insets and frame and provides them via context. - `useSafeAreaInsets()` / `useSafeAreaFrame()` hooks, plus `SafeAreaInsetsContext` / `SafeAreaFrameContext`, `withSafeAreaInsets` HOC, and `SafeAreaListener`. - `initialWindowMetrics` for synchronously seeding insets/frame to avoid a first-frame jump. `SafeAreaView` gains `edges` (per-edge `off` / `additive` / `maximum`) and `mode` (`padding` | `margin`) props. The inset-to-padding/margin conversion lives in the shared C++ shadow node (`SafeAreaViewShadowNode::adjustLayoutWithState`) driven from the component descriptor's `adopt`, so iOS and Android share one implementation. On Android, `SafeAreaView` now reports insets into Fabric state without consuming them, so nested providers/views compute correctly, and a new native `SafeAreaProvider` component reports insets and frame to JS via an `onInsetsChange` event. This targets the new architecture (Fabric). Changelog: [Android][Added] - Add `SafeAreaProvider`, `useSafeAreaInsets`/`useSafeAreaFrame`, and `edges`/`mode` props on `SafeAreaView` Differential Revision: D111273335
1 parent 5976273 commit 66912fe

21 files changed

Lines changed: 1101 additions & 37 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 {EdgeInsets, Metrics} from './SafeAreaViewTypes';
12+
13+
import NativeSafeAreaContext from '../../../src/private/specs_DEPRECATED/modules/NativeSafeAreaContext';
14+
15+
/**
16+
* Safe area metrics available synchronously at startup, read from a native
17+
* constant. Pass to `SafeAreaProvider`'s `initialMetrics` prop to avoid a
18+
* first-frame layout jump. `null` when the native module is unavailable.
19+
*/
20+
export const initialWindowMetrics: Metrics | null =
21+
NativeSafeAreaContext?.getConstants().initialWindowMetrics ?? null;
22+
23+
/**
24+
* @deprecated Use `initialWindowMetrics` instead.
25+
*/
26+
export const initialWindowSafeAreaInsets: EdgeInsets | void =
27+
initialWindowMetrics?.insets;
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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 {HostInstance} from '../../../src/private/types/HostInstance';
12+
import type {NativeSyntheticEvent} from '../../Types/CoreEventTypes';
13+
import type {ViewProps} from '../View/ViewPropTypes';
14+
import type {EdgeInsets, Metrics, Rect} from './SafeAreaViewTypes';
15+
16+
import NativeSafeAreaProvider from '../../../src/private/components/safeareaprovider/specs/SafeAreaProviderNativeComponent';
17+
import StyleSheet from '../../StyleSheet/StyleSheet';
18+
import Dimensions from '../../Utilities/Dimensions';
19+
import * as React from 'react';
20+
21+
export type InsetChangedEvent = NativeSyntheticEvent<Metrics>;
22+
23+
export type SafeAreaProviderProps = Readonly<{
24+
...ViewProps,
25+
children?: React.Node,
26+
/**
27+
* Seed insets and frame synchronously so the first frame does not jump.
28+
* Typically `initialWindowMetrics` from `./InitialWindow`.
29+
*/
30+
initialMetrics?: ?Metrics,
31+
}>;
32+
33+
export const SafeAreaInsetsContext: React.Context<EdgeInsets | null> =
34+
React.createContext<EdgeInsets | null>(null);
35+
36+
export const SafeAreaFrameContext: React.Context<Rect | null> =
37+
React.createContext<Rect | null>(null);
38+
39+
if (__DEV__) {
40+
SafeAreaInsetsContext.displayName = 'SafeAreaInsetsContext';
41+
SafeAreaFrameContext.displayName = 'SafeAreaFrameContext';
42+
}
43+
44+
export const SafeAreaProvider: component(
45+
ref?: React.RefSetter<HostInstance>,
46+
...props: SafeAreaProviderProps
47+
) = React.forwardRef<SafeAreaProviderProps, HostInstance>(
48+
function SafeAreaProvider(props, forwardedRef) {
49+
const {children, initialMetrics, style, ...others} = props;
50+
51+
// Inherit from a parent provider so nested providers keep working.
52+
const parentInsets = React.useContext(SafeAreaInsetsContext);
53+
const parentFrame = React.useContext(SafeAreaFrameContext);
54+
55+
const [insets, setInsets] = React.useState<EdgeInsets | null>(
56+
initialMetrics?.insets ?? parentInsets ?? null,
57+
);
58+
const [frame, setFrame] = React.useState<Rect>(
59+
initialMetrics?.frame ??
60+
parentFrame ?? {
61+
x: 0,
62+
y: 0,
63+
width: Dimensions.get('window').width,
64+
height: Dimensions.get('window').height,
65+
},
66+
);
67+
68+
const onInsetsChange = React.useCallback((event: InsetChangedEvent) => {
69+
const {
70+
nativeEvent: {frame: nextFrame, insets: nextInsets},
71+
} = event;
72+
73+
setFrame(curFrame => {
74+
if (
75+
nextFrame != null &&
76+
(nextFrame.height !== curFrame.height ||
77+
nextFrame.width !== curFrame.width ||
78+
nextFrame.x !== curFrame.x ||
79+
nextFrame.y !== curFrame.y)
80+
) {
81+
return nextFrame;
82+
}
83+
return curFrame;
84+
});
85+
86+
setInsets(curInsets => {
87+
if (
88+
curInsets == null ||
89+
nextInsets.bottom !== curInsets.bottom ||
90+
nextInsets.left !== curInsets.left ||
91+
nextInsets.right !== curInsets.right ||
92+
nextInsets.top !== curInsets.top
93+
) {
94+
return nextInsets;
95+
}
96+
return curInsets;
97+
});
98+
}, []);
99+
100+
return (
101+
<NativeSafeAreaProvider
102+
ref={forwardedRef}
103+
style={[styles.fill, style]}
104+
onInsetsChange={onInsetsChange}
105+
{...others}>
106+
{insets != null ? (
107+
<SafeAreaFrameContext.Provider value={frame}>
108+
<SafeAreaInsetsContext.Provider value={insets}>
109+
{children}
110+
</SafeAreaInsetsContext.Provider>
111+
</SafeAreaFrameContext.Provider>
112+
) : null}
113+
</NativeSafeAreaProvider>
114+
);
115+
},
116+
);
117+
118+
export type SafeAreaListenerProps = Readonly<{
119+
...ViewProps,
120+
children?: React.Node,
121+
onChange: (data: {insets: EdgeInsets, frame: Rect}) => void,
122+
}>;
123+
124+
/**
125+
* Observe safe area changes without providing a React context. Useful for
126+
* imperative consumers (animations, measurements) that do not want to re-render
127+
* on every inset change.
128+
*/
129+
export function SafeAreaListener(props: SafeAreaListenerProps): React.Node {
130+
const {onChange, style, children, ...others} = props;
131+
return (
132+
<NativeSafeAreaProvider
133+
{...others}
134+
style={[styles.fill, style]}
135+
onInsetsChange={event => {
136+
onChange({
137+
insets: event.nativeEvent.insets,
138+
frame: event.nativeEvent.frame,
139+
});
140+
}}>
141+
{children}
142+
</NativeSafeAreaProvider>
143+
);
144+
}
145+
146+
const NO_INSETS_ERROR =
147+
'No safe area value available. Make sure you are rendering `<SafeAreaProvider>` at the top of your app.';
148+
149+
export function useSafeAreaInsets(): EdgeInsets {
150+
const insets = React.useContext(SafeAreaInsetsContext);
151+
if (insets == null) {
152+
throw new Error(NO_INSETS_ERROR);
153+
}
154+
return insets;
155+
}
156+
157+
export function useSafeAreaFrame(): Rect {
158+
const frame = React.useContext(SafeAreaFrameContext);
159+
if (frame == null) {
160+
throw new Error(NO_INSETS_ERROR);
161+
}
162+
return frame;
163+
}
164+
165+
export type WithSafeAreaInsetsProps = {
166+
insets: EdgeInsets,
167+
};
168+
169+
export function withSafeAreaInsets<Props: {...}>(
170+
WrappedComponent: React.ComponentType<{...Props, +insets: EdgeInsets}>,
171+
): React.ComponentType<Props> {
172+
return function WithSafeAreaInsets(props: Props): React.Node {
173+
const insets = useSafeAreaInsets();
174+
return <WrappedComponent {...props} insets={insets} />;
175+
};
176+
}
177+
178+
/**
179+
* @deprecated Use `useSafeAreaInsets` instead.
180+
*/
181+
export function useSafeArea(): EdgeInsets {
182+
return useSafeAreaInsets();
183+
}
184+
185+
/**
186+
* @deprecated Use `SafeAreaInsetsContext.Consumer` instead.
187+
*/
188+
export const SafeAreaConsumer: React.ComponentType<
189+
(value: EdgeInsets | null) => React.Node,
190+
> = SafeAreaInsetsContext.Consumer;
191+
192+
/**
193+
* @deprecated Use `SafeAreaInsetsContext` instead.
194+
*/
195+
export const SafeAreaContext: React.Context<EdgeInsets | null> =
196+
SafeAreaInsetsContext;
197+
198+
const styles = StyleSheet.create({
199+
fill: {flex: 1},
200+
});

packages/react-native/Libraries/Components/SafeAreaView/SafeAreaView.js

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,77 @@
1010

1111
import type {HostInstance} from '../../../src/private/types/HostInstance';
1212
import type {ViewProps} from '../View/ViewPropTypes';
13+
import type {Edge, EdgeMode, EdgeRecord, Edges} from './SafeAreaViewTypes';
1314

14-
import Platform from '../../Utilities/Platform';
15-
import View from '../View/View';
15+
import SafeAreaViewNativeComponent from '../../../src/private/components/safeareaview/specs/RCTSafeAreaViewNativeComponent';
1616
import * as React from 'react';
1717

1818
export type SafeAreaViewInstance = HostInstance;
1919

20+
export type SafeAreaViewProps = Readonly<{
21+
...ViewProps,
22+
/**
23+
* Apply the safe area insets as `padding` (default) or `margin`.
24+
*/
25+
mode?: 'padding' | 'margin',
26+
/**
27+
* Which edges to inset. Either a list of edges (each applied `additive`) or a
28+
* per-edge record of `'off' | 'additive' | 'maximum'`. Defaults to all four
29+
* edges `additive`.
30+
*/
31+
edges?: Edges,
32+
}>;
33+
34+
const defaultEdges: {[Edge]: EdgeMode} = {
35+
top: 'additive',
36+
right: 'additive',
37+
bottom: 'additive',
38+
left: 'additive',
39+
};
40+
2041
/**
21-
* 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.
42+
* Renders content within the safe area boundaries of a device, applying padding
43+
* (or margin) that reflects the portion of the view covered by system bars,
44+
* notches, and other ancestor views.
2245
*
2346
* @see https://reactnative.dev/docs/safeareaview
24-
* @deprecated Use `react-native-safe-area-context` instead.
25-
* @platform ios
2647
*/
2748
const SafeAreaView: component(
2849
ref?: React.RefSetter<SafeAreaViewInstance>,
29-
...props: ViewProps
30-
) = Platform.select({
31-
ios: require('./RCTSafeAreaViewNativeComponent').default,
32-
default: View,
33-
});
50+
...props: SafeAreaViewProps
51+
) = React.forwardRef<SafeAreaViewProps, SafeAreaViewInstance>(
52+
({edges, mode, ...props}, ref) => {
53+
const nativeEdges = React.useMemo(() => {
54+
if (edges == null) {
55+
return defaultEdges;
56+
}
57+
const edgesObj: EdgeRecord = Array.isArray(edges)
58+
? edges.reduce(
59+
(acc, edge) => {
60+
acc[edge] = 'additive';
61+
return acc;
62+
},
63+
({}: {[Edge]: EdgeMode}),
64+
)
65+
: edges;
66+
// Fabric requires every edge to be present.
67+
return {
68+
top: edgesObj.top ?? 'off',
69+
right: edgesObj.right ?? 'off',
70+
bottom: edgesObj.bottom ?? 'off',
71+
left: edgesObj.left ?? 'off',
72+
};
73+
}, [edges]);
74+
75+
return (
76+
<SafeAreaViewNativeComponent
77+
{...props}
78+
mode={mode}
79+
edges={nativeEdges}
80+
ref={ref}
81+
/>
82+
);
83+
},
84+
);
3485

3586
export default SafeAreaView;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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
8+
* @format
9+
*/
10+
11+
export type EdgeInsets = {
12+
top: number,
13+
right: number,
14+
bottom: number,
15+
left: number,
16+
};
17+
18+
export type Rect = {
19+
x: number,
20+
y: number,
21+
width: number,
22+
height: number,
23+
};
24+
25+
export type Metrics = {
26+
insets: EdgeInsets,
27+
frame: Rect,
28+
};
29+
30+
export type Edge = 'top' | 'right' | 'bottom' | 'left';
31+
32+
export type EdgeMode = 'off' | 'additive' | 'maximum';
33+
34+
export type EdgeRecord = Partial<{[edge: Edge]: EdgeMode}>;
35+
36+
export type Edges = ReadonlyArray<Edge> | Readonly<EdgeRecord>;

0 commit comments

Comments
 (0)