mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-27 21:08:02 +08:00
refactor: move headerMode to options
BREAKING CHANGE: headerMode is now moved to options instead of props
This commit is contained in:
159
example/src/Screens/MixedHeaderMode.tsx
Normal file
159
example/src/Screens/MixedHeaderMode.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import * as React from 'react';
|
||||
import { View, Platform, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import type { ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackScreenProps,
|
||||
TransitionPresets,
|
||||
HeaderStyleInterpolators,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
import NewsFeed from '../Shared/NewsFeed';
|
||||
|
||||
export type SimpleStackParams = {
|
||||
Article: { author: string } | undefined;
|
||||
NewsFeed: { date: number };
|
||||
Albums: undefined;
|
||||
};
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('NewsFeed', { date: Date.now() })}
|
||||
style={styles.button}
|
||||
>
|
||||
Push feed
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.pop()}
|
||||
style={styles.button}
|
||||
>
|
||||
Pop screen
|
||||
</Button>
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: route.params?.author ?? 'Unknown' }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const NewsFeedScreen = ({
|
||||
route,
|
||||
navigation,
|
||||
}: StackScreenProps<SimpleStackParams, 'NewsFeed'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Albums')}
|
||||
style={styles.button}
|
||||
>
|
||||
Navigate to album
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.pop()}
|
||||
style={styles.button}
|
||||
>
|
||||
Pop screen
|
||||
</Button>
|
||||
</View>
|
||||
<NewsFeed scrollEnabled={scrollEnabled} date={route.params.date} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({
|
||||
navigation,
|
||||
}: StackScreenProps<SimpleStackParams, 'Albums'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
|
||||
style={styles.button}
|
||||
>
|
||||
Push article
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.pop()}
|
||||
style={styles.button}
|
||||
>
|
||||
Pop screen
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||
|
||||
export default function SimpleStackScreen({
|
||||
navigation,
|
||||
}: StackScreenProps<ParamListBase>) {
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<SimpleStack.Navigator
|
||||
screenOptions={{
|
||||
...TransitionPresets.SlideFromRightIOS,
|
||||
headerMode: 'float',
|
||||
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
|
||||
}}
|
||||
>
|
||||
<SimpleStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params?.author ?? 'Unknown'}`,
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="NewsFeed"
|
||||
component={NewsFeedScreen}
|
||||
options={{ title: 'Feed' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="Albums"
|
||||
component={AlbumsScreen}
|
||||
options={{
|
||||
...TransitionPresets.ModalSlideFromBottomIOS,
|
||||
headerMode: 'screen',
|
||||
title: 'Albums',
|
||||
}}
|
||||
/>
|
||||
</SimpleStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttons: {
|
||||
flexDirection: 'row',
|
||||
padding: 8,
|
||||
},
|
||||
button: {
|
||||
margin: 8,
|
||||
},
|
||||
});
|
||||
@@ -42,6 +42,7 @@ import LinkingPrefixes from './LinkingPrefixes';
|
||||
import SettingsItem from './Shared/SettingsItem';
|
||||
import SimpleStack from './Screens/SimpleStack';
|
||||
import ModalStack from './Screens/ModalStack';
|
||||
import MixedHeaderMode from './Screens/MixedHeaderMode';
|
||||
import StackTransparent from './Screens/StackTransparent';
|
||||
import StackHeaderCustomization from './Screens/StackHeaderCustomization';
|
||||
import BottomTabs from './Screens/BottomTabs';
|
||||
@@ -70,6 +71,10 @@ const SCREENS = {
|
||||
title: 'Modal Stack',
|
||||
component: ModalStack,
|
||||
},
|
||||
MixedHeaderMode: {
|
||||
title: 'Float + Screen Header Stack',
|
||||
component: MixedHeaderMode,
|
||||
},
|
||||
StackTransparent: {
|
||||
title: 'Transparent Stack',
|
||||
component: StackTransparent,
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
StackNavigationConfig,
|
||||
StackNavigationOptions,
|
||||
StackNavigationEventMap,
|
||||
StackHeaderMode,
|
||||
} from '../types';
|
||||
|
||||
type Props = DefaultNavigatorOptions<StackNavigationOptions> &
|
||||
@@ -31,13 +32,18 @@ function StackNavigator({
|
||||
...rest
|
||||
}: Props) {
|
||||
// @ts-expect-error: headerMode='none' is deprecated
|
||||
const isHeaderModeNone = rest.headerMode === 'none';
|
||||
const headerMode = rest.headerMode as StackHeaderMode | 'none';
|
||||
|
||||
warnOnce(
|
||||
isHeaderModeNone,
|
||||
headerMode === 'none',
|
||||
`Stack Navigator: 'headerMode="none"' is deprecated. Use 'headerShown: false' in 'screenOptions' instead.`
|
||||
);
|
||||
|
||||
warnOnce(
|
||||
headerMode !== 'none',
|
||||
`Stack Navigator: 'headerMode' is moved to 'options'. Moved it to 'screenOptions' to keep current behavior.`
|
||||
);
|
||||
|
||||
const { state, descriptors, navigation } = useNavigationBuilder<
|
||||
StackNavigationState<ParamListBase>,
|
||||
StackRouterOptions,
|
||||
@@ -49,7 +55,13 @@ function StackNavigator({
|
||||
children,
|
||||
screenOptions,
|
||||
defaultScreenOptions: {
|
||||
headerShown: !isHeaderModeNone,
|
||||
headerShown: headerMode !== 'none',
|
||||
headerMode:
|
||||
headerMode !== 'none'
|
||||
? headerMode
|
||||
: rest.mode !== 'modal' && Platform.OS === 'ios'
|
||||
? 'float'
|
||||
: 'screen',
|
||||
gestureEnabled: Platform.OS === 'ios',
|
||||
animationEnabled:
|
||||
Platform.OS !== 'web' &&
|
||||
|
||||
@@ -198,7 +198,12 @@ export type StackNavigationOptions = StackHeaderOptions &
|
||||
*/
|
||||
header?: (props: StackHeaderProps) => React.ReactNode;
|
||||
/**
|
||||
* Whether to show the header. The header is shown by default unless `headerMode` was set to `none`.
|
||||
* Whether the header floats above the screen or part of the screen.
|
||||
* Defaults to `float` on iOS for non-modals, and `screen` for the rest.
|
||||
*/
|
||||
headerMode?: StackHeaderMode;
|
||||
/**
|
||||
* Whether to show the header. The header is shown by default.
|
||||
* Setting this to `false` hides the header.
|
||||
*/
|
||||
headerShown?: boolean;
|
||||
@@ -273,7 +278,6 @@ export type StackNavigationOptions = StackHeaderOptions &
|
||||
|
||||
export type StackNavigationConfig = {
|
||||
mode?: StackCardMode;
|
||||
headerMode?: StackHeaderMode;
|
||||
/**
|
||||
* If `false`, the keyboard will NOT automatically dismiss when navigating to a new screen.
|
||||
* Defaults to `true`.
|
||||
|
||||
@@ -21,7 +21,6 @@ import type {
|
||||
StackHeaderStyleInterpolator,
|
||||
StackNavigationProp,
|
||||
StackHeaderProps,
|
||||
GestureDirection,
|
||||
} from '../../types';
|
||||
|
||||
export type Props = {
|
||||
@@ -35,7 +34,6 @@ export type Props = {
|
||||
height: number;
|
||||
}) => void;
|
||||
styleInterpolator: StackHeaderStyleInterpolator;
|
||||
gestureDirection: GestureDirection;
|
||||
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
};
|
||||
|
||||
@@ -46,7 +44,6 @@ export default function HeaderContainer({
|
||||
getPreviousScene,
|
||||
getFocusedRoute,
|
||||
onContentHeightChange,
|
||||
gestureDirection,
|
||||
styleInterpolator,
|
||||
style,
|
||||
}: Props) {
|
||||
@@ -60,10 +57,10 @@ export default function HeaderContainer({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { header, headerShown = true, headerTransparent } =
|
||||
const { header, headerMode, headerShown = true, headerTransparent } =
|
||||
scene.descriptor.options || {};
|
||||
|
||||
if (!headerShown) {
|
||||
if (headerMode !== mode || !headerShown) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -87,18 +84,24 @@ export default function HeaderContainer({
|
||||
const previousDescriptor = self[i - 1]?.descriptor;
|
||||
const nextDescriptor = self[i + 1]?.descriptor;
|
||||
|
||||
const { headerShown: previousHeaderShown = true } =
|
||||
previousDescriptor?.options || {};
|
||||
const {
|
||||
headerShown: previousHeaderShown = true,
|
||||
headerMode: previousHeaderMode,
|
||||
} = previousDescriptor?.options || {};
|
||||
|
||||
const { headerShown: nextHeaderShown = true } =
|
||||
nextDescriptor?.options || {};
|
||||
const {
|
||||
headerShown: nextHeaderShown = true,
|
||||
headerMode: nextHeaderMode,
|
||||
gestureDirection: nextGestureDirection,
|
||||
} = nextDescriptor?.options || {};
|
||||
|
||||
const isHeaderStatic =
|
||||
(previousHeaderShown === false &&
|
||||
((previousHeaderShown === false || previousHeaderMode === 'screen') &&
|
||||
// We still need to animate when coming back from next scene
|
||||
// A hacky way to check this is if the next scene exists
|
||||
!nextDescriptor) ||
|
||||
nextHeaderShown === false;
|
||||
nextHeaderShown === false ||
|
||||
nextHeaderMode === 'screen';
|
||||
|
||||
const props: StackHeaderProps = {
|
||||
layout,
|
||||
@@ -111,10 +114,10 @@ export default function HeaderContainer({
|
||||
styleInterpolator:
|
||||
mode === 'float'
|
||||
? isHeaderStatic
|
||||
? gestureDirection === 'vertical' ||
|
||||
gestureDirection === 'vertical-inverted'
|
||||
? nextGestureDirection === 'vertical' ||
|
||||
nextGestureDirection === 'vertical-inverted'
|
||||
? forSlideUp
|
||||
: gestureDirection === 'horizontal-inverted'
|
||||
: nextGestureDirection === 'horizontal-inverted'
|
||||
? forSlideRight
|
||||
: forSlideLeft
|
||||
: styleInterpolator
|
||||
|
||||
@@ -64,7 +64,7 @@ type Props = TransitionPreset & {
|
||||
mode: StackCardMode;
|
||||
headerMode: StackHeaderMode;
|
||||
headerShown: boolean;
|
||||
hasAbsoluteHeader: boolean;
|
||||
hasAbsoluteFloatHeader: boolean;
|
||||
headerHeight: number;
|
||||
onHeaderHeightChange: (props: {
|
||||
route: Route<string>;
|
||||
@@ -96,7 +96,7 @@ function CardContainer({
|
||||
headerMode,
|
||||
headerShown,
|
||||
headerStyleInterpolator,
|
||||
hasAbsoluteHeader,
|
||||
hasAbsoluteFloatHeader,
|
||||
headerHeight,
|
||||
onHeaderHeightChange,
|
||||
isParentHeaderShown,
|
||||
@@ -251,7 +251,11 @@ function CardContainer({
|
||||
pointerEvents={active ? 'box-none' : pointerEvents}
|
||||
pageOverflowEnabled={headerMode !== 'float' && mode === 'card'}
|
||||
headerDarkContent={headerDarkContent}
|
||||
containerStyle={hasAbsoluteHeader ? { marginTop: headerHeight } : null}
|
||||
containerStyle={
|
||||
hasAbsoluteFloatHeader && headerMode !== 'screen'
|
||||
? { marginTop: headerHeight }
|
||||
: null
|
||||
}
|
||||
contentStyle={[{ backgroundColor: colors.background }, cardStyle]}
|
||||
style={[
|
||||
{
|
||||
@@ -284,7 +288,6 @@ function CardContainer({
|
||||
scenes: [previousScene, scene],
|
||||
getPreviousScene,
|
||||
getFocusedRoute,
|
||||
gestureDirection,
|
||||
styleInterpolator: headerStyleInterpolator,
|
||||
onContentHeightChange: onHeaderHeightChange,
|
||||
})}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import type { EdgeInsets } from 'react-native-safe-area-context';
|
||||
import Color from 'color';
|
||||
import type {
|
||||
ParamListBase,
|
||||
Route,
|
||||
@@ -27,19 +28,16 @@ import {
|
||||
DefaultTransition,
|
||||
ModalTransition,
|
||||
} from '../../TransitionConfigs/TransitionPresets';
|
||||
import { forNoAnimation as forNoAnimationHeader } from '../../TransitionConfigs/HeaderStyleInterpolators';
|
||||
import { forNoAnimation as forNoAnimationCard } from '../../TransitionConfigs/CardStyleInterpolators';
|
||||
import getDistanceForDirection from '../../utils/getDistanceForDirection';
|
||||
import type {
|
||||
Layout,
|
||||
StackHeaderMode,
|
||||
StackCardMode,
|
||||
StackDescriptorMap,
|
||||
StackNavigationOptions,
|
||||
StackDescriptor,
|
||||
Scene,
|
||||
} from '../../types';
|
||||
import Color from 'color';
|
||||
|
||||
type GestureValues = {
|
||||
[key: string]: Animated.Value;
|
||||
@@ -61,7 +59,6 @@ type Props = {
|
||||
getGesturesEnabled: (props: { route: Route<string> }) => boolean;
|
||||
renderHeader: (props: HeaderContainerProps) => React.ReactNode;
|
||||
renderScene: (props: { route: Route<string> }) => React.ReactNode;
|
||||
headerMode: StackHeaderMode;
|
||||
isParentHeaderShown: boolean;
|
||||
onTransitionStart: (
|
||||
props: { route: Route<string> },
|
||||
@@ -382,7 +379,6 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
getGesturesEnabled,
|
||||
renderHeader,
|
||||
renderScene,
|
||||
headerMode,
|
||||
isParentHeaderShown,
|
||||
onTransitionStart,
|
||||
onTransitionEnd,
|
||||
@@ -409,13 +405,6 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
let defaultTransitionPreset =
|
||||
mode === 'modal' ? ModalTransition : DefaultTransition;
|
||||
|
||||
if (headerMode !== 'float') {
|
||||
defaultTransitionPreset = {
|
||||
...defaultTransitionPreset,
|
||||
headerStyleInterpolator: forNoAnimationHeader,
|
||||
};
|
||||
}
|
||||
|
||||
let activeScreensLimit = 1;
|
||||
|
||||
for (let i = scenes.length - 1; i >= 0; i--) {
|
||||
@@ -433,50 +422,49 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const isFloatHeaderAbsolute =
|
||||
headerMode === 'float'
|
||||
? this.state.scenes.slice(-2).some((scene) => {
|
||||
const { descriptor } = scene;
|
||||
const options = descriptor ? descriptor.options : {};
|
||||
const { headerTransparent, headerShown = true } = options;
|
||||
const isFloatHeaderAbsolute = this.state.scenes.slice(-2).some((scene) => {
|
||||
const options = scene.descriptor.options ?? {};
|
||||
const {
|
||||
headerMode = 'screen',
|
||||
headerTransparent,
|
||||
headerShown = true,
|
||||
} = options;
|
||||
|
||||
if (headerTransparent || headerShown === false) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
headerTransparent ||
|
||||
headerShown === false ||
|
||||
headerMode === 'screen'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
: false;
|
||||
return false;
|
||||
});
|
||||
|
||||
const floatingHeader =
|
||||
headerMode === 'float' ? (
|
||||
<React.Fragment key="header">
|
||||
{renderHeader({
|
||||
mode: 'float',
|
||||
layout,
|
||||
scenes,
|
||||
getPreviousScene: this.getPreviousScene,
|
||||
getFocusedRoute: this.getFocusedRoute,
|
||||
onContentHeightChange: this.handleHeaderLayout,
|
||||
gestureDirection:
|
||||
focusedOptions.gestureDirection !== undefined
|
||||
? focusedOptions.gestureDirection
|
||||
: defaultTransitionPreset.gestureDirection,
|
||||
styleInterpolator:
|
||||
focusedOptions.headerStyleInterpolator !== undefined
|
||||
? focusedOptions.headerStyleInterpolator
|
||||
: defaultTransitionPreset.headerStyleInterpolator,
|
||||
style: [
|
||||
styles.floating,
|
||||
isFloatHeaderAbsolute && [
|
||||
// Without this, the header buttons won't be touchable on Android when headerTransparent: true
|
||||
{ height: focusedHeaderHeight },
|
||||
styles.absolute,
|
||||
],
|
||||
const floatingHeader = (
|
||||
<React.Fragment key="header">
|
||||
{renderHeader({
|
||||
mode: 'float',
|
||||
layout,
|
||||
scenes,
|
||||
getPreviousScene: this.getPreviousScene,
|
||||
getFocusedRoute: this.getFocusedRoute,
|
||||
onContentHeightChange: this.handleHeaderLayout,
|
||||
styleInterpolator:
|
||||
focusedOptions.headerStyleInterpolator !== undefined
|
||||
? focusedOptions.headerStyleInterpolator
|
||||
: defaultTransitionPreset.headerStyleInterpolator,
|
||||
style: [
|
||||
styles.floating,
|
||||
isFloatHeaderAbsolute && [
|
||||
// Without this, the header buttons won't be touchable on Android when headerTransparent: true
|
||||
{ height: focusedHeaderHeight },
|
||||
styles.absolute,
|
||||
],
|
||||
})}
|
||||
</React.Fragment>
|
||||
) : null;
|
||||
],
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -529,6 +517,7 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
|
||||
const {
|
||||
headerShown = true,
|
||||
headerMode = 'screen',
|
||||
headerTransparent,
|
||||
headerStyle,
|
||||
headerTintColor,
|
||||
@@ -648,7 +637,7 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
headerMode={headerMode}
|
||||
headerShown={headerShown}
|
||||
headerDarkContent={headerDarkContent}
|
||||
hasAbsoluteHeader={
|
||||
hasAbsoluteFloatHeader={
|
||||
isFloatHeaderAbsolute && !headerTransparent
|
||||
}
|
||||
renderHeader={renderHeader}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, Platform, StyleSheet } from 'react-native';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import {
|
||||
SafeAreaInsetsContext,
|
||||
EdgeInsets,
|
||||
@@ -439,9 +439,6 @@ export default class StackView extends React.Component<Props, State> {
|
||||
navigation,
|
||||
keyboardHandlingEnabled,
|
||||
mode = 'card',
|
||||
headerMode = mode === 'card' && Platform.OS === 'ios'
|
||||
? 'float'
|
||||
: 'screen',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
descriptors: _,
|
||||
...rest
|
||||
@@ -479,7 +476,6 @@ export default class StackView extends React.Component<Props, State> {
|
||||
onTransitionEnd={this.handleTransitionEnd}
|
||||
renderHeader={this.renderHeader}
|
||||
renderScene={this.renderScene}
|
||||
headerMode={headerMode}
|
||||
state={state}
|
||||
descriptors={descriptors}
|
||||
onGestureStart={this.handleGestureStart}
|
||||
|
||||
Reference in New Issue
Block a user