diff --git a/packages/native-stack/src/types.tsx b/packages/native-stack/src/types.tsx index 7ca194fc..a7d6f90f 100644 --- a/packages/native-stack/src/types.tsx +++ b/packages/native-stack/src/types.tsx @@ -4,6 +4,7 @@ import type { NavigationHelpers, NavigationProp, ParamListBase, + Route, RouteProp, StackActionHelpers, StackNavigationState, @@ -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; + /** + * Navigation prop for the header. + */ + navigation: NativeStackNavigationProp; +}; + 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. diff --git a/packages/native-stack/src/views/NativeStackView.native.tsx b/packages/native-stack/src/views/NativeStackView.native.tsx index b70e9738..bbf19d24 100644 --- a/packages/native-stack/src/views/NativeStackView.native.tsx +++ b/packages/native-stack/src/views/NativeStackView.native.tsx @@ -1,5 +1,6 @@ import { getDefaultHeaderHeight, + getHeaderTitle, HeaderHeightContext, HeaderShownContext, SafeAreaProviderCompat, @@ -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); @@ -120,6 +121,7 @@ const MaybeNestedStack = ({ type SceneViewProps = { index: number; descriptor: NativeStackDescriptor; + previousDescriptor?: NativeStackDescriptor; onWillDisappear: () => void; onAppear: () => void; onDisappear: () => void; @@ -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, @@ -199,11 +203,28 @@ const SceneView = ({ isHeaderInPush !== false ? headerHeight : parentHeaderHeight ?? 0 } > - + {header !== undefined && headerShown !== false ? ( + // TODO: expose custom header height + header({ + back: previousDescriptor + ? { + title: getHeaderTitle( + previousDescriptor.options, + previousDescriptor.route.name + ), + } + : undefined, + options, + route, + navigation, + }) + ) : ( + + )} - {state.routes.map((route, index) => ( - { - 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 ( + { + 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); + }} + /> + ); + })} ); } diff --git a/packages/native-stack/src/views/NativeStackView.tsx b/packages/native-stack/src/views/NativeStackView.tsx index a74b3cfd..10be0236 100644 --- a/packages/native-stack/src/views/NativeStackView.tsx +++ b/packages/native-stack/src/views/NativeStackView.tsx @@ -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, @@ -56,57 +61,76 @@ export default function NativeStackView({ state, descriptors }: Props) { navigation={navigation} headerShown={headerShown} header={ -
headerLeft({ tintColor }) - : headerLeft === undefined && canGoBack - ? ({ 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, + }) + ) : ( +
headerLeft({ tintColor }) + : headerLeft === undefined && canGoBack + ? ({ 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,