@react-navigation/stack 8.0.0-alpha.3 → 8.0.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/module/index.js.map +1 -1
- package/lib/module/navigators/createStackNavigator.js +3 -7
- package/lib/module/navigators/createStackNavigator.js.map +1 -1
- package/lib/module/utils/useCardAnimation.js +1 -1
- package/lib/module/utils/useCardAnimation.js.map +1 -1
- package/lib/module/utils/useGestureHandlerRef.js +1 -1
- package/lib/module/utils/useGestureHandlerRef.js.map +1 -1
- package/lib/module/utils/useKeyboardManager.js +50 -26
- package/lib/module/utils/useKeyboardManager.js.map +1 -1
- package/lib/module/views/Header/Header.js +2 -2
- package/lib/module/views/Header/Header.js.map +1 -1
- package/lib/module/views/Header/HeaderContainer.js +51 -17
- package/lib/module/views/Header/HeaderContainer.js.map +1 -1
- package/lib/module/views/Header/HeaderSegment.js +2 -2
- package/lib/module/views/Header/HeaderSegment.js.map +1 -1
- package/lib/module/views/Stack/Card.js +25 -20
- package/lib/module/views/Stack/Card.js.map +1 -1
- package/lib/module/views/Stack/CardContainer.js +69 -98
- package/lib/module/views/Stack/CardContainer.js.map +1 -1
- package/lib/module/views/Stack/CardStack.js +134 -116
- package/lib/module/views/Stack/CardStack.js.map +1 -1
- package/lib/module/views/Stack/StackView.js +82 -49
- package/lib/module/views/Stack/StackView.js.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/navigators/createStackNavigator.d.ts +8 -14
- package/lib/typescript/src/navigators/createStackNavigator.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +46 -53
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/gestureActivationCriteria.d.ts +1 -1
- package/lib/typescript/src/utils/gestureActivationCriteria.d.ts.map +1 -1
- package/lib/typescript/src/utils/useKeyboardManager.d.ts +9 -2
- package/lib/typescript/src/utils/useKeyboardManager.d.ts.map +1 -1
- package/lib/typescript/src/views/Header/Header.d.ts +1 -1
- package/lib/typescript/src/views/Header/Header.d.ts.map +1 -1
- package/lib/typescript/src/views/Header/HeaderContainer.d.ts +7 -2
- package/lib/typescript/src/views/Header/HeaderContainer.d.ts.map +1 -1
- package/lib/typescript/src/views/Header/HeaderSegment.d.ts +2 -2
- package/lib/typescript/src/views/Header/HeaderSegment.d.ts.map +1 -1
- package/lib/typescript/src/views/Stack/Card.d.ts +3 -3
- package/lib/typescript/src/views/Stack/Card.d.ts.map +1 -1
- package/lib/typescript/src/views/Stack/CardContainer.d.ts +4 -6
- package/lib/typescript/src/views/Stack/CardContainer.d.ts.map +1 -1
- package/lib/typescript/src/views/Stack/CardStack.d.ts +4 -3
- package/lib/typescript/src/views/Stack/CardStack.d.ts.map +1 -1
- package/lib/typescript/src/views/Stack/StackView.d.ts +6 -9
- package/lib/typescript/src/views/Stack/StackView.d.ts.map +1 -1
- package/package.json +20 -22
- package/src/index.tsx +1 -0
- package/src/navigators/createStackNavigator.tsx +9 -40
- package/src/types.tsx +73 -73
- package/src/utils/gestureActivationCriteria.tsx +1 -1
- package/src/utils/useCardAnimation.tsx +1 -1
- package/src/utils/useGestureHandlerRef.tsx +1 -1
- package/src/utils/useKeyboardManager.tsx +67 -33
- package/src/views/Header/Header.tsx +2 -2
- package/src/views/Header/HeaderContainer.tsx +66 -20
- package/src/views/Header/HeaderSegment.tsx +4 -4
- package/src/views/Stack/Card.tsx +32 -23
- package/src/views/Stack/CardContainer.tsx +81 -127
- package/src/views/Stack/CardStack.tsx +334 -318
- package/src/views/Stack/StackView.tsx +138 -66
- package/lib/module/views/Stack/CardA11yWrapper.js +0 -43
- package/lib/module/views/Stack/CardA11yWrapper.js.map +0 -1
- package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts +0 -15
- package/lib/typescript/src/views/Stack/CardA11yWrapper.d.ts.map +0 -1
- package/src/views/Stack/CardA11yWrapper.tsx +0 -67
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { getDefaultHeaderHeight } from '@react-navigation/elements';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ActivityView,
|
|
4
|
+
SafeAreaProviderCompat,
|
|
5
|
+
} from '@react-navigation/elements/internal';
|
|
3
6
|
import type {
|
|
4
7
|
LocaleDirection,
|
|
5
8
|
ParamListBase,
|
|
@@ -15,7 +18,6 @@ import {
|
|
|
15
18
|
View,
|
|
16
19
|
} from 'react-native';
|
|
17
20
|
import type { EdgeInsets } from 'react-native-safe-area-context';
|
|
18
|
-
import { Screen, ScreenContainer } from 'react-native-screens';
|
|
19
21
|
|
|
20
22
|
import {
|
|
21
23
|
forModalPresentationIOS,
|
|
@@ -44,16 +46,20 @@ import type {
|
|
|
44
46
|
StackNavigationOptions,
|
|
45
47
|
TransitionPreset,
|
|
46
48
|
} from '../../types';
|
|
47
|
-
import { findLastIndex } from '../../utils/findLastIndex';
|
|
48
49
|
import { getDistanceForDirection } from '../../utils/getDistanceForDirection';
|
|
49
50
|
import { getModalRouteKeys } from '../../utils/getModalRoutesKeys';
|
|
50
|
-
import type {
|
|
51
|
+
import type {
|
|
52
|
+
HeaderHeight,
|
|
53
|
+
Props as HeaderContainerProps,
|
|
54
|
+
} from '../Header/HeaderContainer';
|
|
51
55
|
import { CardContainer } from './CardContainer';
|
|
52
56
|
|
|
53
57
|
type GestureValues = {
|
|
54
58
|
[key: string]: Animated.Value;
|
|
55
59
|
};
|
|
56
60
|
|
|
61
|
+
type HeaderHeights = Record<string, HeaderHeight>;
|
|
62
|
+
|
|
57
63
|
type Props = {
|
|
58
64
|
direction: LocaleDirection;
|
|
59
65
|
insets: EdgeInsets;
|
|
@@ -62,6 +68,7 @@ type Props = {
|
|
|
62
68
|
routes: Route<string>[];
|
|
63
69
|
openingRouteKeys: string[];
|
|
64
70
|
closingRouteKeys: string[];
|
|
71
|
+
replacingRouteKeys: string[];
|
|
65
72
|
onOpenRoute: (props: { route: Route<string> }) => void;
|
|
66
73
|
onCloseRoute: (props: { route: Route<string> }) => void;
|
|
67
74
|
getPreviousRoute: (props: {
|
|
@@ -87,8 +94,7 @@ type State = {
|
|
|
87
94
|
scenes: Scene[];
|
|
88
95
|
gestures: GestureValues;
|
|
89
96
|
layout: Layout;
|
|
90
|
-
|
|
91
|
-
headerHeights: Record<string, number>;
|
|
97
|
+
headerHeights: HeaderHeights;
|
|
92
98
|
};
|
|
93
99
|
|
|
94
100
|
const NAMED_TRANSITIONS_PRESETS = {
|
|
@@ -107,14 +113,10 @@ const NAMED_TRANSITIONS_PRESETS = {
|
|
|
107
113
|
}),
|
|
108
114
|
} as const satisfies Record<StackAnimationName, TransitionPreset>;
|
|
109
115
|
|
|
110
|
-
const EPSILON = 1e-5;
|
|
111
|
-
|
|
112
|
-
const STATE_INACTIVE = 0;
|
|
113
|
-
const STATE_TRANSITIONING_OR_BELOW_TOP = 1;
|
|
114
|
-
const STATE_ON_TOP = 2;
|
|
115
|
-
|
|
116
116
|
const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
|
|
117
117
|
|
|
118
|
+
const HIDDEN_HEADER_HEIGHT = { measured: false, value: 0 };
|
|
119
|
+
|
|
118
120
|
const getInterpolationIndex = (scenes: Scene[], index: number) => {
|
|
119
121
|
const { cardStyleInterpolator } = scenes[index].descriptor.options;
|
|
120
122
|
|
|
@@ -167,9 +169,9 @@ const getHeaderHeights = (
|
|
|
167
169
|
isParentHeaderShown: boolean,
|
|
168
170
|
isParentModal: boolean,
|
|
169
171
|
layout: Layout,
|
|
170
|
-
previous:
|
|
172
|
+
previous: HeaderHeights
|
|
171
173
|
) => {
|
|
172
|
-
return scenes.reduce<
|
|
174
|
+
return scenes.reduce<HeaderHeights>((acc, curr, index) => {
|
|
173
175
|
const {
|
|
174
176
|
headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
|
|
175
177
|
headerStyle,
|
|
@@ -180,19 +182,34 @@ const getHeaderHeights = (
|
|
|
180
182
|
const height =
|
|
181
183
|
'height' in style && typeof style.height === 'number'
|
|
182
184
|
? style.height
|
|
183
|
-
:
|
|
185
|
+
: undefined;
|
|
184
186
|
|
|
185
187
|
const interpolationIndex = getInterpolationIndex(scenes, index);
|
|
186
188
|
const isModal = getIsModal(curr, interpolationIndex, isParentModal);
|
|
187
189
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
190
|
+
if (typeof height === 'number') {
|
|
191
|
+
acc[curr.route.key] = { measured: true, value: height };
|
|
192
|
+
} else {
|
|
193
|
+
const previousHeight = previous[curr.route.key];
|
|
194
|
+
const defaultHeight = getDefaultHeaderHeight({
|
|
195
|
+
landscape: layout.width > layout.height,
|
|
196
|
+
modalPresentation: isModal,
|
|
197
|
+
topInset: headerStatusBarHeight,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// If we hadn't measured before, use re-computed default height
|
|
201
|
+
if (
|
|
202
|
+
!previousHeight?.measured &&
|
|
203
|
+
defaultHeight !== previousHeight?.value
|
|
204
|
+
) {
|
|
205
|
+
acc[curr.route.key] = {
|
|
206
|
+
measured: false,
|
|
207
|
+
value: defaultHeight,
|
|
208
|
+
};
|
|
209
|
+
} else if (previousHeight) {
|
|
210
|
+
acc[curr.route.key] = previousHeight;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
196
213
|
|
|
197
214
|
return acc;
|
|
198
215
|
}, {});
|
|
@@ -263,6 +280,27 @@ export function getAnimationEnabled(animation: StackAnimationName | undefined) {
|
|
|
263
280
|
return getDefaultAnimation(animation) !== 'none';
|
|
264
281
|
}
|
|
265
282
|
|
|
283
|
+
const getAllRoutes = (
|
|
284
|
+
routes: Route<string>[],
|
|
285
|
+
state: StackNavigationState<ParamListBase>
|
|
286
|
+
) => {
|
|
287
|
+
const routeKeys = new Set(routes.map((route) => route.key));
|
|
288
|
+
const inactiveRoutes = state.routes.slice(state.index + 1);
|
|
289
|
+
|
|
290
|
+
// If a route is moved from active routes to inactive routes,
|
|
291
|
+
// It can still be in the local copy of `routes` until the animation ends
|
|
292
|
+
// So we need to deduplicate the routes to avoid rendering the same route twice
|
|
293
|
+
return [
|
|
294
|
+
...routes,
|
|
295
|
+
...inactiveRoutes.filter((route) => !routeKeys.has(route.key)),
|
|
296
|
+
];
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const isInactiveRoute = (route: Route<string>, routes: Route<string>[]) =>
|
|
300
|
+
// `routes` contains active routes and routes animating during transitions.
|
|
301
|
+
// Any route added by `getAllRoutes` that's missing from this list is inactive.
|
|
302
|
+
!routes.some((currentRoute) => currentRoute.key === route.key);
|
|
303
|
+
|
|
266
304
|
export class CardStack extends React.Component<Props, State> {
|
|
267
305
|
static getDerivedStateFromProps(
|
|
268
306
|
props: Props,
|
|
@@ -275,10 +313,9 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
275
313
|
return null;
|
|
276
314
|
}
|
|
277
315
|
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
].reduce<GestureValues>((acc, curr) => {
|
|
316
|
+
const allRoutes = getAllRoutes(props.routes, props.state);
|
|
317
|
+
|
|
318
|
+
const gestures = allRoutes.reduce<GestureValues>((acc, curr) => {
|
|
282
319
|
const descriptor = props.descriptors[curr.key];
|
|
283
320
|
const { animation } = descriptor?.options || {};
|
|
284
321
|
|
|
@@ -287,7 +324,7 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
287
324
|
new Animated.Value(
|
|
288
325
|
(props.openingRouteKeys.includes(curr.key) &&
|
|
289
326
|
getAnimationEnabled(animation)) ||
|
|
290
|
-
|
|
327
|
+
isInactiveRoute(curr, props.routes)
|
|
291
328
|
? getDistanceFromOptions(
|
|
292
329
|
state.layout,
|
|
293
330
|
descriptor?.options,
|
|
@@ -299,254 +336,170 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
299
336
|
return acc;
|
|
300
337
|
}, {});
|
|
301
338
|
|
|
302
|
-
const modalRouteKeys = getModalRouteKeys(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// or current screen is a modal when no presentation is specified
|
|
356
|
-
const isModal = modalRouteKeys.includes(route.key);
|
|
357
|
-
|
|
358
|
-
const animation = getDefaultAnimation(
|
|
359
|
-
optionsForTransitionConfig.animation
|
|
360
|
-
);
|
|
339
|
+
const modalRouteKeys = getModalRouteKeys(allRoutes, props.descriptors);
|
|
340
|
+
|
|
341
|
+
const scenes = allRoutes.map((route, index, self) => {
|
|
342
|
+
// For preloaded or retained screens, we don't care about the previous and the next screen
|
|
343
|
+
const isInactive = isInactiveRoute(route, props.routes);
|
|
344
|
+
const previousRoute = isInactive ? undefined : self[index - 1];
|
|
345
|
+
const nextRoute = isInactive ? undefined : self[index + 1];
|
|
346
|
+
|
|
347
|
+
const oldScene = state.scenes[index];
|
|
348
|
+
|
|
349
|
+
const currentGesture = gestures[route.key];
|
|
350
|
+
const previousGesture = previousRoute
|
|
351
|
+
? gestures[previousRoute.key]
|
|
352
|
+
: undefined;
|
|
353
|
+
const nextGesture = nextRoute ? gestures[nextRoute.key] : undefined;
|
|
354
|
+
|
|
355
|
+
const descriptor =
|
|
356
|
+
props.descriptors[route.key] ||
|
|
357
|
+
state.descriptors[route.key] ||
|
|
358
|
+
(oldScene ? oldScene.descriptor : FALLBACK_DESCRIPTOR);
|
|
359
|
+
|
|
360
|
+
const nextOptions =
|
|
361
|
+
nextRoute &&
|
|
362
|
+
(props.descriptors[nextRoute?.key] || state.descriptors[nextRoute?.key])
|
|
363
|
+
?.options;
|
|
364
|
+
|
|
365
|
+
const previousOptions =
|
|
366
|
+
previousRoute &&
|
|
367
|
+
(
|
|
368
|
+
props.descriptors[previousRoute?.key] ||
|
|
369
|
+
state.descriptors[previousRoute?.key]
|
|
370
|
+
)?.options;
|
|
371
|
+
|
|
372
|
+
// When a screen is not the last, it should use next screen's transition config
|
|
373
|
+
// Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
|
|
374
|
+
// For example combining a slide and a modal transition would look wrong otherwise
|
|
375
|
+
// With this approach, combining different transition styles in the same navigator mostly looks right
|
|
376
|
+
// This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
|
|
377
|
+
// but the majority of the transitions look alright
|
|
378
|
+
const optionsForTransitionConfig =
|
|
379
|
+
index !== self.length - 1 &&
|
|
380
|
+
nextOptions &&
|
|
381
|
+
nextOptions?.presentation !== 'transparentModal'
|
|
382
|
+
? nextOptions
|
|
383
|
+
: descriptor.options;
|
|
384
|
+
|
|
385
|
+
// Assume modal if there are already modal screens in the stack
|
|
386
|
+
// or current screen is a modal when no presentation is specified
|
|
387
|
+
const isModal = modalRouteKeys.includes(route.key);
|
|
388
|
+
|
|
389
|
+
const animation = getDefaultAnimation(
|
|
390
|
+
optionsForTransitionConfig.animation
|
|
391
|
+
);
|
|
361
392
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
},
|
|
393
|
+
const isAnimationEnabled = getAnimationEnabled(animation);
|
|
394
|
+
|
|
395
|
+
const transitionPreset =
|
|
396
|
+
animation !== 'default'
|
|
397
|
+
? NAMED_TRANSITIONS_PRESETS[animation]
|
|
398
|
+
: optionsForTransitionConfig.presentation === 'transparentModal'
|
|
399
|
+
? ModalFadeTransition
|
|
400
|
+
: optionsForTransitionConfig.presentation === 'modal' || isModal
|
|
401
|
+
? ModalTransition
|
|
402
|
+
: DefaultTransition;
|
|
403
|
+
|
|
404
|
+
const {
|
|
405
|
+
gestureEnabled = Platform.OS === 'ios' && isAnimationEnabled,
|
|
406
|
+
gestureDirection = transitionPreset.gestureDirection,
|
|
407
|
+
transitionSpec = transitionPreset.transitionSpec,
|
|
408
|
+
cardStyleInterpolator = isAnimationEnabled
|
|
409
|
+
? transitionPreset.cardStyleInterpolator
|
|
410
|
+
: forNoAnimationCard,
|
|
411
|
+
headerStyleInterpolator = transitionPreset.headerStyleInterpolator,
|
|
412
|
+
cardOverlayEnabled = (Platform.OS !== 'ios' &&
|
|
413
|
+
optionsForTransitionConfig.presentation !== 'transparentModal') ||
|
|
414
|
+
getIsModalPresentation(cardStyleInterpolator),
|
|
415
|
+
} = optionsForTransitionConfig;
|
|
416
|
+
|
|
417
|
+
const headerMode: StackHeaderMode =
|
|
418
|
+
descriptor.options.headerMode ??
|
|
419
|
+
(!(
|
|
420
|
+
optionsForTransitionConfig.presentation === 'modal' ||
|
|
421
|
+
optionsForTransitionConfig.presentation === 'transparentModal' ||
|
|
422
|
+
nextOptions?.presentation === 'modal' ||
|
|
423
|
+
nextOptions?.presentation === 'transparentModal' ||
|
|
424
|
+
getIsModalPresentation(cardStyleInterpolator)
|
|
425
|
+
) &&
|
|
426
|
+
Platform.OS === 'ios' &&
|
|
427
|
+
descriptor.options.header === undefined
|
|
428
|
+
? 'float'
|
|
429
|
+
: 'screen');
|
|
430
|
+
|
|
431
|
+
const isRTL = props.direction === 'rtl';
|
|
432
|
+
|
|
433
|
+
const scene = {
|
|
434
|
+
route,
|
|
435
|
+
descriptor: {
|
|
436
|
+
...descriptor,
|
|
437
|
+
options: {
|
|
438
|
+
...descriptor.options,
|
|
439
|
+
animation,
|
|
440
|
+
cardOverlayEnabled,
|
|
441
|
+
cardStyleInterpolator,
|
|
442
|
+
gestureDirection,
|
|
443
|
+
gestureEnabled,
|
|
444
|
+
headerStyleInterpolator,
|
|
445
|
+
transitionSpec,
|
|
446
|
+
headerMode,
|
|
417
447
|
},
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
nextGesture,
|
|
429
|
-
state.layout,
|
|
430
|
-
nextOptions,
|
|
431
|
-
isRTL
|
|
432
|
-
)
|
|
433
|
-
: undefined,
|
|
434
|
-
previous: previousGesture
|
|
448
|
+
},
|
|
449
|
+
progress: {
|
|
450
|
+
current: getProgressFromGesture(
|
|
451
|
+
currentGesture,
|
|
452
|
+
state.layout,
|
|
453
|
+
descriptor.options,
|
|
454
|
+
isRTL
|
|
455
|
+
),
|
|
456
|
+
next:
|
|
457
|
+
nextGesture && nextOptions?.presentation !== 'transparentModal'
|
|
435
458
|
? getProgressFromGesture(
|
|
436
|
-
|
|
459
|
+
nextGesture,
|
|
437
460
|
state.layout,
|
|
438
|
-
|
|
461
|
+
nextOptions,
|
|
439
462
|
isRTL
|
|
440
463
|
)
|
|
441
464
|
: undefined,
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
return oldScene;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
return scene;
|
|
465
|
-
}
|
|
466
|
-
);
|
|
465
|
+
previous: previousGesture
|
|
466
|
+
? getProgressFromGesture(
|
|
467
|
+
previousGesture,
|
|
468
|
+
state.layout,
|
|
469
|
+
previousOptions,
|
|
470
|
+
isRTL
|
|
471
|
+
)
|
|
472
|
+
: undefined,
|
|
473
|
+
},
|
|
474
|
+
__memo: [
|
|
475
|
+
state.layout,
|
|
476
|
+
descriptor,
|
|
477
|
+
nextOptions,
|
|
478
|
+
previousOptions,
|
|
479
|
+
currentGesture,
|
|
480
|
+
nextGesture,
|
|
481
|
+
previousGesture,
|
|
482
|
+
],
|
|
483
|
+
};
|
|
467
484
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const {
|
|
477
|
-
// By default, we don't want to detach the previous screen of the active one for modals
|
|
478
|
-
detachPreviousScreen = options.presentation === 'transparentModal'
|
|
479
|
-
? false
|
|
480
|
-
: getIsModalPresentation(options.cardStyleInterpolator)
|
|
481
|
-
? i !==
|
|
482
|
-
findLastIndex(scenes, (scene) => {
|
|
483
|
-
const { cardStyleInterpolator } = scene.descriptor.options;
|
|
484
|
-
|
|
485
|
-
return (
|
|
486
|
-
cardStyleInterpolator === forModalPresentationIOS ||
|
|
487
|
-
cardStyleInterpolator?.name === 'forModalPresentationIOS'
|
|
488
|
-
);
|
|
489
|
-
})
|
|
490
|
-
: true,
|
|
491
|
-
} = options;
|
|
492
|
-
|
|
493
|
-
if (detachPreviousScreen === false) {
|
|
494
|
-
activeScreensLimit++;
|
|
495
|
-
} else {
|
|
496
|
-
// Check at least last 2 screens before stopping
|
|
497
|
-
// This will make sure that screen isn't detached when another screen is animating on top of the transparent one
|
|
498
|
-
// e.g. opaque -> transparent -> opaque
|
|
499
|
-
if (i <= props.routes.length - 2) {
|
|
500
|
-
break;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
485
|
+
if (
|
|
486
|
+
oldScene &&
|
|
487
|
+
scene.__memo.every((it, i) => {
|
|
488
|
+
// @ts-expect-error: we haven't added __memo to the annotation to prevent usage elsewhere
|
|
489
|
+
return oldScene.__memo[i] === it;
|
|
490
|
+
})
|
|
491
|
+
) {
|
|
492
|
+
return oldScene;
|
|
503
493
|
}
|
|
504
494
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// 0 - inactive, the screen is detached
|
|
508
|
-
// 1 - transitioning or below the top screen, the screen is mounted but interaction is disabled
|
|
509
|
-
// 2 - on top of the stack, the screen is mounted and interaction is enabled
|
|
510
|
-
let activityState:
|
|
511
|
-
| Animated.AnimatedInterpolation<0 | 1 | 2>
|
|
512
|
-
| 0
|
|
513
|
-
| 1
|
|
514
|
-
| 2;
|
|
515
|
-
|
|
516
|
-
const lastActiveState = state.activeStates[index];
|
|
517
|
-
const activeAfterTransition = index >= self.length - activeScreensLimit;
|
|
518
|
-
|
|
519
|
-
if (lastActiveState === STATE_INACTIVE && !activeAfterTransition) {
|
|
520
|
-
// screen was inactive before and it will still be inactive after the transition
|
|
521
|
-
activityState = STATE_INACTIVE;
|
|
522
|
-
} else {
|
|
523
|
-
const sceneForActivity = scenes[self.length - 1];
|
|
524
|
-
const outputValue =
|
|
525
|
-
index === self.length - 1
|
|
526
|
-
? STATE_ON_TOP // the screen is on top after the transition
|
|
527
|
-
: activeAfterTransition
|
|
528
|
-
? STATE_TRANSITIONING_OR_BELOW_TOP // the screen should stay active after the transition, it is not on top but is in activeLimit
|
|
529
|
-
: STATE_INACTIVE; // the screen should be active only during the transition, it is at the edge of activeLimit
|
|
530
|
-
|
|
531
|
-
activityState = sceneForActivity
|
|
532
|
-
? sceneForActivity.progress.current.interpolate({
|
|
533
|
-
inputRange: [0, 1 - EPSILON, 1],
|
|
534
|
-
outputRange: [1, 1, outputValue],
|
|
535
|
-
extrapolate: 'clamp',
|
|
536
|
-
})
|
|
537
|
-
: STATE_TRANSITIONING_OR_BELOW_TOP;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
return activityState;
|
|
541
|
-
});
|
|
542
|
-
}
|
|
495
|
+
return scene;
|
|
496
|
+
});
|
|
543
497
|
|
|
544
498
|
return {
|
|
545
499
|
routes: props.routes,
|
|
546
500
|
scenes,
|
|
547
501
|
gestures,
|
|
548
502
|
descriptors: props.descriptors,
|
|
549
|
-
activeStates,
|
|
550
503
|
headerHeights: getHeaderHeights(
|
|
551
504
|
scenes,
|
|
552
505
|
props.insets,
|
|
@@ -567,7 +520,6 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
567
520
|
gestures: {},
|
|
568
521
|
layout: SafeAreaProviderCompat.initialMetrics.frame,
|
|
569
522
|
descriptors: this.props.descriptors,
|
|
570
|
-
activeStates: [],
|
|
571
523
|
// Used when card's header is null and mode is float to make transition
|
|
572
524
|
// between screens with headers and those without headers smooth.
|
|
573
525
|
// This is not a great heuristic here. We don't know synchronously
|
|
@@ -611,14 +563,17 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
611
563
|
this.setState(({ headerHeights }) => {
|
|
612
564
|
const previousHeight = headerHeights[route.key];
|
|
613
565
|
|
|
614
|
-
if (previousHeight === height) {
|
|
566
|
+
if (previousHeight?.value === height) {
|
|
615
567
|
return null;
|
|
616
568
|
}
|
|
617
569
|
|
|
618
570
|
return {
|
|
619
571
|
headerHeights: {
|
|
620
572
|
...headerHeights,
|
|
621
|
-
[route.key]:
|
|
573
|
+
[route.key]: {
|
|
574
|
+
measured: true,
|
|
575
|
+
value: height,
|
|
576
|
+
},
|
|
622
577
|
},
|
|
623
578
|
};
|
|
624
579
|
});
|
|
@@ -654,23 +609,22 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
654
609
|
routes,
|
|
655
610
|
openingRouteKeys,
|
|
656
611
|
closingRouteKeys,
|
|
612
|
+
replacingRouteKeys,
|
|
657
613
|
onOpenRoute,
|
|
658
614
|
onCloseRoute,
|
|
615
|
+
onGestureStart,
|
|
616
|
+
onGestureEnd,
|
|
617
|
+
onGestureCancel,
|
|
659
618
|
renderHeader,
|
|
660
619
|
isParentHeaderShown,
|
|
661
620
|
isParentModal,
|
|
662
621
|
onTransitionStart,
|
|
663
622
|
onTransitionEnd,
|
|
664
|
-
onGestureStart,
|
|
665
|
-
onGestureEnd,
|
|
666
|
-
onGestureCancel,
|
|
667
|
-
detachInactiveScreens = Platform.OS === 'web' ||
|
|
668
|
-
Platform.OS === 'android' ||
|
|
669
|
-
Platform.OS === 'ios',
|
|
670
623
|
} = this.props;
|
|
671
624
|
|
|
672
|
-
const { scenes, layout, gestures,
|
|
673
|
-
|
|
625
|
+
const { scenes, layout, gestures, headerHeights } = this.state;
|
|
626
|
+
|
|
627
|
+
const allRoutes = getAllRoutes(routes, state);
|
|
674
628
|
|
|
675
629
|
const focusedRoute = state.routes[state.index];
|
|
676
630
|
|
|
@@ -693,6 +647,8 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
693
647
|
scenes.findLast((scene) => scene.route.key === focusedRoute.key)
|
|
694
648
|
?.descriptor.options.headerMode === 'float';
|
|
695
649
|
|
|
650
|
+
const focusedHeaderHeight = headerHeights[focusedRoute.key];
|
|
651
|
+
|
|
696
652
|
return (
|
|
697
653
|
<View style={styles.container}>
|
|
698
654
|
{renderHeader({
|
|
@@ -700,6 +656,7 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
700
656
|
scenes,
|
|
701
657
|
getPreviousScene: this.getPreviousScene,
|
|
702
658
|
getFocusedRoute: this.getFocusedRoute,
|
|
659
|
+
contentHeight: headerHeights[focusedRoute.key],
|
|
703
660
|
onContentHeightChange: this.handleHeaderLayout,
|
|
704
661
|
style: [
|
|
705
662
|
isFloatHeaderAbsolute && [
|
|
@@ -707,39 +664,23 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
707
664
|
// The header title can't be found with maestro without this
|
|
708
665
|
// In some cases, header buttons may not be touchable as well
|
|
709
666
|
isFloatHeaderFocused && {
|
|
710
|
-
height:
|
|
667
|
+
height: focusedHeaderHeight?.value,
|
|
711
668
|
},
|
|
712
669
|
],
|
|
713
670
|
],
|
|
714
671
|
})}
|
|
715
|
-
<
|
|
716
|
-
|
|
717
|
-
style={styles.container}
|
|
718
|
-
onLayout={this.handleLayout}
|
|
719
|
-
>
|
|
720
|
-
{[...routes, ...state.preloadedRoutes].map((route, index) => {
|
|
672
|
+
<View style={styles.container} onLayout={this.handleLayout}>
|
|
673
|
+
{allRoutes.map((route, index, self) => {
|
|
721
674
|
const focused = focusedRoute.key === route.key;
|
|
722
675
|
const gesture = gestures[route.key];
|
|
723
676
|
const scene = scenes[index];
|
|
724
|
-
// It is possible that for a short period the route appears in both arrays.
|
|
725
|
-
// Particularly, if the screen is removed with `retain`, then it needs a moment to execute the animation.
|
|
726
|
-
// However, due to the router action, it immediately populates the `preloadedRoutes` array.
|
|
727
|
-
// Practically, the logic below takes care that it is rendered only once.
|
|
728
|
-
const isPreloaded =
|
|
729
|
-
state.preloadedRoutes.includes(route) && !routes.includes(route);
|
|
730
|
-
if (
|
|
731
|
-
state.preloadedRoutes.includes(route) &&
|
|
732
|
-
routes.includes(route) &&
|
|
733
|
-
index >= routes.length
|
|
734
|
-
) {
|
|
735
|
-
return null;
|
|
736
|
-
}
|
|
737
677
|
|
|
738
678
|
const {
|
|
679
|
+
inactiveBehavior = 'pause',
|
|
680
|
+
animation,
|
|
739
681
|
headerShown = true,
|
|
682
|
+
headerMode,
|
|
740
683
|
headerTransparent,
|
|
741
|
-
freezeOnBlur,
|
|
742
|
-
autoHideHomeIndicator,
|
|
743
684
|
} = scene.descriptor.options;
|
|
744
685
|
|
|
745
686
|
const safeAreaInsetTop = insets.top;
|
|
@@ -748,7 +689,9 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
748
689
|
const safeAreaInsetLeft = insets.left;
|
|
749
690
|
|
|
750
691
|
const headerHeight =
|
|
751
|
-
headerShown !== false
|
|
692
|
+
headerShown !== false
|
|
693
|
+
? headerHeights[route.key]
|
|
694
|
+
: HIDDEN_HEADER_HEIGHT;
|
|
752
695
|
|
|
753
696
|
// Start from current card and count backwards the number of cards with same interpolation
|
|
754
697
|
const interpolationIndex = getInterpolationIndex(scenes, index);
|
|
@@ -758,33 +701,109 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
758
701
|
isParentModal
|
|
759
702
|
);
|
|
760
703
|
|
|
704
|
+
const isAnimationEnabled =
|
|
705
|
+
getAnimationEnabled(animation) ||
|
|
706
|
+
// Also check next screen's animation,
|
|
707
|
+
// As it will result in both screens being visible
|
|
708
|
+
(scenes[index + 1]
|
|
709
|
+
? getAnimationEnabled(
|
|
710
|
+
scenes[index + 1].descriptor.options.animation
|
|
711
|
+
)
|
|
712
|
+
: false);
|
|
713
|
+
|
|
761
714
|
const isNextScreenTransparent =
|
|
762
715
|
scenes[index + 1]?.descriptor.options.presentation ===
|
|
763
716
|
'transparentModal';
|
|
764
717
|
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
718
|
+
const isRemoving =
|
|
719
|
+
replacingRouteKeys.includes(route.key) ||
|
|
720
|
+
closingRouteKeys.includes(route.key);
|
|
721
|
+
|
|
722
|
+
const isFocusing =
|
|
723
|
+
openingRouteKeys.includes(route.key) ||
|
|
724
|
+
[...closingRouteKeys, ...replacingRouteKeys].includes(
|
|
725
|
+
self[index + 1]?.key
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
const isInactive = isInactiveRoute(route, this.props.routes);
|
|
729
|
+
const isRetained = state.retainedRouteKeys?.includes(route.key);
|
|
730
|
+
const isPreloaded = isInactive && !isRetained;
|
|
731
|
+
|
|
732
|
+
const isTopmost = index === routes.length - 1;
|
|
733
|
+
const isBeforeLast = index === routes.length - 2;
|
|
734
|
+
|
|
735
|
+
// Keep animating and the last two rendered routes visible for smoother transitions
|
|
736
|
+
const isVisible =
|
|
737
|
+
focused ||
|
|
738
|
+
isNextScreenTransparent ||
|
|
739
|
+
// React native destroys the view when hidden
|
|
740
|
+
// So we keep retained screens visible
|
|
741
|
+
// This makes sure any videos being played don't stop etc.
|
|
742
|
+
isRetained ||
|
|
743
|
+
// We only need to keep other screens visible when animation is enabled
|
|
744
|
+
(isAnimationEnabled &&
|
|
745
|
+
(isFocusing ||
|
|
746
|
+
isRemoving ||
|
|
747
|
+
// Preloaded screens should stay mounted, but remain hidden until focused
|
|
748
|
+
(!isPreloaded && index >= routes.length - 2)));
|
|
749
|
+
|
|
750
|
+
const activityMode = // Render focused and animating screens normally
|
|
751
|
+
focused || isFocusing
|
|
752
|
+
? 'normal'
|
|
753
|
+
: inactiveBehavior === 'none' ||
|
|
754
|
+
// Unpause preloaded or retained screens so updates are visible
|
|
755
|
+
// Handle retained explicitly as isInactive won't be updated until animation end
|
|
756
|
+
isInactive ||
|
|
757
|
+
isRetained ||
|
|
758
|
+
// Keep the screen before transparent screen active
|
|
759
|
+
// This lets the screen under the transparent screen update and animate
|
|
760
|
+
isNextScreenTransparent ||
|
|
761
|
+
// Keep the screen before last screen active
|
|
762
|
+
// Otherwise it breaks animation when going back
|
|
763
|
+
isBeforeLast
|
|
764
|
+
? 'inert'
|
|
765
|
+
: inactiveBehavior === 'unmount' &&
|
|
766
|
+
// Don't unmount screens that needs to stay visible
|
|
767
|
+
!isVisible &&
|
|
768
|
+
// Don't unmount screens with nested navigators
|
|
769
|
+
// So we don't lose their state
|
|
770
|
+
!('state' in route && route.state)
|
|
771
|
+
? 'unmounted'
|
|
772
|
+
: 'paused';
|
|
773
|
+
|
|
774
|
+
if (activityMode === 'unmounted') {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
772
777
|
|
|
773
778
|
return (
|
|
774
|
-
<
|
|
779
|
+
<ActivityView
|
|
775
780
|
key={route.key}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
781
|
+
mode={activityMode}
|
|
782
|
+
visible={isVisible}
|
|
783
|
+
style={{
|
|
784
|
+
...StyleSheet.absoluteFill,
|
|
785
|
+
// Avoid unfocused larger pages increasing scroll area
|
|
786
|
+
// e.g. when a smaller screen is pushed over a larger one on web
|
|
787
|
+
overflow: isTopmost ? undefined : 'hidden',
|
|
788
|
+
marginTop:
|
|
789
|
+
headerTransparent || headerMode === 'screen'
|
|
790
|
+
? 0
|
|
791
|
+
: // Add offset for non-transparent floating header
|
|
792
|
+
isFloatHeaderAbsolute
|
|
793
|
+
? // If header is absolute, offset the card by the header height
|
|
794
|
+
headerHeight?.value
|
|
795
|
+
: // Otherwise, the in-flow float header already takes the
|
|
796
|
+
// focused header's space, so offset by the difference
|
|
797
|
+
headerHeight && focusedHeaderHeight
|
|
798
|
+
? headerHeight.value - focusedHeaderHeight.value
|
|
799
|
+
: 0,
|
|
800
|
+
}}
|
|
782
801
|
>
|
|
783
802
|
<CardContainer
|
|
784
803
|
index={index}
|
|
785
804
|
interpolationIndex={interpolationIndex}
|
|
786
805
|
modal={isModal}
|
|
787
|
-
active={
|
|
806
|
+
active={isTopmost}
|
|
788
807
|
focused={focused}
|
|
789
808
|
opening={openingRouteKeys.includes(route.key)}
|
|
790
809
|
closing={closingRouteKeys.includes(route.key)}
|
|
@@ -803,22 +822,19 @@ export class CardStack extends React.Component<Props, State> {
|
|
|
803
822
|
onHeaderHeightChange={this.handleHeaderLayout}
|
|
804
823
|
getPreviousScene={this.getPreviousScene}
|
|
805
824
|
getFocusedRoute={this.getFocusedRoute}
|
|
806
|
-
hasAbsoluteFloatHeader={
|
|
807
|
-
isFloatHeaderAbsolute && !headerTransparent
|
|
808
|
-
}
|
|
809
825
|
renderHeader={renderHeader}
|
|
810
826
|
onOpenRoute={onOpenRoute}
|
|
811
827
|
onCloseRoute={onCloseRoute}
|
|
812
828
|
onTransitionStart={onTransitionStart}
|
|
813
829
|
onTransitionEnd={onTransitionEnd}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
</
|
|
830
|
+
preloaded={isInactive}
|
|
831
|
+
>
|
|
832
|
+
{scene.descriptor.render()}
|
|
833
|
+
</CardContainer>
|
|
834
|
+
</ActivityView>
|
|
819
835
|
);
|
|
820
836
|
})}
|
|
821
|
-
</
|
|
837
|
+
</View>
|
|
822
838
|
</View>
|
|
823
839
|
);
|
|
824
840
|
}
|