refactor: simplify props for stack and drawer headers

BREAKING CHANGE: Previously, the stack header accepted scene and previous scene which contained things such as descriptor, navigation prop, progress etc. The commit simplifies them to pass `route`, `navigation`, `options` and `progress` directly to the header. Similaryly, the `previous` argument now contains `options`, `route` and `progress`.
This commit is contained in:
Satyajit Sahoo
2020-11-12 02:47:51 +01:00
parent d5c091cbcf
commit 4cad132c2c
11 changed files with 129 additions and 101 deletions

View File

@@ -75,34 +75,32 @@ export type GestureDirection =
| 'vertical'
| 'vertical-inverted';
export type Scene<T> = {
export type Scene = {
/**
* Current route object,
*/
route: T;
/**
* Descriptor object for the route containing options and navigation object.
* Descriptor object for the screen.
*/
descriptor: StackDescriptor;
/**
* Animated nodes representing the progress of the animation.
*/
progress: {
/**
* Progress value of the current screen.
*/
current: Animated.AnimatedInterpolation;
/**
* Progress value for the screen after this one in the stack.
* This can be `undefined` in case the screen animating is the last one.
*/
next?: Animated.AnimatedInterpolation;
/**
* Progress value for the screen before this one in the stack.
* This can be `undefined` in case the screen animating is the first one.
*/
previous?: Animated.AnimatedInterpolation;
};
progress: SceneProgress;
};
export type SceneProgress = {
/**
* Progress value of the current screen.
*/
current: Animated.AnimatedInterpolation;
/**
* Progress value for the screen after this one in the stack.
* This can be `undefined` in case the screen animating is the last one.
*/
next?: Animated.AnimatedInterpolation;
/**
* Progress value for the screen before this one in the stack.
* This can be `undefined` in case the screen animating is the first one.
*/
previous?: Animated.AnimatedInterpolation;
};
export type StackHeaderMode = 'float' | 'screen';
@@ -221,11 +219,6 @@ export type StackHeaderOptions = {
};
export type StackHeaderProps = {
/**
* Mode of the header: `float` renders a single floating header across all screens,
* `screen` renders separate headers for each screen.
*/
mode: 'float' | 'screen';
/**
* Layout of the screen.
*/
@@ -234,14 +227,35 @@ export type StackHeaderProps = {
* Safe area insets to use in the header, e.g. to apply extra spacing for statusbar and notch.
*/
insets: EdgeInsets;
/**
* Object representing the current scene, such as the route object and animation progress.
*/
scene: Scene<Route<string>>;
/**
* Object representing the previous scene.
*/
previous?: Scene<Route<string>>;
previous?: {
/**
* Options for the previous screen.
*/
options: StackNavigationOptions;
/**
* Route object for the current screen.
*/
route: Route<string>;
/**
* Animated nodes representing the progress of the animation of the previous screen.
*/
progress: SceneProgress;
};
/**
* Animated nodes representing the progress of the animation.
*/
progress: SceneProgress;
/**
* Options for the current screen.
*/
options: StackNavigationOptions;
/**
* Route object for the current screen.
*/
route: Route<string>;
/**
* Navigation prop for the header.
*/

View File

@@ -1,9 +1,6 @@
import * as React from 'react';
import type { Route } from '@react-navigation/native';
import type { Scene } from '../types';
const PreviousSceneContext = React.createContext<
Scene<Route<string>> | undefined
>(undefined);
const PreviousSceneContext = React.createContext<Scene | undefined>(undefined);
export default PreviousSceneContext;

View File

@@ -9,21 +9,22 @@ import debounce from '../../utils/debounce';
import type { StackHeaderProps, StackHeaderTitleProps } from '../../types';
export default React.memo(function Header({
scene,
previous,
layout,
insets,
progress,
options,
route,
navigation,
styleInterpolator,
}: StackHeaderProps) {
const { options } = scene.descriptor;
const title =
typeof options.headerTitle !== 'function' &&
options.headerTitle !== undefined
? options.headerTitle
: options.title !== undefined
? options.title
: scene.route.name;
: route.name;
let leftLabel;
@@ -32,7 +33,7 @@ export default React.memo(function Header({
if (options.headerBackTitle !== undefined) {
leftLabel = options.headerBackTitle;
} else if (previous) {
const o = previous.descriptor.options;
const o = previous.options;
leftLabel =
typeof o.headerTitle !== 'function' && o.headerTitle !== undefined
@@ -48,17 +49,17 @@ export default React.memo(function Header({
if (navigation.isFocused() && navigation.canGoBack()) {
navigation.dispatch({
...StackActions.pop(),
source: scene.route.key,
source: route.key,
});
}
}, 50),
[navigation, scene.route.key]
[navigation, route.key]
);
const isModal = React.useContext(ModalPresentationContext);
const isParentHeaderShown = React.useContext(HeaderShownContext);
const isFirstRouteInParent = useNavigationState(
(state) => state.routes[0].key === scene.route.key
(state) => state.routes[0].key === route.key
);
const statusBarHeight =
@@ -67,9 +68,9 @@ export default React.memo(function Header({
return (
<HeaderSegment
{...options}
progress={progress}
insets={insets}
layout={layout}
scene={scene}
title={title}
leftLabel={leftLabel}
headerTitle={

View File

@@ -21,6 +21,7 @@ import type {
Scene,
StackHeaderStyleInterpolator,
StackNavigationProp,
StackHeaderProps,
GestureDirection,
} from '../../types';
@@ -28,10 +29,8 @@ export type Props = {
mode: 'float' | 'screen';
layout: Layout;
insets: EdgeInsets;
scenes: (Scene<Route<string>> | undefined)[];
getPreviousScene: (props: {
route: Route<string>;
}) => Scene<Route<string>> | undefined;
scenes: (Scene | undefined)[];
getPreviousScene: (props: { route: Route<string> }) => Scene | undefined;
getFocusedRoute: () => Route<string>;
onContentHeightChange?: (props: {
route: Route<string>;
@@ -71,34 +70,42 @@ export default function HeaderContainer({
return null;
}
const isFocused = focusedRoute.key === scene.route.key;
const previous =
getPreviousScene({ route: scene.route }) ?? parentPreviousScene;
const isFocused = focusedRoute.key === scene.descriptor.route.key;
const previousScene =
getPreviousScene({ route: scene.descriptor.route }) ??
parentPreviousScene;
// If the screen is next to a headerless screen, we need to make the header appear static
// This makes the header look like it's moving with the screen
const previousScene = self[i - 1];
const nextScene = self[i + 1];
const previousDescriptor = self[i - 1]?.descriptor;
const nextDescriptor = self[i + 1]?.descriptor;
const { headerShown: previousHeaderShown = true } =
previousScene?.descriptor.options || {};
previousDescriptor?.options || {};
const { headerShown: nextHeaderShown = true } =
nextScene?.descriptor.options || {};
nextDescriptor?.options || {};
const isHeaderStatic =
(previousHeaderShown === false &&
// We still need to animate when coming back from next scene
// A hacky way to check this is if the next scene exists
!nextScene) ||
!nextDescriptor) ||
nextHeaderShown === false;
const props = {
mode,
const props: StackHeaderProps = {
layout,
insets,
scene,
previous,
previous: previousScene
? {
progress: previousScene.progress,
options: previousScene.descriptor.options,
route: previousScene.descriptor.route,
}
: undefined,
progress: scene.progress,
options: scene.descriptor.options,
route: scene.descriptor.route,
navigation: scene.descriptor.navigation as StackNavigationProp<
ParamListBase
>,
@@ -117,10 +124,10 @@ export default function HeaderContainer({
return (
<NavigationContext.Provider
key={scene.route.key}
key={scene.descriptor.route.key}
value={scene.descriptor.navigation}
>
<NavigationRouteContext.Provider value={scene.route}>
<NavigationRouteContext.Provider value={scene.descriptor.route}>
<View
onLayout={
onContentHeightChange
@@ -128,7 +135,7 @@ export default function HeaderContainer({
const { height } = e.nativeEvent.layout;
onContentHeightChange({
route: scene.route,
route: scene.descriptor.route,
height,
});
}

View File

@@ -8,7 +8,6 @@ import {
ViewStyle,
} from 'react-native';
import type { EdgeInsets } from 'react-native-safe-area-context';
import type { Route } from '@react-navigation/native';
import HeaderBackButton from './HeaderBackButton';
import HeaderBackground from './HeaderBackground';
import memoize from '../../utils/memoize';
@@ -18,7 +17,7 @@ import type {
StackHeaderLeftButtonProps,
StackHeaderTitleProps,
StackHeaderOptions,
Scene,
SceneProgress,
} from '../../types';
type Props = Omit<
@@ -32,7 +31,7 @@ type Props = Omit<
onGoBack?: () => void;
title?: string;
leftLabel?: string;
scene: Scene<Route<string>>;
progress: SceneProgress;
styleInterpolator: StackHeaderStyleInterpolator;
};
@@ -140,7 +139,7 @@ export default function HeaderSegment(props: Props) {
);
const {
scene,
progress,
layout,
insets,
title: currentTitle,
@@ -281,8 +280,8 @@ export default function HeaderSegment(props: Props) {
} = getInterpolatedStyle(
styleInterpolator,
layout,
scene.progress.current,
scene.progress.next,
progress.current,
progress.next,
titleLayout,
previousTitle ? leftLabelLayout : undefined,
typeof height === 'number' ? height : defaultHeight

View File

@@ -9,11 +9,11 @@ import HeaderShownContext from '../../utils/HeaderShownContext';
import PreviousSceneContext from '../../utils/PreviousSceneContext';
import ModalPresentationContext from '../../utils/ModalPresentationContext';
import type {
Scene,
Layout,
StackHeaderMode,
StackCardMode,
TransitionPreset,
Scene,
} from '../../types';
type Props = TransitionPreset & {
@@ -23,7 +23,7 @@ type Props = TransitionPreset & {
closing: boolean;
layout: Layout;
gesture: Animated.Value;
scene: Scene<Route<string>>;
scene: Scene;
safeAreaInsetTop: number;
safeAreaInsetRight: number;
safeAreaInsetBottom: number;
@@ -34,9 +34,7 @@ type Props = TransitionPreset & {
cardOverlayEnabled: boolean;
cardShadowEnabled?: boolean;
cardStyle?: StyleProp<ViewStyle>;
getPreviousScene: (props: {
route: Route<string>;
}) => Scene<Route<string>> | undefined;
getPreviousScene: (props: { route: Route<string> }) => Scene | undefined;
getFocusedRoute: () => Route<string>;
renderHeader: (props: HeaderContainerProps) => React.ReactNode;
renderScene: (props: { route: Route<string> }) => React.ReactNode;
@@ -123,27 +121,27 @@ function CardContainer({
}, [active, onPageChangeConfirm]);
const handleOpen = () => {
onTransitionEnd?.({ route: scene.route }, false);
onOpenRoute({ route: scene.route });
onTransitionEnd?.({ route: scene.descriptor.route }, false);
onOpenRoute({ route: scene.descriptor.route });
};
const handleClose = () => {
onTransitionEnd?.({ route: scene.route }, true);
onCloseRoute({ route: scene.route });
onTransitionEnd?.({ route: scene.descriptor.route }, true);
onCloseRoute({ route: scene.descriptor.route });
};
const handleGestureBegin = () => {
onPageChangeStart?.();
onGestureStart?.({ route: scene.route });
onGestureStart?.({ route: scene.descriptor.route });
};
const handleGestureCanceled = () => {
onPageChangeCancel?.();
onGestureCancel?.({ route: scene.route });
onGestureCancel?.({ route: scene.descriptor.route });
};
const handleGestureEnd = () => {
onGestureEnd?.({ route: scene.route });
onGestureEnd?.({ route: scene.descriptor.route });
};
const handleTransitionStart = ({ closing }: { closing: boolean }) => {
@@ -153,7 +151,7 @@ function CardContainer({
onPageChangeCancel?.();
}
onTransitionStart?.({ route: scene.route }, closing);
onTransitionStart?.({ route: scene.descriptor.route }, closing);
};
const insets = {
@@ -185,7 +183,7 @@ function CardContainer({
};
}, [pointerEvents, scene.progress.next]);
const previousScene = getPreviousScene({ route: scene.route });
const previousScene = getPreviousScene({ route: scene.descriptor.route });
const isModalPresentation = cardStyleInterpolator === forModalPresentationIOS;
return (
@@ -234,7 +232,7 @@ function CardContainer({
value={isParentHeaderShown || headerShown !== false}
>
<HeaderHeightContext.Provider value={headerHeight}>
{renderScene({ route: scene.route })}
{renderScene({ route: scene.descriptor.route })}
</HeaderHeightContext.Provider>
</HeaderShownContext.Provider>
</PreviousSceneContext.Provider>

View File

@@ -32,10 +32,10 @@ import type {
Layout,
StackHeaderMode,
StackCardMode,
Scene,
StackDescriptorMap,
StackNavigationOptions,
StackDescriptor,
Scene,
} from '../../types';
type GestureValues = {
@@ -77,7 +77,7 @@ type Props = {
type State = {
routes: Route<string>[];
descriptors: StackDescriptorMap;
scenes: Scene<Route<string>>[];
scenes: Scene[];
gestures: GestureValues;
layout: Layout;
headerHeights: Record<string, number>;
@@ -248,7 +248,6 @@ export default class CardStack extends React.Component<Props, State> {
: undefined,
},
__memo: [
route,
state.layout,
descriptor,
nextDescriptor,
@@ -365,7 +364,7 @@ export default class CardStack extends React.Component<Props, State> {
if (previousRoute) {
const previousScene = scenes.find(
(scene) => scene.route.key === previousRoute.key
(scene) => scene.descriptor.route.key === previousRoute.key
);
return previousScene;