diff --git a/example/src/Screens/NativeStack.tsx b/example/src/Screens/NativeStack.tsx index 1c874d9e..0470550f 100644 --- a/example/src/Screens/NativeStack.tsx +++ b/example/src/Screens/NativeStack.tsx @@ -118,6 +118,7 @@ export default function NativeStackScreen({ React.useLayoutEffect(() => { navigation.setOptions({ headerShown: false, + gestureEnabled: false, }); }, [navigation]); diff --git a/example/src/Screens/NativeStackHeaderCustomization.tsx b/example/src/Screens/NativeStackHeaderCustomization.tsx new file mode 100644 index 00000000..ecffeeb7 --- /dev/null +++ b/example/src/Screens/NativeStackHeaderCustomization.tsx @@ -0,0 +1,185 @@ +import * as React from 'react'; +import { View, Platform, StyleSheet, ScrollView, Alert } from 'react-native'; +import { Appbar, Button } from 'react-native-paper'; +import type { ParamListBase } from '@react-navigation/native'; +import { + createNativeStackNavigator, + NativeStackScreenProps, +} from '@react-navigation/native-stack'; +import Article from '../Shared/Article'; +import Albums from '../Shared/Albums'; +import NewsFeed from '../Shared/NewsFeed'; + +export type NativeStackParams = { + Article: { author: string } | undefined; + NewsFeed: { date: number }; + Albums: undefined; +}; + +const scrollEnabled = Platform.select({ web: true, default: false }); + +const ArticleScreen = ({ + navigation, + route, +}: NativeStackScreenProps) => { + return ( + + + + + +
+ + ); +}; + +const NewsFeedScreen = ({ + route, + navigation, +}: NativeStackScreenProps) => { + return ( + + + + + + + + ); +}; + +const AlbumsScreen = ({ + navigation, +}: NativeStackScreenProps) => { + return ( + + + + + + + + ); +}; + +const NativeStack = createNativeStackNavigator(); + +export default function NativeStackScreen({ + navigation, +}: NativeStackScreenProps) { + React.useLayoutEffect(() => { + navigation.setOptions({ + headerShown: false, + gestureEnabled: false, + }); + }, [navigation]); + + const onPress = () => { + Alert.alert( + 'Never gonna give you up!', + 'Never gonna let you down! Never gonna run around and desert you!' + ); + }; + + return ( + + ({ + title: `Article by ${route.params?.author ?? 'Unknown'}`, + headerTitle: ({ tintColor }) => ( + + ), + headerRight: ({ tintColor }) => ( + + ), + })} + initialParams={{ author: 'Gandalf' }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); +} + +const styles = StyleSheet.create({ + buttons: { + flexDirection: 'row', + flexWrap: 'wrap', + padding: 8, + }, + button: { + margin: 8, + }, +}); diff --git a/example/src/index.tsx b/example/src/index.tsx index 526fbdd0..f2af4cd3 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -50,6 +50,7 @@ import MixedStack from './Screens/MixedStack'; import MixedHeaderMode from './Screens/MixedHeaderMode'; import StackTransparent from './Screens/StackTransparent'; import StackHeaderCustomization from './Screens/StackHeaderCustomization'; +import NativeStackHeaderCustomization from './Screens/NativeStackHeaderCustomization'; import BottomTabs from './Screens/BottomTabs'; import MaterialTopTabsScreen from './Screens/MaterialTopTabs'; import MaterialBottomTabs from './Screens/MaterialBottomTabs'; @@ -98,6 +99,10 @@ const SCREENS = { title: 'Header Customization in Stack', component: StackHeaderCustomization, }, + NativeStackHeaderCustomization: { + title: 'Header Customization in Native Stack', + component: NativeStackHeaderCustomization, + }, BottomTabs: { title: 'Bottom Tabs', component: BottomTabs }, MaterialTopTabs: { title: 'Material Top Tabs', diff --git a/packages/elements/src/types.tsx b/packages/elements/src/types.tsx index 59371760..8909f5db 100644 --- a/packages/elements/src/types.tsx +++ b/packages/elements/src/types.tsx @@ -12,6 +12,7 @@ export type HeaderOptions = { /** * String or a function that returns a React Element to be used by the header. * Defaults to scene `title`. + * * It receives `allowFontScaling`, `tintColor`, `style` and `children` in the options object as an argument. * The title string is passed in `children`. */ diff --git a/packages/native-stack/src/types.tsx b/packages/native-stack/src/types.tsx index a2e91658..12cfab73 100644 --- a/packages/native-stack/src/types.tsx +++ b/packages/native-stack/src/types.tsx @@ -62,6 +62,9 @@ export type NativeStackNavigationOptions = { title?: string; /** * 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. + * + * This will have no effect on the first screen in the stack. */ headerBackVisible?: boolean; /** @@ -149,20 +152,6 @@ export type NativeStackNavigationOptions = { fontWeight?: string; color?: string; }>; - /** - * Function which returns a React Element to display on the left side of the header. - * This is shown alongside the back button. See `headerBackVisible` to hide the back button. - */ - headerLeft?: (props: { tintColor?: string }) => React.ReactNode; - /** - * Function which returns a React Element to display on the right side of the header. - */ - headerRight?: (props: { tintColor?: string }) => React.ReactNode; - /** - * Function which returns a React Element to display in the center of the header. - * This replaces the header title on iOS. - */ - headerCenter?: (props: { tintColor?: string }) => React.ReactNode; /** * Whether to show the header. The header is shown by default. * Setting this to `false` hides the header. @@ -198,9 +187,33 @@ export type NativeStackNavigationOptions = { */ headerTintColor?: string; /** - * String to display in the header as title. Defaults to scene `title`. + * Function which returns a React Element to display on the left side of the header. + * This replaces the back button. See `headerBackVisible` to show the back button along side left element. */ - headerTitle?: string; + headerLeft?: (props: { tintColor?: string }) => React.ReactNode; + /** + * Function which returns a React Element to display on the right side of the header. + */ + headerRight?: (props: { tintColor?: string }) => React.ReactNode; + /** + * String or a function that returns a React Element to be used by the header. + * Defaults to scene `title`. + * + * It receives `tintColor` and`children` in the options object as an argument. + * The title string is passed in `children`. + */ + headerTitle?: + | string + | ((props: { + /** + * The title text of the header. + */ + children: string; + /** + * Tint color for the header. + */ + tintColor?: string; + }) => React.ReactNode); /** * Style object for header title. Supported properties: * - fontFamily diff --git a/packages/native-stack/src/views/HeaderConfig.tsx b/packages/native-stack/src/views/HeaderConfig.tsx index 6e938666..601e8a9c 100644 --- a/packages/native-stack/src/views/HeaderConfig.tsx +++ b/packages/native-stack/src/views/HeaderConfig.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { StyleSheet, I18nManager, Platform } from 'react-native'; +import { StyleSheet, I18nManager, Platform, View } from 'react-native'; import { ScreenStackHeaderBackButtonImage, ScreenStackHeaderCenterView, @@ -10,6 +10,7 @@ import { SearchBar, } from 'react-native-screens'; import { Route, useTheme } from '@react-navigation/native'; +import { HeaderTitle } from '@react-navigation/elements'; import type { NativeStackNavigationOptions } from '../types'; import { processFonts } from './FontProcessor'; @@ -22,7 +23,6 @@ export default function HeaderConfig({ headerBackTitle, headerBackTitleStyle, headerBackTitleVisible = true, - headerCenter, headerBackVisible, headerShadowVisible, headerLargeStyle, @@ -48,7 +48,12 @@ export default function HeaderConfig({ title, }: Props): JSX.Element { const { colors } = useTheme(); - const tintColor = headerTintColor ?? colors.primary; + const tintColor = + headerTintColor != null + ? headerTintColor + : Platform.OS === 'ios' + ? colors.primary + : colors.text; const headerBackTitleStyleFlattened = StyleSheet.flatten(headerBackTitleStyle) || {}; @@ -68,9 +73,14 @@ export default function HeaderConfig({ headerTitleStyleFlattened.fontFamily, ]); + const titleText = title !== undefined ? title : route.name; + const headerLeftElement = headerLeft?.({ tintColor }); const headerRightElement = headerRight?.({ tintColor }); - const headerCenterElement = headerCenter?.({ tintColor }); + const headerTitleElement = + typeof headerTitle === 'function' + ? headerTitle({ tintColor, children: titleText }) + : null; if (Platform.OS === 'ios' && headerSearchBar != null && SearchBar == null) { throw new Error( @@ -78,9 +88,18 @@ export default function HeaderConfig({ ); } + /** + * We need to set this in if: + * - Back button should stay visible when `headerLeft` is specified + * - If `headerTitle` for Android is specified, so we only need to remove the title and keep the back button + */ + const backButtonInCustomView = headerBackVisible + ? headerLeftElement != null + : Platform.OS === 'android' && headerTitleElement != null; + return ( - {headerRightElement != null ? ( - - {headerRightElement} - - ) : null} + {Platform.OS === 'ios' ? ( + <> + {headerLeftElement != null ? ( + + {headerLeftElement} + + ) : null} + {headerTitleElement != null ? ( + + {headerTitleElement} + + ) : null} + + ) : ( + <> + {headerLeftElement != null || typeof headerTitle === 'function' ? ( + + + {headerLeftElement} + {typeof headerTitle === 'function' ? ( + headerTitleElement + ) : ( + {titleText} + )} + + + ) : null} + + )} {headerBackImageSource !== undefined ? ( ) : null} - {headerLeftElement != null ? ( - - {headerLeftElement} - - ) : null} - {headerCenterElement != null ? ( - - {headerCenterElement} - + {headerRightElement != null ? ( + + {headerRightElement} + ) : null} {Platform.OS === 'ios' && headerSearchBar != null ? ( @@ -153,3 +185,10 @@ export default function HeaderConfig({ ); } + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + alignItems: 'center', + }, +});