UNPKG

19.4 kBJavaScriptView Raw
1function _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; }
2
3import { Background, getDefaultHeaderHeight, SafeAreaProviderCompat } from '@react-navigation/elements';
4import Color from 'color';
5import * as React from 'react';
6import { Animated, Platform, StyleSheet } from 'react-native';
7import { forModalPresentationIOS, forNoAnimation as forNoAnimationCard } from '../../TransitionConfigs/CardStyleInterpolators';
8import { DefaultTransition, ModalFadeTransition, ModalTransition } from '../../TransitionConfigs/TransitionPresets';
9import getDistanceForDirection from '../../utils/getDistanceForDirection';
10import { MaybeScreen, MaybeScreenContainer } from '../Screens';
11import { getIsModalPresentation } from './Card';
12import CardContainer from './CardContainer';
13const EPSILON = 0.01;
14const STATE_INACTIVE = 0;
15const STATE_TRANSITIONING_OR_BELOW_TOP = 1;
16const STATE_ON_TOP = 2;
17const FALLBACK_DESCRIPTOR = Object.freeze({
18 options: {}
19});
20
21const getInterpolationIndex = (scenes, index) => {
22 const {
23 cardStyleInterpolator
24 } = scenes[index].descriptor.options; // Start from current card and count backwards the number of cards with same interpolation
25
26 let interpolationIndex = 0;
27
28 for (let i = index - 1; i >= 0; i--) {
29 var _scenes$i;
30
31 const cardStyleInterpolatorCurrent = (_scenes$i = scenes[i]) === null || _scenes$i === void 0 ? void 0 : _scenes$i.descriptor.options.cardStyleInterpolator;
32
33 if (cardStyleInterpolatorCurrent !== cardStyleInterpolator) {
34 break;
35 }
36
37 interpolationIndex++;
38 }
39
40 return interpolationIndex;
41};
42
43const getIsModal = (scene, interpolationIndex, isParentModal) => {
44 if (isParentModal) {
45 return true;
46 }
47
48 const {
49 cardStyleInterpolator
50 } = scene.descriptor.options;
51 const isModalPresentation = getIsModalPresentation(cardStyleInterpolator);
52 const isModal = isModalPresentation && interpolationIndex !== 0;
53 return isModal;
54};
55
56const getHeaderHeights = (scenes, insets, isParentHeaderShown, isParentModal, layout, previous) => {
57 return scenes.reduce((acc, curr, index) => {
58 const {
59 headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
60 headerStyle
61 } = curr.descriptor.options;
62 const style = StyleSheet.flatten(headerStyle || {});
63 const height = 'height' in style && typeof style.height === 'number' ? style.height : previous[curr.route.key];
64 const interpolationIndex = getInterpolationIndex(scenes, index);
65 const isModal = getIsModal(curr, interpolationIndex, isParentModal);
66 acc[curr.route.key] = typeof height === 'number' ? height : getDefaultHeaderHeight(layout, isModal, headerStatusBarHeight);
67 return acc;
68 }, {});
69};
70
71const getDistanceFromOptions = (layout, descriptor) => {
72 const {
73 presentation,
74 gestureDirection = presentation === 'modal' ? ModalTransition.gestureDirection : DefaultTransition.gestureDirection
75 } = (descriptor === null || descriptor === void 0 ? void 0 : descriptor.options) || {};
76 return getDistanceForDirection(layout, gestureDirection);
77};
78
79const getProgressFromGesture = (gesture, layout, descriptor) => {
80 const distance = getDistanceFromOptions({
81 // Make sure that we have a non-zero distance, otherwise there will be incorrect progress
82 // This causes blank screen on web if it was previously inside container with display: none
83 width: Math.max(1, layout.width),
84 height: Math.max(1, layout.height)
85 }, descriptor);
86
87 if (distance > 0) {
88 return gesture.interpolate({
89 inputRange: [0, distance],
90 outputRange: [1, 0]
91 });
92 }
93
94 return gesture.interpolate({
95 inputRange: [distance, 0],
96 outputRange: [0, 1]
97 });
98};
99
100export default class CardStack extends React.Component {
101 static getDerivedStateFromProps(props, state) {
102 if (props.routes === state.routes && props.descriptors === state.descriptors) {
103 return null;
104 }
105
106 const gestures = props.routes.reduce((acc, curr) => {
107 const descriptor = props.descriptors[curr.key];
108 const {
109 animationEnabled
110 } = (descriptor === null || descriptor === void 0 ? void 0 : descriptor.options) || {};
111 acc[curr.key] = state.gestures[curr.key] || new Animated.Value(props.openingRouteKeys.includes(curr.key) && animationEnabled !== false ? getDistanceFromOptions(state.layout, descriptor) : 0);
112 return acc;
113 }, {});
114 const scenes = props.routes.map((route, index, self) => {
115 var _descriptor$options$h;
116
117 const previousRoute = self[index - 1];
118 const nextRoute = self[index + 1];
119 const oldScene = state.scenes[index];
120 const currentGesture = gestures[route.key];
121 const previousGesture = previousRoute ? gestures[previousRoute.key] : undefined;
122 const nextGesture = nextRoute ? gestures[nextRoute.key] : undefined;
123 const descriptor = props.descriptors[route.key] || state.descriptors[route.key] || (oldScene ? oldScene.descriptor : FALLBACK_DESCRIPTOR);
124 const nextDescriptor = props.descriptors[nextRoute === null || nextRoute === void 0 ? void 0 : nextRoute.key] || state.descriptors[nextRoute === null || nextRoute === void 0 ? void 0 : nextRoute.key];
125 const previousDescriptor = props.descriptors[previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.key] || state.descriptors[previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.key]; // When a screen is not the last, it should use next screen's transition config
126 // Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
127 // For example combining a slide and a modal transition would look wrong otherwise
128 // With this approach, combining different transition styles in the same navigator mostly looks right
129 // This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
130 // but majority of the transitions look alright
131
132 const optionsForTransitionConfig = index !== self.length - 1 && nextDescriptor && nextDescriptor.options.presentation !== 'transparentModal' ? nextDescriptor.options : descriptor.options;
133 let defaultTransitionPreset = optionsForTransitionConfig.presentation === 'modal' ? ModalTransition : optionsForTransitionConfig.presentation === 'transparentModal' ? ModalFadeTransition : DefaultTransition;
134 const {
135 animationEnabled = Platform.OS !== 'web' && Platform.OS !== 'windows' && Platform.OS !== 'macos',
136 gestureEnabled = Platform.OS === 'ios' && animationEnabled,
137 gestureDirection = defaultTransitionPreset.gestureDirection,
138 transitionSpec = defaultTransitionPreset.transitionSpec,
139 cardStyleInterpolator = animationEnabled === false ? forNoAnimationCard : defaultTransitionPreset.cardStyleInterpolator,
140 headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
141 cardOverlayEnabled = Platform.OS !== 'ios' && optionsForTransitionConfig.presentation !== 'transparentModal' || getIsModalPresentation(cardStyleInterpolator)
142 } = optionsForTransitionConfig;
143 const headerMode = (_descriptor$options$h = descriptor.options.headerMode) !== null && _descriptor$options$h !== void 0 ? _descriptor$options$h : !(optionsForTransitionConfig.presentation === 'modal' || optionsForTransitionConfig.presentation === 'transparentModal' || (nextDescriptor === null || nextDescriptor === void 0 ? void 0 : nextDescriptor.options.presentation) === 'modal' || (nextDescriptor === null || nextDescriptor === void 0 ? void 0 : nextDescriptor.options.presentation) === 'transparentModal' || getIsModalPresentation(cardStyleInterpolator)) && Platform.OS === 'ios' && descriptor.options.header === undefined ? 'float' : 'screen';
144 const scene = {
145 route,
146 descriptor: { ...descriptor,
147 options: { ...descriptor.options,
148 animationEnabled,
149 cardOverlayEnabled,
150 cardStyleInterpolator,
151 gestureDirection,
152 gestureEnabled,
153 headerStyleInterpolator,
154 transitionSpec,
155 headerMode
156 }
157 },
158 progress: {
159 current: getProgressFromGesture(currentGesture, state.layout, descriptor),
160 next: nextGesture && (nextDescriptor === null || nextDescriptor === void 0 ? void 0 : nextDescriptor.options.presentation) !== 'transparentModal' ? getProgressFromGesture(nextGesture, state.layout, nextDescriptor) : undefined,
161 previous: previousGesture ? getProgressFromGesture(previousGesture, state.layout, previousDescriptor) : undefined
162 },
163 __memo: [state.layout, descriptor, nextDescriptor, previousDescriptor, currentGesture, nextGesture, previousGesture]
164 };
165
166 if (oldScene && scene.__memo.every((it, i) => {
167 // @ts-expect-error: we haven't added __memo to the annotation to prevent usage elsewhere
168 return oldScene.__memo[i] === it;
169 })) {
170 return oldScene;
171 }
172
173 return scene;
174 });
175 return {
176 routes: props.routes,
177 scenes,
178 gestures,
179 descriptors: props.descriptors,
180 headerHeights: getHeaderHeights(scenes, props.insets, props.isParentHeaderShown, props.isParentModal, state.layout, state.headerHeights)
181 };
182 }
183
184 constructor(_props) {
185 super(_props);
186
187 _defineProperty(this, "handleLayout", e => {
188 const {
189 height,
190 width
191 } = e.nativeEvent.layout;
192 const layout = {
193 width,
194 height
195 };
196 this.setState((state, props) => {
197 if (height === state.layout.height && width === state.layout.width) {
198 return null;
199 }
200
201 return {
202 layout,
203 headerHeights: getHeaderHeights(state.scenes, props.insets, props.isParentHeaderShown, props.isParentModal, layout, state.headerHeights)
204 };
205 });
206 });
207
208 _defineProperty(this, "handleHeaderLayout", _ref => {
209 let {
210 route,
211 height
212 } = _ref;
213 this.setState(_ref2 => {
214 let {
215 headerHeights
216 } = _ref2;
217 const previousHeight = headerHeights[route.key];
218
219 if (previousHeight === height) {
220 return null;
221 }
222
223 return {
224 headerHeights: { ...headerHeights,
225 [route.key]: height
226 }
227 };
228 });
229 });
230
231 _defineProperty(this, "getFocusedRoute", () => {
232 const {
233 state
234 } = this.props;
235 return state.routes[state.index];
236 });
237
238 _defineProperty(this, "getPreviousScene", _ref3 => {
239 let {
240 route
241 } = _ref3;
242 const {
243 getPreviousRoute
244 } = this.props;
245 const {
246 scenes
247 } = this.state;
248 const previousRoute = getPreviousRoute({
249 route
250 });
251
252 if (previousRoute) {
253 const previousScene = scenes.find(scene => scene.descriptor.route.key === previousRoute.key);
254 return previousScene;
255 }
256
257 return undefined;
258 });
259
260 this.state = {
261 routes: [],
262 scenes: [],
263 gestures: {},
264 layout: SafeAreaProviderCompat.initialMetrics.frame,
265 descriptors: this.props.descriptors,
266 // Used when card's header is null and mode is float to make transition
267 // between screens with headers and those without headers smooth.
268 // This is not a great heuristic here. We don't know synchronously
269 // on mount what the header height is so we have just used the most
270 // common cases here.
271 headerHeights: {}
272 };
273 }
274
275 render() {
276 const {
277 insets,
278 state,
279 routes,
280 closingRouteKeys,
281 onOpenRoute,
282 onCloseRoute,
283 renderHeader,
284 renderScene,
285 isParentHeaderShown,
286 isParentModal,
287 onTransitionStart,
288 onTransitionEnd,
289 onGestureStart,
290 onGestureEnd,
291 onGestureCancel,
292 detachInactiveScreens = Platform.OS === 'web' || Platform.OS === 'android' || Platform.OS === 'ios'
293 } = this.props;
294 const {
295 scenes,
296 layout,
297 gestures,
298 headerHeights
299 } = this.state;
300 const focusedRoute = state.routes[state.index];
301 const focusedHeaderHeight = headerHeights[focusedRoute.key];
302 const isFloatHeaderAbsolute = this.state.scenes.slice(-2).some(scene => {
303 var _scene$descriptor$opt;
304
305 const options = (_scene$descriptor$opt = scene.descriptor.options) !== null && _scene$descriptor$opt !== void 0 ? _scene$descriptor$opt : {};
306 const {
307 headerMode,
308 headerTransparent,
309 headerShown = true
310 } = options;
311
312 if (headerTransparent || headerShown === false || headerMode === 'screen') {
313 return true;
314 }
315
316 return false;
317 });
318 let activeScreensLimit = 1;
319
320 for (let i = scenes.length - 1; i >= 0; i--) {
321 const {
322 options
323 } = scenes[i].descriptor;
324 const {
325 // By default, we don't want to detach the previous screen of the active one for modals
326 detachPreviousScreen = options.presentation === 'transparentModal' ? false : getIsModalPresentation(options.cardStyleInterpolator) ? i !== scenes.map(scene => scene.descriptor.options.cardStyleInterpolator).lastIndexOf(forModalPresentationIOS) : true
327 } = options;
328
329 if (detachPreviousScreen === false) {
330 activeScreensLimit++;
331 } else {
332 // Check at least last 2 screens before stopping
333 // This will make sure that screen isn't detached when another screen is animating on top of the transparent one
334 // For example, (Opaque -> Transparent -> Opaque)
335 if (i <= scenes.length - 2) {
336 break;
337 }
338 }
339 }
340
341 const floatingHeader = /*#__PURE__*/React.createElement(React.Fragment, {
342 key: "header"
343 }, renderHeader({
344 mode: 'float',
345 layout,
346 scenes,
347 getPreviousScene: this.getPreviousScene,
348 getFocusedRoute: this.getFocusedRoute,
349 onContentHeightChange: this.handleHeaderLayout,
350 style: [styles.floating, isFloatHeaderAbsolute && [// Without this, the header buttons won't be touchable on Android when headerTransparent: true
351 {
352 height: focusedHeaderHeight
353 }, styles.absolute]]
354 }));
355 return /*#__PURE__*/React.createElement(Background, null, isFloatHeaderAbsolute ? null : floatingHeader, /*#__PURE__*/React.createElement(MaybeScreenContainer, {
356 enabled: detachInactiveScreens,
357 style: styles.container,
358 onLayout: this.handleLayout
359 }, routes.map((route, index, self) => {
360 var _scenes, _scenes2;
361
362 const focused = focusedRoute.key === route.key;
363 const gesture = gestures[route.key];
364 const scene = scenes[index]; // For the screens that shouldn't be active, the value is 0
365 // For those that should be active, but are not the top screen, the value is 1
366 // For those on top of the stack and with interaction enabled, the value is 2
367 // For the old implementation, it stays the same it was
368
369 let isScreenActive = 1;
370
371 if (index < self.length - activeScreensLimit - 1) {
372 // screen should be inactive because it is too deep in the stack
373 isScreenActive = STATE_INACTIVE;
374 } else {
375 const sceneForActivity = scenes[self.length - 1];
376 const outputValue = index === self.length - 1 ? STATE_ON_TOP // the screen is on top after the transition
377 : index >= self.length - activeScreensLimit ? STATE_TRANSITIONING_OR_BELOW_TOP // the screen should stay active after the transition, it is not on top but is in activeLimit
378 : STATE_INACTIVE; // the screen should be active only during the transition, it is at the edge of activeLimit
379
380 isScreenActive = sceneForActivity ? sceneForActivity.progress.current.interpolate({
381 inputRange: [0, 1 - EPSILON, 1],
382 outputRange: [1, 1, outputValue],
383 extrapolate: 'clamp'
384 }) : STATE_TRANSITIONING_OR_BELOW_TOP;
385 }
386
387 const {
388 headerShown = true,
389 headerTransparent,
390 headerStyle,
391 headerTintColor
392 } = scene.descriptor.options;
393 const safeAreaInsetTop = insets.top;
394 const safeAreaInsetRight = insets.right;
395 const safeAreaInsetBottom = insets.bottom;
396 const safeAreaInsetLeft = insets.left;
397 const headerHeight = headerShown !== false ? headerHeights[route.key] : 0;
398 let headerDarkContent;
399
400 if (headerShown) {
401 if (typeof headerTintColor === 'string') {
402 headerDarkContent = Color(headerTintColor).isDark();
403 } else {
404 const flattenedHeaderStyle = StyleSheet.flatten(headerStyle);
405
406 if (flattenedHeaderStyle && 'backgroundColor' in flattenedHeaderStyle && typeof flattenedHeaderStyle.backgroundColor === 'string') {
407 headerDarkContent = !Color(flattenedHeaderStyle.backgroundColor).isDark();
408 }
409 }
410 } // Start from current card and count backwards the number of cards with same interpolation
411
412
413 const interpolationIndex = getInterpolationIndex(scenes, index);
414 const isModal = getIsModal(scene, interpolationIndex, isParentModal);
415 const isNextScreenTransparent = ((_scenes = scenes[index + 1]) === null || _scenes === void 0 ? void 0 : _scenes.descriptor.options.presentation) === 'transparentModal';
416 const detachCurrentScreen = ((_scenes2 = scenes[index + 1]) === null || _scenes2 === void 0 ? void 0 : _scenes2.descriptor.options.detachPreviousScreen) !== false;
417 return /*#__PURE__*/React.createElement(MaybeScreen, {
418 key: route.key,
419 style: StyleSheet.absoluteFill,
420 enabled: detachInactiveScreens,
421 active: isScreenActive,
422 pointerEvents: "box-none"
423 }, /*#__PURE__*/React.createElement(CardContainer, {
424 index: index,
425 interpolationIndex: interpolationIndex,
426 modal: isModal,
427 active: index === self.length - 1,
428 focused: focused,
429 closing: closingRouteKeys.includes(route.key),
430 layout: layout,
431 gesture: gesture,
432 scene: scene,
433 safeAreaInsetTop: safeAreaInsetTop,
434 safeAreaInsetRight: safeAreaInsetRight,
435 safeAreaInsetBottom: safeAreaInsetBottom,
436 safeAreaInsetLeft: safeAreaInsetLeft,
437 onGestureStart: onGestureStart,
438 onGestureCancel: onGestureCancel,
439 onGestureEnd: onGestureEnd,
440 headerHeight: headerHeight,
441 isParentHeaderShown: isParentHeaderShown,
442 onHeaderHeightChange: this.handleHeaderLayout,
443 getPreviousScene: this.getPreviousScene,
444 getFocusedRoute: this.getFocusedRoute,
445 headerDarkContent: headerDarkContent,
446 hasAbsoluteFloatHeader: isFloatHeaderAbsolute && !headerTransparent,
447 renderHeader: renderHeader,
448 renderScene: renderScene,
449 onOpenRoute: onOpenRoute,
450 onCloseRoute: onCloseRoute,
451 onTransitionStart: onTransitionStart,
452 onTransitionEnd: onTransitionEnd,
453 isNextScreenTransparent: isNextScreenTransparent,
454 detachCurrentScreen: detachCurrentScreen
455 }));
456 })), isFloatHeaderAbsolute ? floatingHeader : null);
457 }
458
459}
460const styles = StyleSheet.create({
461 container: {
462 flex: 1
463 },
464 absolute: {
465 position: 'absolute',
466 top: 0,
467 left: 0,
468 right: 0
469 },
470 floating: {
471 zIndex: 1
472 }
473});
474//# sourceMappingURL=CardStack.js.map
\No newline at end of file