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
@@ -0,0 +1,27 @@
/**
* 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 type {EdgeInsets, Metrics} from './SafeAreaViewTypes';

import NativeSafeAreaContext from '../../../src/private/specs_DEPRECATED/modules/NativeSafeAreaContext';

/**
* Safe area metrics available synchronously at startup, read from a native
* constant. Pass to `SafeAreaProvider`'s `initialMetrics` prop to avoid a
* first-frame layout jump. `null` when the native module is unavailable.
*/
export const initialWindowMetrics: Metrics | null =
NativeSafeAreaContext?.getConstants().initialWindowMetrics ?? null;

/**
* @deprecated Use `initialWindowMetrics` instead.
*/
export const initialWindowSafeAreaInsets: EdgeInsets | void =
initialWindowMetrics?.insets;
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/**
* 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 type {HostInstance} from '../../../src/private/types/HostInstance';
import type {NativeSyntheticEvent} from '../../Types/CoreEventTypes';
import type {ViewProps} from '../View/ViewPropTypes';
import type {EdgeInsets, Metrics, Rect} from './SafeAreaViewTypes';

import NativeSafeAreaProvider from '../../../src/private/components/safeareaprovider/specs/SafeAreaProviderNativeComponent';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Dimensions from '../../Utilities/Dimensions';
import * as React from 'react';

export type InsetChangedEvent = NativeSyntheticEvent<Metrics>;

export type SafeAreaProviderProps = Readonly<{
...ViewProps,
children?: React.Node,
/**
* Seed insets and frame synchronously so the first frame does not jump.
* Typically `initialWindowMetrics` from `./InitialWindow`.
*/
initialMetrics?: ?Metrics,
}>;

export const SafeAreaInsetsContext: React.Context<EdgeInsets | null> =
React.createContext<EdgeInsets | null>(null);

export const SafeAreaFrameContext: React.Context<Rect | null> =
React.createContext<Rect | null>(null);

if (__DEV__) {
SafeAreaInsetsContext.displayName = 'SafeAreaInsetsContext';
SafeAreaFrameContext.displayName = 'SafeAreaFrameContext';
}

export const SafeAreaProvider: component(
ref?: React.RefSetter<HostInstance>,
...props: SafeAreaProviderProps
) = React.forwardRef<SafeAreaProviderProps, HostInstance>(
function SafeAreaProvider(props, forwardedRef) {
const {children, initialMetrics, style, ...others} = props;

// Inherit from a parent provider so nested providers keep working.
const parentInsets = React.useContext(SafeAreaInsetsContext);
const parentFrame = React.useContext(SafeAreaFrameContext);

const [insets, setInsets] = React.useState<EdgeInsets | null>(
initialMetrics?.insets ?? parentInsets ?? null,
);
const [frame, setFrame] = React.useState<Rect>(
initialMetrics?.frame ??
parentFrame ?? {
x: 0,
y: 0,
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
},
);

const onInsetsChange = React.useCallback((event: InsetChangedEvent) => {
const {
nativeEvent: {frame: nextFrame, insets: nextInsets},
} = event;

setFrame(curFrame => {
if (
nextFrame != null &&
(nextFrame.height !== curFrame.height ||
nextFrame.width !== curFrame.width ||
nextFrame.x !== curFrame.x ||
nextFrame.y !== curFrame.y)
) {
return nextFrame;
}
return curFrame;
});

setInsets(curInsets => {
if (
curInsets == null ||
nextInsets.bottom !== curInsets.bottom ||
nextInsets.left !== curInsets.left ||
nextInsets.right !== curInsets.right ||
nextInsets.top !== curInsets.top
) {
return nextInsets;
}
return curInsets;
});
}, []);

return (
<NativeSafeAreaProvider
ref={forwardedRef}
style={[styles.fill, style]}
onInsetsChange={onInsetsChange}
{...others}>
{insets != null ? (
<SafeAreaFrameContext.Provider value={frame}>
<SafeAreaInsetsContext.Provider value={insets}>
{children}
</SafeAreaInsetsContext.Provider>
</SafeAreaFrameContext.Provider>
) : null}
</NativeSafeAreaProvider>
);
},
);

export type SafeAreaListenerProps = Readonly<{
...ViewProps,
children?: React.Node,
onChange: (data: {insets: EdgeInsets, frame: Rect}) => void,
}>;

