You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
595 lines
19 KiB
595 lines
19 KiB
import React from 'react';
|
|
import {
|
|
Platform,
|
|
StyleSheet,
|
|
Animated,
|
|
StyleProp,
|
|
TextStyle,
|
|
ViewStyle,
|
|
} from 'react-native';
|
|
import {
|
|
ScreenContext,
|
|
ScreenStack,
|
|
ScreenStackHeaderBackButtonImage,
|
|
ScreenStackHeaderCenterView,
|
|
ScreenStackHeaderConfig,
|
|
ScreenStackHeaderConfigProps,
|
|
ScreenStackHeaderLeftView,
|
|
ScreenStackHeaderRightView,
|
|
ScreenStackHeaderSearchBarView,
|
|
SearchBar,
|
|
StackPresentationTypes,
|
|
} from 'react-native-screens';
|
|
import {
|
|
createNavigator,
|
|
SceneView,
|
|
StackActions,
|
|
StackRouter,
|
|
NavigationRouteConfigMap,
|
|
CreateNavigatorConfig,
|
|
NavigationStackRouterConfig,
|
|
NavigationParams,
|
|
NavigationRoute,
|
|
NavigationDescriptor,
|
|
NavigationState,
|
|
NavigationNavigator,
|
|
NavigationAction,
|
|
NavigationProp,
|
|
NavigationScreenProp,
|
|
} from 'react-navigation';
|
|
import { NativeStackNavigationOptions as NativeStackNavigationOptionsV5 } from './native-stack/types';
|
|
import { HeaderBackButton } from 'react-navigation-stack';
|
|
import {
|
|
StackNavigationHelpers,
|
|
StackNavigationProp,
|
|
Layout,
|
|
} from 'react-navigation-stack/src/vendor/types';
|
|
|
|
const REMOVE_ACTION = 'NativeStackNavigator/REMOVE';
|
|
|
|
const isAndroid = Platform.OS === 'android';
|
|
|
|
let didWarn = isAndroid;
|
|
|
|
function renderComponentOrThunk(componentOrThunk: unknown, props: unknown) {
|
|
if (typeof componentOrThunk === 'function') {
|
|
return componentOrThunk(props);
|
|
}
|
|
return componentOrThunk;
|
|
}
|
|
|
|
type NativeStackRemoveNavigationAction = {
|
|
type: typeof REMOVE_ACTION;
|
|
immediate: boolean;
|
|
dismissCount: number;
|
|
key?: string;
|
|
};
|
|
|
|
export type NativeStackNavigationProp = StackNavigationProp;
|
|
|
|
export type NativeStackNavigationOptions = StackNavigatorOptions &
|
|
NativeStackNavigationOptionsV5 &
|
|
BackButtonProps & {
|
|
onWillAppear?: () => void;
|
|
onAppear?: () => void;
|
|
onWillDisappear?: () => void;
|
|
onDisappear?: () => void;
|
|
// these props differ from the ones used in v5 `native-stack`, and we would like to keep the API consistent between versions
|
|
/** Use `headerHideShadow` to be consistent with v5 `native-stack` */
|
|
hideShadow?: boolean;
|
|
/** Use `headerLargeTitle` to be consistent with v5 `native-stack` */
|
|
largeTitle?: boolean;
|
|
/** Use `headerLargeTitleHideShadow` to be consistent with v5 `native-stack` */
|
|
largeTitleHideShadow?: boolean;
|
|
/** Use `headerTranslucent` to be consistent with v5 `native-stack` */
|
|
translucent?: boolean;
|
|
};
|
|
|
|
// these are adopted from `stack` navigator
|
|
type StackNavigatorOptions = {
|
|
/** This is an option from `stackNavigator` and it hides the header when set to `null`. Use `headerShown` instead to be consistent with v5 `native-stack`. */
|
|
header?: React.ComponentType<Record<string, unknown>> | null;
|
|
/** This is an option from `stackNavigator` and it controls the stack presentation along with `mode` prop. Use `stackPresentation` instead to be consistent with v5 `native-stack` */
|
|
cardTransparent?: boolean;
|
|
/** This is an option from `stackNavigator` and it sets stack animation to none when `false` passed. Use `stackAnimation: 'none'` instead to be consistent with v5 `native-stack` */
|
|
animationEnabled?: boolean;
|
|
cardStyle?: StyleProp<ViewStyle>;
|
|
};
|
|
|
|
// these are the props used for rendering back button taken from `react-navigation-stack`
|
|
type BackButtonProps = {
|
|
headerBackImage?: (props: { tintColor: string }) => React.ReactNode;
|
|
headerPressColorAndroid?: string;
|
|
headerTintColor?: string;
|
|
backButtonTitle?: string;
|
|
truncatedBackButtonTitle?: string;
|
|
backTitleVisible?: boolean;
|
|
headerBackTitleStyle?: Animated.WithAnimatedValue<StyleProp<TextStyle>>;
|
|
layoutPreset?: Layout;
|
|
};
|
|
|
|
type NativeStackDescriptor = NavigationDescriptor<
|
|
NavigationParams,
|
|
NativeStackNavigationOptions
|
|
>;
|
|
|
|
type NativeStackDescriptorMap = {
|
|
[key: string]: NativeStackDescriptor;
|
|
};
|
|
|
|
// these are the props used for rendering back button taken from `react-navigation-stack`
|
|
type NativeStackNavigationConfig = {
|
|
/** This is an option from `stackNavigator` and controls the stack presentation along with `cardTransparent` prop. Use `stackPresentation` instead to be consistent with v5 `native-stack` */
|
|
mode?: 'modal' | 'containedModal';
|
|
/** This is an option from `stackNavigator` and makes the header hide when set to `none`. Use `headerShown` instead to be consistent with v5 `native-stack` */
|
|
headerMode?: 'none';
|
|
/** This is an option from `stackNavigator` and controls the stack presentation along with `mode` prop. Use `stackPresentation` instead to be consistent with v5 `native-stack` */
|
|
transparentCard?: boolean;
|
|
};
|
|
|
|
function removeScene(
|
|
route: NavigationRoute<NavigationParams>,
|
|
dismissCount: number,
|
|
navigation: StackNavigationHelpers
|
|
) {
|
|
navigation.dispatch({
|
|
// @ts-ignore special navigation action for native stack
|
|
type: REMOVE_ACTION,
|
|
immediate: true,
|
|
key: route.key,
|
|
dismissCount,
|
|
});
|
|
}
|
|
|
|
function onAppear(
|
|
route: NavigationRoute<NavigationParams>,
|
|
descriptor: NativeStackDescriptor,
|
|
navigation: StackNavigationHelpers
|
|
) {
|
|
descriptor.options?.onAppear?.();
|
|
navigation.dispatch(
|
|
StackActions.completeTransition({
|
|
toChildKey: route.key,
|
|
key: navigation.state.key,
|
|
})
|
|
);
|
|
}
|
|
|
|
function onFinishTransitioning(navigation: StackNavigationHelpers) {
|
|
const { routes } = navigation.state;
|
|
const lastRoute = routes?.length && routes[routes.length - 1];
|
|
|
|
if (lastRoute) {
|
|
navigation.dispatch(
|
|
StackActions.completeTransition({
|
|
toChildKey: lastRoute.key,
|
|
key: navigation.state.key,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
function renderHeaderConfig(
|
|
index: number,
|
|
route: NavigationRoute<NavigationParams>,
|
|
descriptor: NativeStackDescriptor,
|
|
navigationConfig: NativeStackNavigationConfig
|
|
) {
|
|
const { options } = descriptor;
|
|
const { headerMode } = navigationConfig;
|
|
|
|
const {
|
|
backButtonInCustomView,
|
|
direction,
|
|
disableBackButtonMenu,
|
|
headerBackTitle,
|
|
headerBackTitleStyle,
|
|
headerBackTitleVisible,
|
|
headerHideBackButton,
|
|
headerHideShadow,
|
|
headerLargeStyle,
|
|
headerLargeTitle,
|
|
headerLargeTitleHideShadow,
|
|
headerLargeTitleStyle,
|
|
headerShown,
|
|
headerStyle,
|
|
headerTintColor,
|
|
headerTitleStyle,
|
|
headerTopInsetEnabled = true,
|
|
headerTranslucent,
|
|
hideShadow,
|
|
largeTitle,
|
|
largeTitleHideShadow,
|
|
title,
|
|
translucent,
|
|
} = options;
|
|
|
|
const scene = {
|
|
index,
|
|
key: route.key,
|
|
route,
|
|
descriptor,
|
|
};
|
|
|
|
const headerOptions: ScreenStackHeaderConfigProps = {
|
|
backButtonInCustomView,
|
|
backTitle: headerBackTitleVisible === false ? '' : headerBackTitle,
|
|
backTitleFontFamily: headerBackTitleStyle?.fontFamily,
|
|
backTitleFontSize: headerBackTitleStyle?.fontSize,
|
|
color: headerTintColor,
|
|
direction,
|
|
disableBackButtonMenu,
|
|
topInsetEnabled: headerTopInsetEnabled,
|
|
hideBackButton: headerHideBackButton,
|
|
hideShadow: headerHideShadow || hideShadow,
|
|
largeTitle: headerLargeTitle || largeTitle,
|
|
largeTitleBackgroundColor:
|
|
headerLargeStyle?.backgroundColor ||
|
|
// @ts-ignore old implementation, will not be present in TS API, but can be used here
|
|
headerLargeTitleStyle?.backgroundColor,
|
|
largeTitleColor: headerLargeTitleStyle?.color,
|
|
largeTitleFontFamily: headerLargeTitleStyle?.fontFamily,
|
|
largeTitleFontSize: headerLargeTitleStyle?.fontSize,
|
|
largeTitleFontWeight: headerLargeTitleStyle?.fontWeight,
|
|
largeTitleHideShadow: largeTitleHideShadow || headerLargeTitleHideShadow,
|
|
title,
|
|
titleColor: headerTitleStyle?.color || headerTintColor,
|
|
titleFontFamily: headerTitleStyle?.fontFamily,
|
|
titleFontSize: headerTitleStyle?.fontSize,
|
|
titleFontWeight: headerTitleStyle?.fontWeight,
|
|
translucent: headerTranslucent || translucent || false,
|
|
};
|
|
|
|
const hasHeader =
|
|
headerShown !== false && headerMode !== 'none' && options.header !== null;
|
|
if (!hasHeader) {
|
|
return <ScreenStackHeaderConfig {...headerOptions} hidden />;
|
|
}
|
|
|
|
if (headerStyle !== undefined) {
|
|
headerOptions.backgroundColor = headerStyle.backgroundColor;
|
|
headerOptions.blurEffect = headerStyle.blurEffect;
|
|
}
|
|
|
|
const children = [];
|
|
|
|
if (options.backButtonImage) {
|
|
children.push(
|
|
<ScreenStackHeaderBackButtonImage
|
|
key="backImage"
|
|
source={options.backButtonImage}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (Platform.OS === 'ios' && options.searchBar) {
|
|
children.push(
|
|
<ScreenStackHeaderSearchBarView>
|
|
<SearchBar {...options.searchBar} />
|
|
</ScreenStackHeaderSearchBarView>
|
|
);
|
|
}
|
|
|
|
if (options.headerLeft !== undefined) {
|
|
children.push(
|
|
<ScreenStackHeaderLeftView key="left">
|
|
{renderComponentOrThunk(options.headerLeft, { scene })}
|
|
</ScreenStackHeaderLeftView>
|
|
);
|
|
} else if (options.headerBackImage !== undefined) {
|
|
const goBack = () => {
|
|
// Go back on next tick because button ripple effect needs to happen on Android
|
|
requestAnimationFrame(() => {
|
|
descriptor.navigation.goBack(descriptor.key);
|
|
});
|
|
};
|
|
|
|
children.push(
|
|
<ScreenStackHeaderLeftView key="left">
|
|
<HeaderBackButton
|
|
onPress={goBack}
|
|
pressColorAndroid={options.headerPressColorAndroid}
|
|
tintColor={options.headerTintColor}
|
|
backImage={options.headerBackImage}
|
|
label={options.backButtonTitle}
|
|
truncatedLabel={options.truncatedBackButtonTitle}
|
|
labelVisible={options.backTitleVisible}
|
|
labelStyle={options.headerBackTitleStyle}
|
|
titleLayout={options.layoutPreset}
|
|
// @ts-ignore old props kept for very old version of `react-navigation-stack`
|
|
title={options.backButtonTitle}
|
|
truncatedTitle={options.truncatedBackButtonTitle}
|
|
backTitleVisible={options.backTitleVisible}
|
|
titleStyle={options.headerBackTitleStyle}
|
|
layoutPreset={options.layoutPreset}
|
|
scene={scene}
|
|
/>
|
|
</ScreenStackHeaderLeftView>
|
|
);
|
|
}
|
|
|
|
if (options.headerTitle) {
|
|
if (title === undefined && typeof options.headerTitle === 'string') {
|
|
headerOptions.title = options.headerTitle;
|
|
} else {
|
|
children.push(
|
|
<ScreenStackHeaderCenterView key="center">
|
|
{renderComponentOrThunk(options.headerTitle, { scene })}
|
|
</ScreenStackHeaderCenterView>
|
|
);
|
|
}
|
|
}
|
|
|
|
if (options.headerRight) {
|
|
children.push(
|
|
<ScreenStackHeaderRightView key="right">
|
|
{renderComponentOrThunk(options.headerRight, { scene })}
|
|
</ScreenStackHeaderRightView>
|
|
);
|
|
}
|
|
|
|
if (children.length > 0) {
|
|
headerOptions.children = children;
|
|
}
|
|
|
|
return <ScreenStackHeaderConfig {...headerOptions} />;
|
|
}
|
|
|
|
const MaybeNestedStack = ({
|
|
isHeaderInModal,
|
|
screenProps,
|
|
route,
|
|
navigation,
|
|
SceneComponent,
|
|
index,
|
|
descriptor,
|
|
navigationConfig,
|
|
}: {
|
|
isHeaderInModal: boolean;
|
|
screenProps: unknown;
|
|
route: NavigationRoute<NavigationParams>;
|
|
navigation: NavigationScreenProp<
|
|
NavigationRoute<NavigationParams>,
|
|
NavigationParams
|
|
>;
|
|
SceneComponent: React.ComponentType<Record<string, unknown>>;
|
|
index: number;
|
|
descriptor: NativeStackDescriptor;
|
|
navigationConfig: NativeStackNavigationConfig;
|
|
}) => {
|
|
const Screen = React.useContext(ScreenContext);
|
|
|
|
if (isHeaderInModal) {
|
|
return (
|
|
<ScreenStack style={styles.scenes}>
|
|
<Screen style={StyleSheet.absoluteFill} enabled isNativeStack>
|
|
{renderHeaderConfig(index, route, descriptor, navigationConfig)}
|
|
<SceneView
|
|
screenProps={screenProps}
|
|
navigation={navigation}
|
|
component={SceneComponent}
|
|
/>
|
|
</Screen>
|
|
</ScreenStack>
|
|
);
|
|
}
|
|
return (
|
|
<SceneView
|
|
screenProps={screenProps}
|
|
navigation={navigation}
|
|
component={SceneComponent}
|
|
/>
|
|
);
|
|
};
|
|
|
|
type StackViewProps = {
|
|
navigation: StackNavigationHelpers;
|
|
descriptors: NativeStackDescriptorMap;
|
|
navigationConfig: NativeStackNavigationConfig;
|
|
screenProps: unknown;
|
|
};
|
|
|
|
function StackView({
|
|
navigation,
|
|
descriptors,
|
|
navigationConfig,
|
|
screenProps,
|
|
}: StackViewProps) {
|
|
const { routes } = navigation.state;
|
|
const Screen = React.useContext(ScreenContext);
|
|
return (
|
|
<ScreenStack
|
|
style={styles.scenes}
|
|
onFinishTransitioning={() => onFinishTransitioning(navigation)}
|
|
>
|
|
{routes.map((route, index) => {
|
|
const descriptor = descriptors[route.key];
|
|
const { getComponent, options } = descriptor;
|
|
const routeNavigationProp = descriptor.navigation;
|
|
const { mode, transparentCard } = navigationConfig;
|
|
const SceneComponent = getComponent();
|
|
|
|
let stackPresentation: StackPresentationTypes = 'push';
|
|
|
|
if (options.stackPresentation) {
|
|
stackPresentation = options.stackPresentation;
|
|
} else {
|
|
// this shouldn't be used because we have a prop for that
|
|
if (mode === 'modal' || mode === 'containedModal') {
|
|
stackPresentation = mode;
|
|
if (transparentCard || options.cardTransparent) {
|
|
stackPresentation =
|
|
mode === 'containedModal'
|
|
? 'containedTransparentModal'
|
|
: 'transparentModal';
|
|
}
|
|
}
|
|
}
|
|
let stackAnimation = options.stackAnimation;
|
|
if (options.animationEnabled === false) {
|
|
stackAnimation = 'none';
|
|
}
|
|
|
|
const hasHeader =
|
|
options.headerShown !== false &&
|
|
navigationConfig?.headerMode !== 'none' &&
|
|
options.header !== null;
|
|
|
|
if (
|
|
!didWarn &&
|
|
stackPresentation !== 'push' &&
|
|
options.headerShown !== undefined
|
|
) {
|
|
didWarn = true;
|
|
console.warn(
|
|
'Be aware that changing the visibility of header in modal on iOS will result in resetting the state of the screen.'
|
|
);
|
|
}
|
|
|
|
const isHeaderInModal = isAndroid
|
|
? false
|
|
: stackPresentation !== 'push' &&
|
|
hasHeader &&
|
|
options.headerShown === true;
|
|
const isHeaderInPush = isAndroid
|
|
? hasHeader
|
|
: stackPresentation === 'push' && hasHeader;
|
|
|
|
return (
|
|
<Screen
|
|
key={`screen_${route.key}`}
|
|
enabled
|
|
isNativeStack
|
|
style={[StyleSheet.absoluteFill, options.cardStyle]}
|
|
stackAnimation={stackAnimation}
|
|
customAnimationOnSwipe={options.customAnimationOnSwipe}
|
|
stackPresentation={stackPresentation}
|
|
replaceAnimation={
|
|
options.replaceAnimation === undefined
|
|
? 'pop'
|
|
: options.replaceAnimation
|
|
}
|
|
pointerEvents={
|
|
index === navigation.state.routes.length - 1 ? 'auto' : 'none'
|
|
}
|
|
gestureEnabled={
|
|
Platform.OS === 'android'
|
|
? false
|
|
: options.gestureEnabled === undefined
|
|
? true
|
|
: options.gestureEnabled
|
|
}
|
|
nativeBackButtonDismissalEnabled={
|
|
options.nativeBackButtonDismissalEnabled
|
|
}
|
|
fullScreenSwipeEnabled={options.fullScreenSwipeEnabled}
|
|
screenOrientation={options.screenOrientation}
|
|
statusBarAnimation={options.statusBarAnimation}
|
|
statusBarColor={options.statusBarColor}
|
|
statusBarHidden={options.statusBarHidden}
|
|
statusBarStyle={options.statusBarStyle}
|
|
statusBarTranslucent={options.statusBarTranslucent}
|
|
onAppear={() => onAppear(route, descriptor, routeNavigationProp)}
|
|
onWillAppear={() => options?.onWillAppear?.()}
|
|
onWillDisappear={() => options?.onWillDisappear?.()}
|
|
onDisappear={() => options?.onDisappear?.()}
|
|
onHeaderBackButtonClicked={() =>
|
|
removeScene(route, 1, routeNavigationProp)
|
|
}
|
|
onDismissed={(e) =>
|
|
removeScene(
|
|
route,
|
|
e.nativeEvent.dismissCount,
|
|
routeNavigationProp
|
|
)
|
|
}
|
|
>
|
|
{isHeaderInPush &&
|
|
renderHeaderConfig(index, route, descriptor, navigationConfig)}
|
|
<MaybeNestedStack
|
|
isHeaderInModal={isHeaderInModal}
|
|
screenProps={screenProps}
|
|
route={route}
|
|
navigation={routeNavigationProp}
|
|
SceneComponent={SceneComponent}
|
|
index={index}
|
|
descriptor={descriptor}
|
|
navigationConfig={navigationConfig}
|
|
/>
|
|
</Screen>
|
|
);
|
|
})}
|
|
</ScreenStack>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
scenes: { flex: 1 },
|
|
});
|
|
|
|
function createStackNavigator(
|
|
routeConfigMap: NavigationRouteConfigMap<
|
|
NativeStackNavigationOptions,
|
|
StackNavigationProp
|
|
>,
|
|
stackConfig: CreateNavigatorConfig<
|
|
NativeStackNavigationConfig,
|
|
NavigationStackRouterConfig,
|
|
NativeStackNavigationOptions,
|
|
StackNavigationProp
|
|
> = {}
|
|
): NavigationNavigator<
|
|
Record<string, unknown>,
|
|
NavigationProp<NavigationState>
|
|
> {
|
|
const router = StackRouter(routeConfigMap, stackConfig);
|
|
|
|
// below we override getStateForAction method in order to add handling for
|
|
// a custom native stack navigation action. The action REMOVE that we want to
|
|
// add works in a similar way to POP, but it does not remove all the routes
|
|
// that sit on top of the removed route. For example if we have three routes
|
|
// [a,b,c] and call POP on b, then both b and c will go away. In case we
|
|
// call REMOVE on b, only b will be removed from the stack and the resulting
|
|
// state will be [a, c]
|
|
const superGetStateForAction = router.getStateForAction;
|
|
router.getStateForAction = (
|
|
action: NavigationAction | NativeStackRemoveNavigationAction,
|
|
state
|
|
) => {
|
|
if (action.type === REMOVE_ACTION) {
|
|
const { key, immediate, dismissCount } = action;
|
|
let backRouteIndex = state.index;
|
|
if (key) {
|
|
const backRoute = state.routes.find(
|
|
(route: NavigationRoute<NavigationParams>) => route.key === key
|
|
);
|
|
backRouteIndex = state.routes.indexOf(backRoute);
|
|
}
|
|
|
|
if (backRouteIndex > 0) {
|
|
const newRoutes = [...state.routes];
|
|
if (dismissCount > 1) {
|
|
// when dismissing with iOS 14 native header back button, we can pop more than 1 screen at a time
|
|
// and the `backRouteIndex` is the index of the previous screen. Since we are starting already
|
|
// on the previous screen, we add 1 to start.
|
|
newRoutes.splice(backRouteIndex - dismissCount + 1, dismissCount);
|
|
} else {
|
|
newRoutes.splice(backRouteIndex, 1);
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
routes: newRoutes,
|
|
index: newRoutes.length - 1,
|
|
isTransitioning: immediate !== true,
|
|
};
|
|
}
|
|
}
|
|
return superGetStateForAction(action as NavigationAction, state);
|
|
};
|
|
// Create a navigator with StackView as the view
|
|
return createNavigator(StackView, router, stackConfig);
|
|
}
|
|
|
|
export default createStackNavigator;
|