Skip to content

Commit

Permalink
feat: add custom header option to native-stack
Browse files Browse the repository at this point in the history
  • Loading branch information
satya164 committed Aug 1, 2021
1 parent 20abccd commit 1a39632
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 93 deletions.
29 changes: 29 additions & 0 deletions packages/native-stack/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
NavigationHelpers,
NavigationProp,
ParamListBase,
Route,
RouteProp,
StackActionHelpers,
StackNavigationState,
Expand Down Expand Up @@ -60,11 +61,39 @@ export type NativeStackNavigationHelpers = NavigationHelpers<
// We want it to be an empty object because navigator does not have any additional props
export type NativeStackNavigationConfig = {};

export type NativeStackHeaderProps = {
/**
* Options for the back button.
*/
back?: {
/**
* Title of the previous screen.
*/
title: string;
};
/**
* Options for the current screen.
*/
options: NativeStackNavigationOptions;
/**
* Route object for the current screen.
*/
route: Route<string>;
/**
* Navigation prop for the header.
*/
navigation: NativeStackNavigationProp<ParamListBase>;
};

export type NativeStackNavigationOptions = {
/**
* String that can be displayed in the header as a fallback for `headerTitle`.
*/
title?: string;
/**
* Function that given `HeaderProps` returns a React Element to display as a header.
*/
header?: (props: NativeStackHeaderProps) => React.ReactNode;
/**
* Whether the back button is visible in the header.
* You can use it to show a back button alongside `headerLeft` if you have specified it.
Expand Down
118 changes: 74 additions & 44 deletions packages/native-stack/src/views/NativeStackView.native.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
getDefaultHeaderHeight,
getHeaderTitle,
HeaderHeightContext,
HeaderShownContext,
SafeAreaProviderCompat,
Expand Down Expand Up @@ -47,11 +48,11 @@ const MaybeNestedStack = ({
children: React.ReactNode;
}) => {
const { colors } = useTheme();
const { headerShown = true, contentStyle } = options;
const { header, headerShown = true, contentStyle } = options;

const isHeaderInModal = isAndroid
? false
: presentation !== 'card' && headerShown === true;
: presentation !== 'card' && headerShown === true && header === undefined;

const headerShownPreviousRef = React.useRef(headerShown);

Expand Down Expand Up @@ -120,6 +121,7 @@ const MaybeNestedStack = ({
type SceneViewProps = {
index: number;
descriptor: NativeStackDescriptor;
previousDescriptor?: NativeStackDescriptor;
onWillDisappear: () => void;
onAppear: () => void;
onDisappear: () => void;
Expand All @@ -128,15 +130,17 @@ type SceneViewProps = {

const SceneView = ({
descriptor,
previousDescriptor,
index,
onWillDisappear,
onAppear,
onDisappear,
onDismissed,
}: SceneViewProps) => {
const { route, options, render } = descriptor;
const { route, navigation, options, render } = descriptor;
const {
gestureEnabled,
header,
headerShown,
animationTypeForReplace = 'pop',
animation,
Expand Down Expand Up @@ -199,11 +203,28 @@ const SceneView = ({
isHeaderInPush !== false ? headerHeight : parentHeaderHeight ?? 0
}
>
<HeaderConfig
{...options}
route={route}
headerShown={isHeaderInPush}
/>
{header !== undefined && headerShown !== false ? (
// TODO: expose custom header height
header({
back: previousDescriptor
? {
title: getHeaderTitle(
previousDescriptor.options,
previousDescriptor.route.name
),
}
: undefined,
options,
route,
navigation,
})
) : (
<HeaderConfig
{...options}
route={route}
headerShown={isHeaderInPush}
/>
)}
<MaybeNestedStack
options={options}
route={route}
Expand Down Expand Up @@ -244,43 +265,52 @@ function NativeStackViewInner({ state, navigation, descriptors }: Props) {

return (
<ScreenStack style={styles.container}>
{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,
});
{state.routes.map((route, index) => {
const descriptor = descriptors[route.key];
const previousKey = state.routes[index - 1]?.key;
const previousDescriptor = previousKey
? descriptors[previousKey]
: undefined;

setNextDismissedKey(route.key);
}}
/>
))}
return (
<SceneView
key={route.key}
index={index}
descriptor={descriptor}
previousDescriptor={previousDescriptor}
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
122 changes: 73 additions & 49 deletions packages/native-stack/src/views/NativeStackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@ export default function NativeStackView({ state, descriptors }: Props) {
{state.routes.map((route, i) => {
const isFocused = state.index === i;
const canGoBack = i !== 0;
const previousKey = state.routes[i - 1]?.key;
const previousDescriptor = previousKey
? descriptors[previousKey]
: undefined;
const { options, navigation, render } = descriptors[route.key];

const {
header,
headerShown,
headerTintColor,
headerBackImageSource,
Expand All @@ -56,57 +61,76 @@ export default function NativeStackView({ state, descriptors }: Props) {
navigation={navigation}
headerShown={headerShown}
header={
<Header
title={getHeaderTitle(options, route.name)}
headerTintColor={headerTintColor}
headerLeft={
typeof headerLeft === 'function'
? ({ tintColor }) => headerLeft({ tintColor })
: headerLeft === undefined && canGoBack
? ({ tintColor }) => (
<HeaderBackButton
tintColor={tintColor}
backImage={
headerBackImageSource !== undefined
? () => (
<Image
source={headerBackImageSource}
style={[styles.backImage, { tintColor }]}
/>
)
: undefined
}
onPress={navigation.goBack}
canGoBack={canGoBack}
/>
)
: headerLeft
}
headerRight={
typeof headerRight === 'function'
? ({ tintColor }) => headerRight({ tintColor })
: headerRight
}
headerTitle={
typeof headerTitle === 'function'
? ({ children, tintColor }) =>
headerTitle({ children, tintColor })
: headerTitle
}
headerTitleStyle={headerTitleStyle}
headerStyle={[
headerTranslucent
header !== undefined ? (
header({
back: previousDescriptor
? {
position: 'absolute',
backgroundColor: 'transparent',
title: getHeaderTitle(
previousDescriptor.options,
previousDescriptor.route.name
),
}
: null,
headerStyle,
headerShadowVisible === false
? { shadowOpacity: 0, borderBottomWidth: 0 }
: null,
]}
/>
: undefined,
options,
route,
navigation,
})
) : (
<Header
title={getHeaderTitle(options, route.name)}
headerTintColor={headerTintColor}
headerLeft={
typeof headerLeft === 'function'
? ({ tintColor }) => headerLeft({ tintColor })
: headerLeft === undefined && canGoBack
? ({ tintColor }) => (
<HeaderBackButton
tintColor={tintColor}
backImage={
headerBackImageSource !== undefined
? () => (
<Image
source={headerBackImageSource}
style={[
styles.backImage,
{ tintColor },
]}
/>
)
: undefined
}
onPress={navigation.goBack}
canGoBack={canGoBack}
/>
)
: headerLeft
}
headerRight={
typeof headerRight === 'function'
? ({ tintColor }) => headerRight({ tintColor })
: headerRight
}
headerTitle={
typeof headerTitle === 'function'
? ({ children, tintColor }) =>
headerTitle({ children, tintColor })
: headerTitle
}
headerTitleStyle={headerTitleStyle}
headerStyle={[
headerTranslucent
? {
position: 'absolute',
backgroundColor: 'transparent',
}
: null,
headerStyle,
headerShadowVisible === false
? { shadowOpacity: 0, borderBottomWidth: 0 }
: null,
]}
/>
)
}
style={[
StyleSheet.absoluteFill,
Expand Down

0 comments on commit 1a39632

Please sign in to comment.