/**
* Observe safe area changes without providing a React context. Useful for
* imperative consumers (animations, measurements) that do not want to re-render
* on every inset change.
*/
export function SafeAreaListener(props: SafeAreaListenerProps): React.Node {
const {onChange, style, children, ...others} = props;
return (
<NativeSafeAreaProvider
{...others}
style={[styles.fill, style]}
onInsetsChange={event => {
onChange({
insets: event.nativeEvent.insets,
frame: event.nativeEvent.frame,
});
}}>
{children}
</NativeSafeAreaProvider>
);
}

const NO_INSETS_ERROR =
'No safe area value available. Make sure you are rendering `<SafeAreaProvider>` at the top of your app.';

export function useSafeAreaInsets(): EdgeInsets {
const insets = React.useContext(SafeAreaInsetsContext);
if (insets == null) {
throw new Error(NO_INSETS_ERROR);
}
return insets;
}

export function useSafeAreaFrame(): Rect {
const frame = React.useContext(SafeAreaFrameContext);
if (frame == null) {
throw new Error(NO_INSETS_ERROR);
}
return frame;
}

export type WithSafeAreaInsetsProps = {
insets: EdgeInsets,
};

export function withSafeAreaInsets<Props: {...}>(
WrappedComponent: React.ComponentType<{...Props, +insets: EdgeInsets}>,
): React.ComponentType<Props> {
return function WithSafeAreaInsets(props: Props): React.Node {
const insets = useSafeAreaInsets();
return <WrappedComponent {...props} insets={insets} />;
};
}

/**
* @deprecated Use `useSafeAreaInsets` instead.
*/
export function useSafeArea(): EdgeInsets {
return useSafeAreaInsets();
}

/**
* @deprecated Use `SafeAreaInsetsContext.Consumer` instead.
*/
export const SafeAreaConsumer: React.ComponentType<
(value: EdgeInsets | null) => React.Node,
> = SafeAreaInsetsContext.Consumer;

/**
* @deprecated Use `SafeAreaInsetsContext` instead.
*/
export const SafeAreaContext: React.Context<EdgeInsets | null> =
SafeAreaInsetsContext;

const styles = StyleSheet.create({
fill: {flex: 1},
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,77 @@

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

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

export type SafeAreaViewInstance = HostInstance;

export type SafeAreaViewProps = Readonly<{
...ViewProps,
/**
* Apply the safe area insets as `padding` (default) or `margin`.
*/
mode?: 'padding' | 'margin',
/**
* Which edges to inset. Either a list of edges (each applied `additive`) or a
* per-edge record of `'off' | 'additive' | 'maximum'`. Defaults to all four
* edges `additive`.
*/
edges?: Edges,
}>;

const defaultEdges: {[Edge]: EdgeMode} = {
top: 'additive',
right: 'additive',
bottom: 'additive',
left: 'additive',
};

/**
* 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, applying padding
* (or margin) that reflects the portion of the view covered by system bars,
* notches, and other ancestor views.
*
* @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,
});
...props: SafeAreaViewProps
) = React.forwardRef<SafeAreaViewProps, SafeAreaViewInstance>(
({edges, mode, ...props}, ref) => {
const nativeEdges = React.useMemo(() => {
if (edges == null) {
return defaultEdges;
}
const edgesObj: EdgeRecord = Array.isArray(edges)
? edges.reduce(
(acc, edge) => {
acc[edge] = 'additive';
return acc;
},
({}: {[Edge]: EdgeMode}),
)
: edges;
// Fabric requires every edge to be present.
return {
top: edgesObj.top ?? 'off',
right: edgesObj.right ?? 'off',
bottom: edgesObj.bottom ?? 'off',
left: edgesObj.left ?? 'off',
};
}, [edges]);

return (
<SafeAreaViewNativeComponent
{...props}
mode={mode}
edges={nativeEdges}
ref={ref}
/>
);
},
);

export default SafeAreaView;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* 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
* @format
*/

export type EdgeInsets = {
top: number,
right: number,
bottom: number,
left: number,
};

export type Rect = {
x: number,
y: number,
width: number,
height: number,
};

export type Metrics = {
insets: EdgeInsets,
frame: Rect,
};

export type Edge = 'top' | 'right' | 'bottom' | 'left';

export type EdgeMode = 'off' | 'additive' | 'maximum';

export type EdgeRecord = Partial<{[edge: Edge]: EdgeMode}>;

export type Edges = ReadonlyArray<Edge> | Readonly<EdgeRecord>;
Loading
Loading