Skip to content

Commit

Permalink
feat: expose header height in native-stack (#9774)
Browse files Browse the repository at this point in the history
  • Loading branch information
WoLewicki committed Aug 1, 2021
1 parent de84458 commit 20abccd
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 106 deletions.
6 changes: 3 additions & 3 deletions packages/elements/src/Header/getDefaultHeaderHeight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Layout } from '../types';

export default function getDefaultHeaderHeight(
layout: Layout,
modal: boolean,
modalPresentation: boolean,
statusBarHeight: number
): number {
let headerHeight;
Expand All @@ -13,7 +13,7 @@ export default function getDefaultHeaderHeight(

if (Platform.OS === 'ios') {
if (Platform.isPad) {
if (modal) {
if (modalPresentation) {
headerHeight = 56;
} else {
headerHeight = 50;
Expand All @@ -22,7 +22,7 @@ export default function getDefaultHeaderHeight(
if (isLandscape) {
headerHeight = 32;
} else {
if (modal) {
if (modalPresentation) {
headerHeight = 56;
} else {
headerHeight = 44;
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/Screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default function Screen(props: Props) {
value={isParentHeaderShown || headerShown !== false}
>
<HeaderHeightContext.Provider
value={headerShown ? headerHeight : parentHeaderHeight}
value={headerShown ? headerHeight : parentHeaderHeight ?? 0}
>
{children}
</HeaderHeightContext.Provider>
Expand Down
273 changes: 172 additions & 101 deletions packages/native-stack/src/views/NativeStackView.native.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { SafeAreaProviderCompat } from '@react-navigation/elements';
import {
getDefaultHeaderHeight,
HeaderHeightContext,
HeaderShownContext,
SafeAreaProviderCompat,
} from '@react-navigation/elements';
import {
ParamListBase,
Route,
Expand All @@ -7,7 +12,11 @@ import {
useTheme,
} from '@react-navigation/native';
import * as React from 'react';
import { Platform, StyleSheet } from 'react-native';
import { Platform, PlatformIOSStatic, StyleSheet } from 'react-native';
import {
useSafeAreaFrame,
useSafeAreaInsets,
} from 'react-native-safe-area-context';
import {
Screen,
ScreenStack,
Expand All @@ -16,6 +25,7 @@ import {
import warnOnce from 'warn-once';

import type {
NativeStackDescriptor,
NativeStackDescriptorMap,
NativeStackNavigationHelpers,
NativeStackNavigationOptions,
Expand Down Expand Up @@ -72,12 +82,33 @@ const MaybeNestedStack = ({
</DebugContainer>
);

const insets = useSafeAreaInsets();
const dimensions = useSafeAreaFrame();
// landscape is meaningful only for iPhone
const isLandscape =
dimensions.width > dimensions.height &&
!(Platform as PlatformIOSStatic).isPad &&
!(Platform as PlatformIOSStatic).isTVOS;
// `modal` and `formSheet` presentations do not take whole screen, so should not take the inset.
const isFullScreenModal =
presentation !== 'modal' && presentation !== 'formSheet';
const topInset = isFullScreenModal && !isLandscape ? insets.top : 0;
const headerHeight = getDefaultHeaderHeight(
dimensions,
!isFullScreenModal,
topInset
);

if (isHeaderInModal) {
return (
<ScreenStack style={styles.container}>
<Screen enabled style={StyleSheet.absoluteFill}>
<HeaderConfig {...options} route={route} />
{content}
<HeaderShownContext.Provider value>
<HeaderHeightContext.Provider value={headerHeight}>
<HeaderConfig {...options} route={route} />
{content}
</HeaderHeightContext.Provider>
</HeaderShownContext.Provider>
</Screen>
</ScreenStack>
);
Expand All @@ -86,6 +117,106 @@ const MaybeNestedStack = ({
return content;
};

type SceneViewProps = {
index: number;
descriptor: NativeStackDescriptor;
onWillDisappear: () => void;
onAppear: () => void;
onDisappear: () => void;
onDismissed: () => void;
};

const SceneView = ({
descriptor,
index,
onWillDisappear,
onAppear,
onDisappear,
onDismissed,
}: SceneViewProps) => {
const { route, options, render } = descriptor;
const {
gestureEnabled,
headerShown,
animationTypeForReplace = 'pop',
animation,
orientation,
statusBarAnimation,
statusBarHidden,
statusBarStyle,
} = options;

let { presentation = 'card' } = options;

if (index === 0) {
// first screen should always be treated as `card`, it resolves problems with no header animation
// for navigator with first screen as `modal` and the next as `card`
presentation = 'card';
}

const isHeaderInPush = isAndroid
? headerShown
: presentation === 'card' && headerShown !== false;

const isParentHeaderShown = React.useContext(HeaderShownContext);
const insets = useSafeAreaInsets();
const parentHeaderHeight = React.useContext(HeaderHeightContext);
const headerHeight = getDefaultHeaderHeight(
useSafeAreaFrame(),
false,
insets.top
);

return (
<Screen
key={route.key}
enabled
style={StyleSheet.absoluteFill}
gestureEnabled={
isAndroid
? // This prop enables handling of system back gestures on Android
// Since we handle them in JS side, we disable this
false
: gestureEnabled
}
replaceAnimation={animationTypeForReplace}
stackPresentation={presentation === 'card' ? 'push' : presentation}
stackAnimation={animation}
screenOrientation={orientation}
statusBarAnimation={statusBarAnimation}
statusBarHidden={statusBarHidden}
statusBarStyle={statusBarStyle}
onWillDisappear={onWillDisappear}
onAppear={onAppear}
onDisappear={onDisappear}
onDismissed={onDismissed}
>
<HeaderShownContext.Provider
value={isParentHeaderShown || isHeaderInPush !== false}
>
<HeaderHeightContext.Provider
value={
isHeaderInPush !== false ? headerHeight : parentHeaderHeight ?? 0
}
>
<HeaderConfig
{...options}
route={route}
headerShown={isHeaderInPush}
/>
<MaybeNestedStack
options={options}
route={route}
presentation={presentation}
>
{render()}
</MaybeNestedStack>
</HeaderHeightContext.Provider>
</HeaderShownContext.Provider>
</Screen>
);
};

type Props = {
state: StackNavigationState<ParamListBase>;
navigation: NativeStackNavigationHelpers;
Expand Down Expand Up @@ -113,103 +244,43 @@ function NativeStackViewInner({ state, navigation, descriptors }: Props) {

return (
<ScreenStack style={styles.container}>
{state.routes.map((route, index) => {
const { options, render: renderScene } = descriptors[route.key];
const {
gestureEnabled,
headerShown,
animationTypeForReplace = 'pop',
animation,
orientation,
statusBarAnimation,
statusBarHidden,
statusBarStyle,
} = options;

let { presentation = 'card' } = options;

if (index === 0) {
// first screen should always be treated as `card`, it resolves problems with no header animation
// for navigator with first screen as `modal` and the next as `card`
presentation = 'card';
}

const isHeaderInPush = isAndroid
? headerShown
: presentation === 'card' && headerShown !== false;

return (
<Screen
key={route.key}
enabled
style={StyleSheet.absoluteFill}
gestureEnabled={
isAndroid
? // This prop enables handling of system back gestures on Android
// Since we handle them in JS side, we disable this
false
: gestureEnabled
}
replaceAnimation={animationTypeForReplace}
stackPresentation={presentation === 'card' ? 'push' : presentation}
stackAnimation={animation}
screenOrientation={orientation}
statusBarAnimation={statusBarAnimation}
statusBarHidden={statusBarHidden}
statusBarStyle={statusBarStyle}
onWillAppear={() => {
navigation.emit({
type: 'transitionStart',
data: { closing: false },
target: route.key,
});
}}
onWillDisappear={() => {
navigation.emit({
type: 'transitionStart',
data: { closing: true },
target: route.key,
});
}}
onAppear={() => {
navigation.emit({
type: 'transitionEnd',
data: { closing: false },
target: route.key,
});
}}
onDisappear={() => {
navigation.emit({
type: 'transitionEnd',
data: { closing: true },
target: route.key,
});
}}
onDismissed={() => {
navigation.dispatch({
...StackActions.pop(),
source: route.key,
target: state.key,
});

setNextDismissedKey(route.key);
}}
>
<HeaderConfig
{...options}
route={route}
headerShown={isHeaderInPush}
/>
<MaybeNestedStack
options={options}
route={route}
presentation={presentation}
>
{renderScene()}
</MaybeNestedStack>
</Screen>
);
})}
{state.routes.map((route, index) => (
<SceneView
key={route.key}
index={index}
descriptor={descriptors[route.key]}
onWillDisappear={() => {
navigation.emit({
type: 'transitionStart',
data: { closing: true },
target: route.key,
});
}}
onAppear={() => {
navigation.emit({
type: 'transitionEnd',
data: { closing: false },
target: route.key,
});
}}
onDisappear={() => {
navigation.emit({
type: 'transitionEnd',
data: { closing: true },
target: route.key,
});
}}
onDismissed={() => {
navigation.dispatch({
...StackActions.pop(),
source: route.key,
target: state.key,
});

setNextDismissedKey(route.key);
}}
/>
))}
</ScreenStack>
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/stack/src/views/Stack/CardContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ function CardContainer({
value={isParentHeaderShown || headerShown !== false}
>
<HeaderHeightContext.Provider
value={headerShown ? headerHeight : parentHeaderHeight}
value={headerShown ? headerHeight : parentHeaderHeight ?? 0}
>
{renderScene({ route: scene.descriptor.route })}
</HeaderHeightContext.Provider>
Expand Down

0 comments on commit 20abccd

Please sign in to comment.