feat: automatically set headerMode if it's modal presentation style

This commit is contained in:
Satyajit Sahoo
2021-05-08 23:13:30 +02:00
parent 260da9b103
commit 4bb0b43f1a
7 changed files with 137 additions and 190 deletions

View File

@@ -1,5 +1,4 @@
import * as React from 'react';
import { Platform } from 'react-native';
import {
useNavigationBuilder,
createNavigatorFactory,
@@ -54,21 +53,9 @@ function StackNavigator({
initialRouteName,
children,
screenOptions,
defaultScreenOptions: ({ options }) => ({
defaultScreenOptions: () => ({
headerShown: headerMode ? headerMode !== 'none' : true,
headerMode:
headerMode && headerMode !== 'none'
? headerMode
: options.animationPresentation !== 'modal' &&
Platform.OS === 'ios' &&
options.header === undefined
? 'float'
: 'screen',
gestureEnabled: Platform.OS === 'ios',
animationEnabled:
Platform.OS !== 'web' &&
Platform.OS !== 'windows' &&
Platform.OS !== 'macos',
headerMode: headerMode && headerMode !== 'none' ? headerMode : undefined,
}),
});

View File

@@ -72,11 +72,21 @@ export type GestureDirection =
| 'vertical'
| 'vertical-inverted';
type SceneOptionsDefaults = TransitionPreset & {
animationEnabled: boolean;
gestureEnabled: boolean;
cardOverlayEnabled: boolean;
headerMode: StackHeaderMode;
};
export type Scene = {
/**
* Descriptor object for the screen.
*/
descriptor: StackDescriptor;
descriptor: Omit<StackDescriptor, 'options'> & {
options: Omit<StackDescriptor['options'], keyof SceneOptionsDefaults> &
SceneOptionsDefaults;
};
/**
* Animated nodes representing the progress of the animation.
*/

View File

@@ -18,13 +18,13 @@ import {
import type {
Layout,
Scene,
StackHeaderStyleInterpolator,
StackNavigationProp,
StackHeaderProps,
StackHeaderMode,
} from '../../types';
export type Props = {
mode: 'float' | 'screen';
mode: StackHeaderMode;
layout: Layout;
scenes: (Scene | undefined)[];
getPreviousScene: (props: { route: Route<string> }) => Scene | undefined;
@@ -33,7 +33,6 @@ export type Props = {
route: Route<string>;
height: number;
}) => void;
styleInterpolator: StackHeaderStyleInterpolator;
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
};
@@ -44,7 +43,6 @@ export default function HeaderContainer({
getPreviousScene,
getFocusedRoute,
onContentHeightChange,
styleInterpolator,
style,
}: Props) {
const focusedRoute = getFocusedRoute();
@@ -57,8 +55,13 @@ export default function HeaderContainer({
return null;
}
const { header, headerMode, headerShown = true, headerTransparent } =
scene.descriptor.options || {};
const {
header,
headerMode,
headerShown = true,
headerTransparent,
headerStyleInterpolator,
} = scene.descriptor.options;
if (headerMode !== mode || !headerShown) {
return null;
@@ -120,7 +123,7 @@ export default function HeaderContainer({
: nextGestureDirection === 'horizontal-inverted'
? forSlideRight
: forSlideLeft
: styleInterpolator
: headerStyleInterpolator
: forNoAnimation,
};

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { Animated, View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import { Animated, View, StyleSheet } from 'react-native';
import { Route, useTheme } from '@react-navigation/native';
import {
HeaderShownContext,
@@ -11,15 +11,9 @@ import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
import Card from './Card';
import { forModalPresentationIOS } from '../../TransitionConfigs/CardStyleInterpolators';
import ModalPresentationContext from '../../utils/ModalPresentationContext';
import type {
Layout,
StackHeaderMode,
StackPresentationMode,
TransitionPreset,
Scene,
} from '../../types';
import type { Layout, Scene } from '../../types';
type Props = TransitionPreset & {
type Props = {
interpolationIndex: number;
active: boolean;
focused: boolean;
@@ -32,12 +26,6 @@ type Props = TransitionPreset & {
safeAreaInsetRight: number;
safeAreaInsetBottom: number;
safeAreaInsetLeft: number;
cardOverlay?: (props: {
style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
}) => React.ReactNode;
cardOverlayEnabled: boolean;
cardShadowEnabled?: boolean;
cardStyle?: StyleProp<ViewStyle>;
getPreviousScene: (props: { route: Route<string> }) => Scene | undefined;
getFocusedRoute: () => Route<string>;
renderHeader: (props: HeaderContainerProps) => React.ReactNode;
@@ -55,12 +43,6 @@ type Props = TransitionPreset & {
onGestureStart?: (props: { route: Route<string> }) => void;
onGestureEnd?: (props: { route: Route<string> }) => void;
onGestureCancel?: (props: { route: Route<string> }) => void;
gestureEnabled?: boolean;
gestureResponseDistance?: number;
gestureVelocityImpact?: number;
animationPresentation?: StackPresentationMode;
headerMode: StackHeaderMode;
headerShown: boolean;
hasAbsoluteFloatHeader: boolean;
headerHeight: number;
onHeaderHeightChange: (props: {
@@ -74,25 +56,12 @@ const EPSILON = 0.1;
function CardContainer({
active,
cardOverlay,
cardOverlayEnabled,
cardShadowEnabled,
cardStyle,
cardStyleInterpolator,
closing,
gesture,
focused,
gestureDirection,
gestureEnabled,
gestureResponseDistance,
gestureVelocityImpact,
getPreviousScene,
getFocusedRoute,
animationPresentation,
headerDarkContent,
headerMode,
headerShown,
headerStyleInterpolator,
hasAbsoluteFloatHeader,
headerHeight,
onHeaderHeightChange,
@@ -116,7 +85,6 @@ function CardContainer({
safeAreaInsetRight,
safeAreaInsetTop,
scene,
transitionSpec,
}: Props) {
const parentHeaderHeight = React.useContext(HeaderHeightContext);
@@ -203,6 +171,22 @@ function CardContainer({
};
}, [pointerEvents, scene.progress.next]);
const {
animationPresentation,
cardOverlay,
cardOverlayEnabled,
cardShadowEnabled,
cardStyle,
cardStyleInterpolator,
gestureDirection,
gestureEnabled,
gestureResponseDistance,
gestureVelocityImpact,
headerMode,
headerShown,
transitionSpec,
} = scene.descriptor.options;
const isModalPresentation = cardStyleInterpolator === forModalPresentationIOS;
const previousScene = getPreviousScene({ route: scene.descriptor.route });
@@ -289,7 +273,6 @@ function CardContainer({
scenes: [previousScene, scene],
getPreviousScene,
getFocusedRoute,
styleInterpolator: headerStyleInterpolator,
onContentHeightChange: onHeaderHeightChange,
})}
</ModalPresentationContext.Provider>

View File

@@ -36,10 +36,11 @@ import {
import getDistanceForDirection from '../../utils/getDistanceForDirection';
import type {
Layout,
StackDescriptorMap,
StackNavigationOptions,
StackDescriptor,
Scene,
StackDescriptor,
StackDescriptorMap,
StackHeaderMode,
StackNavigationOptions,
} from '../../types';
type GestureValues = {
@@ -58,7 +59,6 @@ type Props = {
getPreviousRoute: (props: {
route: Route<string>;
}) => Route<string> | undefined;
getGesturesEnabled: (props: { route: Route<string> }) => boolean;
renderHeader: (props: HeaderContainerProps) => React.ReactNode;
renderScene: (props: { route: Route<string> }) => React.ReactNode;
isParentHeaderShown: boolean;
@@ -164,7 +164,10 @@ const getProgressFromGesture = (
};
export default class CardStack extends React.Component<Props, State> {
static getDerivedStateFromProps(props: Props, state: State) {
static getDerivedStateFromProps(
props: Props,
state: State
): Partial<State> | null {
if (
props.routes === state.routes &&
props.descriptors === state.descriptors
@@ -215,9 +218,89 @@ export default class CardStack extends React.Component<Props, State> {
props.descriptors[previousRoute?.key] ||
state.descriptors[previousRoute?.key];
const { options } = descriptor;
let defaultTransitionPreset =
options.animationPresentation === 'modal'
? ModalTransition
: DefaultTransition;
const {
animationEnabled = Platform.OS !== 'web' &&
Platform.OS !== 'windows' &&
Platform.OS !== 'macos',
gestureEnabled = Platform.OS === 'ios' &&
animationEnabled &&
index !== 0,
gestureDirection = defaultTransitionPreset.gestureDirection,
transitionSpec = defaultTransitionPreset.transitionSpec,
cardStyleInterpolator = animationEnabled === false
? forNoAnimationCard
: defaultTransitionPreset.cardStyleInterpolator,
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
cardOverlayEnabled = Platform.OS !== 'ios' ||
cardStyleInterpolator === forModalPresentationIOS,
} = options;
let transitionConfig = {
gestureDirection,
transitionSpec,
cardStyleInterpolator,
headerStyleInterpolator,
cardOverlayEnabled,
};
// When a screen is not the last, it should use next screen's transition config
// Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
// For example combining a slide and a modal transition would look wrong otherwise
// With this approach, combining different transition styles in the same navigator mostly looks right
// This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
// but majority of the transitions look alright
if (index !== self.length - 1) {
if (nextDescriptor) {
const {
animationEnabled,
gestureDirection = defaultTransitionPreset.gestureDirection,
transitionSpec = defaultTransitionPreset.transitionSpec,
cardStyleInterpolator = animationEnabled === false
? forNoAnimationCard
: defaultTransitionPreset.cardStyleInterpolator,
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
cardOverlayEnabled = descriptor.options.cardOverlayEnabled ??
cardStyleInterpolator === forModalPresentationIOS,
} = nextDescriptor.options;
transitionConfig = {
gestureDirection,
transitionSpec,
cardStyleInterpolator,
headerStyleInterpolator,
cardOverlayEnabled,
};
}
}
const headerMode: StackHeaderMode =
options.headerMode ??
(options.animationPresentation !== 'modal' &&
transitionConfig.cardStyleInterpolator !== forModalPresentationIOS &&
Platform.OS === 'ios' &&
options.header === undefined
? 'float'
: 'screen');
const scene = {
route,
descriptor,
descriptor: {
...descriptor,
options: {
...options,
...transitionConfig,
animationEnabled,
gestureEnabled,
headerMode,
},
},
progress: {
current: getProgressFromGesture(
currentGesture,
@@ -371,7 +454,6 @@ export default class CardStack extends React.Component<Props, State> {
closingRouteKeys,
onOpenRoute,
onCloseRoute,
getGesturesEnabled,
renderHeader,
renderScene,
isParentHeaderShown,
@@ -397,11 +479,7 @@ export default class CardStack extends React.Component<Props, State> {
const isFloatHeaderAbsolute = this.state.scenes.slice(-2).some((scene) => {
const options = scene.descriptor.options ?? {};
const {
headerMode = 'screen',
headerTransparent,
headerShown = true,
} = options;
const { headerMode, headerTransparent, headerShown = true } = options;
if (
headerTransparent ||
@@ -414,73 +492,6 @@ export default class CardStack extends React.Component<Props, State> {
return false;
});
const transitionConfigsList = routes.map((_, index, self) => {
const scene = scenes[index];
const options =
scene.descriptor?.options || ({} as StackNavigationOptions);
let defaultTransitionPreset =
options.animationPresentation === 'modal'
? ModalTransition
: DefaultTransition;
const {
animationEnabled,
gestureDirection = defaultTransitionPreset.gestureDirection,
transitionSpec = defaultTransitionPreset.transitionSpec,
cardStyleInterpolator = animationEnabled === false
? forNoAnimationCard
: defaultTransitionPreset.cardStyleInterpolator,
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
cardOverlayEnabled = Platform.OS !== 'ios' ||
cardStyleInterpolator === forModalPresentationIOS,
} = options;
let transitionConfig = {
gestureDirection,
transitionSpec,
cardStyleInterpolator,
headerStyleInterpolator,
cardOverlayEnabled,
};
// When a screen is not the last, it should use next screen's transition config
// Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
// For example combining a slide and a modal transition would look wrong otherwise
// With this approach, combining different transition styles in the same navigator mostly looks right
// This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
// but majority of the transitions look alright
if (index !== self.length - 1) {
const nextScene = scenes[index + 1];
if (nextScene) {
const {
animationEnabled,
gestureDirection = defaultTransitionPreset.gestureDirection,
transitionSpec = defaultTransitionPreset.transitionSpec,
cardStyleInterpolator = animationEnabled === false
? forNoAnimationCard
: defaultTransitionPreset.cardStyleInterpolator,
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
cardOverlayEnabled = scene.descriptor.options.cardOverlayEnabled ??
cardStyleInterpolator === forModalPresentationIOS,
} = nextScene.descriptor
? nextScene.descriptor.options
: ({} as StackNavigationOptions);
transitionConfig = {
gestureDirection,
transitionSpec,
cardStyleInterpolator,
headerStyleInterpolator,
cardOverlayEnabled,
};
}
}
return transitionConfig;
});
let activeScreensLimit = 1;
for (let i = scenes.length - 1; i >= 0; i--) {
@@ -488,8 +499,7 @@ export default class CardStack extends React.Component<Props, State> {
const {
// By default, we don't want to detach the previous screen of the active one for modals
detachPreviousScreen = options.animationPresentation === 'modal' ||
transitionConfigsList[i].cardStyleInterpolator ===
forModalPresentationIOS
options.cardStyleInterpolator === forModalPresentationIOS
? i !== scenes.length - 1
: true,
} = options;
@@ -501,7 +511,6 @@ export default class CardStack extends React.Component<Props, State> {
}
}
const focusedTransitionConfig = transitionConfigsList[state.index];
const floatingHeader = (
<React.Fragment key="header">
{renderHeader({
@@ -511,7 +520,6 @@ export default class CardStack extends React.Component<Props, State> {
getPreviousScene: this.getPreviousScene,
getFocusedRoute: this.getFocusedRoute,
onContentHeightChange: this.handleHeaderLayout,
styleInterpolator: focusedTransitionConfig.headerStyleInterpolator,
style: [
styles.floating,
isFloatHeaderAbsolute && [
@@ -573,23 +581,13 @@ export default class CardStack extends React.Component<Props, State> {
: 1;
}
const transitionConfig = transitionConfigsList[index];
const {
animationPresentation,
gestureResponseDistance,
gestureVelocityImpact,
cardStyleInterpolator,
headerShown = true,
headerMode = 'screen',
headerTransparent,
headerStyle,
headerTintColor,
cardShadowEnabled,
cardOverlay,
cardStyle,
} = scene.descriptor
? scene.descriptor.options
: ({} as StackNavigationOptions);
} = scene.descriptor.options;
const safeAreaInsetTop = insets.top;
const safeAreaInsetRight = insets.right;
@@ -617,12 +615,9 @@ export default class CardStack extends React.Component<Props, State> {
for (let i = index - 1; i >= 0; i--) {
const cardStyleInterpolatorCurrent =
transitionConfigsList[i].cardStyleInterpolator;
scenes[i]?.descriptor.options.cardStyleInterpolator;
if (
cardStyleInterpolatorCurrent !==
transitionConfig.cardStyleInterpolator
) {
if (cardStyleInterpolatorCurrent !== cardStyleInterpolator) {
break;
}
@@ -649,23 +644,17 @@ export default class CardStack extends React.Component<Props, State> {
safeAreaInsetRight={safeAreaInsetRight}
safeAreaInsetBottom={safeAreaInsetBottom}
safeAreaInsetLeft={safeAreaInsetLeft}
cardOverlay={cardOverlay}
cardShadowEnabled={cardShadowEnabled}
cardStyle={cardStyle}
onPageChangeStart={onPageChangeStart}
onPageChangeConfirm={onPageChangeConfirm}
onPageChangeCancel={onPageChangeCancel}
onGestureStart={onGestureStart}
onGestureCancel={onGestureCancel}
onGestureEnd={onGestureEnd}
gestureResponseDistance={gestureResponseDistance}
headerHeight={headerHeight}
isParentHeaderShown={isParentHeaderShown}
onHeaderHeightChange={this.handleHeaderLayout}
getPreviousScene={this.getPreviousScene}
getFocusedRoute={this.getFocusedRoute}
headerMode={headerMode}
headerShown={headerShown}
headerDarkContent={headerDarkContent}
hasAbsoluteFloatHeader={
isFloatHeaderAbsolute && !headerTransparent
@@ -676,10 +665,6 @@ export default class CardStack extends React.Component<Props, State> {
onCloseRoute={onCloseRoute}
onTransitionStart={onTransitionStart}
onTransitionEnd={onTransitionEnd}
gestureEnabled={index !== 0 && getGesturesEnabled({ route })}
gestureVelocityImpact={gestureVelocityImpact}
animationPresentation={animationPresentation}
{...transitionConfig}
/>
</MaybeScreen>
);

View File

@@ -288,24 +288,6 @@ export default class StackView extends React.Component<Props, State> {
descriptors: {},
};
private getGesturesEnabled = ({ route }: { route: Route<string> }) => {
const descriptor = this.state.descriptors[route.key];
if (descriptor) {
const { gestureEnabled, animationEnabled } = descriptor.options;
if (animationEnabled === false) {
// When animation is disabled, also disable gestures
// The gesture to dismiss a route will look weird when not animated
return false;
}
return gestureEnabled !== false;
}
return false;
};
private getPreviousRoute = ({ route }: { route: Route<string> }) => {
const { closingRouteKeys, replacingRouteKeys } = this.state;
const routes = this.state.routes.filter(
@@ -464,7 +446,6 @@ export default class StackView extends React.Component<Props, State> {
insets={insets as EdgeInsets}
isParentHeaderShown={isParentHeaderShown}
getPreviousRoute={this.getPreviousRoute}
getGesturesEnabled={this.getGesturesEnabled}
routes={routes}
openingRouteKeys={openingRouteKeys}
closingRouteKeys={closingRouteKeys}