react-navigation-stack
Version:
Stack navigator component for React Navigation
441 lines (397 loc) • 17.4 kB
JavaScript
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
import * as React from 'react';
import { Animated, View, StyleSheet, Dimensions, Platform } from 'react-native';
import { ScreenContainer, Screen, screensEnabled } from 'react-native-screens'; // Import with * as to prevent getters being called
import { getDefaultHeaderHeight } from '../Header/HeaderSegment';
import CardContainer from './CardContainer';
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';
const EPSILON = 0.01;
const MaybeScreenContainer = (_ref) => {
let {
enabled
} = _ref,
rest = _objectWithoutProperties(_ref, ["enabled"]);
if (enabled && screensEnabled()) {
return /*#__PURE__*/React.createElement(ScreenContainer, rest);
}
return /*#__PURE__*/React.createElement(View, rest);
};
const MaybeScreen = (_ref2) => {
let {
enabled,
active
} = _ref2,
rest = _objectWithoutProperties(_ref2, ["enabled", "active"]);
if (enabled && screensEnabled()) {
// @ts-ignore
return /*#__PURE__*/React.createElement(Screen, _extends({
active: active
}, rest));
}
return /*#__PURE__*/React.createElement(View, rest);
};
const FALLBACK_DESCRIPTOR = Object.freeze({
options: {}
});
const getHeaderHeights = (routes, insets, descriptors, layout, previous) => {
return routes.reduce((acc, curr) => {
const {
options = {}
} = descriptors[curr.key] || {};
const {
height = previous[curr.key]
} = StyleSheet.flatten(options.headerStyle || {});
const safeAreaInsets = _objectSpread({}, insets, {}, options.safeAreaInsets);
const {
headerStatusBarHeight = safeAreaInsets.top
} = options;
acc[curr.key] = typeof height === 'number' ? height : getDefaultHeaderHeight(layout, headerStatusBarHeight);
return acc;
}, {});
};
const getDistanceFromOptions = (mode, layout, descriptor) => {
const {
gestureDirection = mode === 'modal' ? ModalTransition.gestureDirection : DefaultTransition.gestureDirection
} = (descriptor === null || descriptor === void 0 ? void 0 : descriptor.options) || {};
return getDistanceForDirection(layout, gestureDirection);
};
const getProgressFromGesture = (mode, gesture, layout, descriptor) => {
const distance = getDistanceFromOptions(mode, {
// Make sure that we have a non-zero distance, otherwise there will be incorrect progress
// This causes blank screen on web if it was previously inside container with display: none
width: Math.max(1, layout.width),
height: Math.max(1, layout.height)
}, descriptor);
if (distance > 0) {
return gesture.interpolate({
inputRange: [0, distance],
outputRange: [1, 0]
});
}
return gesture.interpolate({
inputRange: [distance, 0],
outputRange: [0, 1]
});
};
export default class CardStack extends React.Component {
static getDerivedStateFromProps(props, state) {
if (props.routes === state.routes && props.descriptors === state.descriptors) {
return null;
}
const gestures = props.routes.reduce((acc, curr) => {
const descriptor = props.descriptors[curr.key];
const {
animationEnabled
} = (descriptor === null || descriptor === void 0 ? void 0 : descriptor.options) || {};
acc[curr.key] = state.gestures[curr.key] || new Animated.Value(props.openingRouteKeys.includes(curr.key) && animationEnabled !== false ? getDistanceFromOptions(props.mode, state.layout, descriptor) : 0);
return acc;
}, {});
return {
routes: props.routes,
scenes: props.routes.map((route, index, self) => {
const previousRoute = self[index - 1];
const nextRoute = self[index + 1];
const oldScene = state.scenes[index];
const currentGesture = gestures[route.key];
const previousGesture = previousRoute ? gestures[previousRoute.key] : undefined;
const nextGesture = nextRoute ? gestures[nextRoute.key] : undefined;
const descriptor = props.descriptors[route.key] || state.descriptors[route.key] || (oldScene ? oldScene.descriptor : FALLBACK_DESCRIPTOR);
const nextDescriptor = props.descriptors[nextRoute === null || nextRoute === void 0 ? void 0 : nextRoute.key] || state.descriptors[nextRoute === null || nextRoute === void 0 ? void 0 : nextRoute.key];
const previousDescriptor = props.descriptors[previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.key] || state.descriptors[previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.key];
const scene = {
route,
descriptor,
progress: {
current: getProgressFromGesture(props.mode, currentGesture, state.layout, descriptor),
next: nextGesture ? getProgressFromGesture(props.mode, nextGesture, state.layout, nextDescriptor) : undefined,
previous: previousGesture ? getProgressFromGesture(props.mode, previousGesture, state.layout, previousDescriptor) : undefined
},
__memo: [route, state.layout, descriptor, nextDescriptor, previousDescriptor, currentGesture, nextGesture, previousGesture]
};
if (oldScene && scene.__memo.every((it, i) => {
// @ts-ignore
return oldScene.__memo[i] === it;
})) {
return oldScene;
}
return scene;
}),
gestures,
descriptors: props.descriptors,
headerHeights: getHeaderHeights(props.routes, props.insets, state.descriptors, state.layout, state.headerHeights)
};
}
constructor(_props) {
super(_props);
_defineProperty(this, "handleLayout", e => {
const {
height,
width
} = e.nativeEvent.layout;
const layout = {
width,
height
};
this.setState((state, props) => {
if (height === state.layout.height && width === state.layout.width) {
return null;
}
return {
layout,
headerHeights: getHeaderHeights(props.routes, props.insets, state.descriptors, layout, {})
};
});
});
_defineProperty(this, "handleHeaderLayout", ({
route,
height
}) => {
this.setState(({
headerHeights
}) => {
const previousHeight = headerHeights[route.key];
if (previousHeight === height) {
return null;
}
return {
headerHeights: _objectSpread({}, headerHeights, {
[route.key]: height
})
};
});
});
_defineProperty(this, "getFocusedRoute", () => {
const {
state
} = this.props;
return state.routes[state.index];
});
const {
height: _height = 0,
width: _width = 0
} = Dimensions.get('window');
this.state = {
routes: [],
scenes: [],
gestures: {},
layout: {
height: _height,
width: _width
},
descriptors: this.props.descriptors,
// Used when card's header is null and mode is float to make transition
// between screens with headers and those without headers smooth.
// This is not a great heuristic here. We don't know synchronously
// on mount what the header height is so we have just used the most
// common cases here.
headerHeights: {}
};
}
render() {
const {
mode,
insets,
descriptors,
state,
routes,
closingRouteKeys,
onOpenRoute,
onCloseRoute,
getPreviousRoute,
getGesturesEnabled,
renderHeader,
renderScene,
headerMode,
onTransitionStart,
onTransitionEnd,
onPageChangeStart,
onPageChangeConfirm,
onPageChangeCancel
} = this.props;
const {
scenes,
layout,
gestures,
headerHeights
} = this.state;
const focusedRoute = state.routes[state.index];
const focusedDescriptor = descriptors[focusedRoute.key];
const focusedOptions = focusedDescriptor ? focusedDescriptor.options : {};
let defaultTransitionPreset = mode === 'modal' ? ModalTransition : DefaultTransition;
if (headerMode === 'screen') {
defaultTransitionPreset = _objectSpread({}, defaultTransitionPreset, {
headerStyleInterpolator: forNoAnimationHeader
});
}
const {
top = insets.top,
right = insets.right,
bottom = insets.bottom,
left = insets.left
} = focusedOptions.safeAreaInsets || {}; // Screens is buggy on iOS and web, so we only enable it on Android
// For modals, usually we want the screen underneath to be visible, so also disable it there
const isScreensEnabled = Platform.OS === 'android' && mode !== 'modal';
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(MaybeScreenContainer, {
enabled: isScreensEnabled,
style: styles.container,
onLayout: this.handleLayout
}, routes.map((route, index, self) => {
const focused = focusedRoute.key === route.key;
const gesture = gestures[route.key];
const scene = scenes[index];
const isScreenActive = scene.progress.next ? scene.progress.next.interpolate({
inputRange: [0, 1 - EPSILON, 1],
outputRange: [1, 1, 0],
extrapolate: 'clamp'
}) : 1;
const {
safeAreaInsets,
headerShown,
headerTransparent,
cardShadowEnabled,
cardOverlayEnabled,
cardOverlay,
cardStyle,
animationEnabled,
gestureResponseDistance,
gestureVelocityImpact,
gestureDirection = defaultTransitionPreset.gestureDirection,
transitionSpec = defaultTransitionPreset.transitionSpec,
cardStyleInterpolator = animationEnabled === false ? forNoAnimationCard : defaultTransitionPreset.cardStyleInterpolator,
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator
} = scene.descriptor ? scene.descriptor.options : {};
let transitionConfig = {
gestureDirection,
transitionSpec,
cardStyleInterpolator,
headerStyleInterpolator
}; // 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
} = nextScene.descriptor ? nextScene.descriptor.options : {};
transitionConfig = {
gestureDirection,
transitionSpec,
cardStyleInterpolator,
headerStyleInterpolator
};
}
}
const {
top: safeAreaInsetTop = insets.top,
right: safeAreaInsetRight = insets.right,
bottom: safeAreaInsetBottom = insets.bottom,
left: safeAreaInsetLeft = insets.left
} = safeAreaInsets || {};
const previousRoute = getPreviousRoute({
route: scene.route
});
let previousScene = scenes[index - 1];
if (previousRoute) {
// The previous scene will be shortly before the current scene in the array
// So loop back from current index to avoid looping over the full array
for (let j = index - 1; j >= 0; j--) {
const s = scenes[j];
if (s && s.route.key === previousRoute.key) {
previousScene = s;
break;
}
}
}
return /*#__PURE__*/React.createElement(MaybeScreen, {
key: route.key,
style: StyleSheet.absoluteFill,
enabled: isScreensEnabled,
active: isScreenActive,
pointerEvents: "box-none"
}, /*#__PURE__*/React.createElement(CardContainer, _extends({
index: index,
active: index === self.length - 1,
focused: focused,
closing: closingRouteKeys.includes(route.key),
layout: layout,
gesture: gesture,
scene: scene,
previousScene: previousScene,
safeAreaInsetTop: safeAreaInsetTop,
safeAreaInsetRight: safeAreaInsetRight,
safeAreaInsetBottom: safeAreaInsetBottom,
safeAreaInsetLeft: safeAreaInsetLeft,
cardOverlay: cardOverlay,
cardOverlayEnabled: cardOverlayEnabled,
cardShadowEnabled: cardShadowEnabled,
cardStyle: cardStyle,
onPageChangeStart: onPageChangeStart,
onPageChangeConfirm: onPageChangeConfirm,
onPageChangeCancel: onPageChangeCancel,
gestureResponseDistance: gestureResponseDistance,
headerHeight: headerHeights[route.key],
onHeaderHeightChange: this.handleHeaderLayout,
getPreviousRoute: getPreviousRoute,
getFocusedRoute: this.getFocusedRoute,
headerMode: headerMode,
headerShown: headerShown,
headerTransparent: headerTransparent,
renderHeader: renderHeader,
renderScene: renderScene,
onOpenRoute: onOpenRoute,
onCloseRoute: onCloseRoute,
onTransitionStart: onTransitionStart,
onTransitionEnd: onTransitionEnd,
gestureEnabled: index !== 0 && getGesturesEnabled({
route
}),
gestureVelocityImpact: gestureVelocityImpact
}, transitionConfig)));
})), headerMode === 'float' ? renderHeader({
mode: 'float',
layout,
insets: {
top,
right,
bottom,
left
},
scenes,
getPreviousRoute,
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
}) : null);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
overflow: 'hidden'
},
floating: {
position: 'absolute',
top: 0,
left: 0,
right: 0
}
});
//# sourceMappingURL=CardStack.js.map