1 | import {
|
2 | HeaderShownContext,
|
3 | SafeAreaProviderCompat,
|
4 | } from '@react-navigation/elements';
|
5 | import {
|
6 | ParamListBase,
|
7 | Route,
|
8 | StackActions,
|
9 | StackNavigationState,
|
10 | } from '@react-navigation/native';
|
11 | import * as React from 'react';
|
12 | import { StyleSheet, View } from 'react-native';
|
13 | import {
|
14 | EdgeInsets,
|
15 | SafeAreaInsetsContext,
|
16 | } from 'react-native-safe-area-context';
|
17 |
|
18 | import type {
|
19 | StackDescriptorMap,
|
20 | StackNavigationConfig,
|
21 | StackNavigationHelpers,
|
22 | } from '../../types';
|
23 | import ModalPresentationContext from '../../utils/ModalPresentationContext';
|
24 | import { GestureHandlerRootView } from '../GestureHandler';
|
25 | import HeaderContainer, {
|
26 | Props as HeaderContainerProps,
|
27 | } from '../Header/HeaderContainer';
|
28 | import CardStack from './CardStack';
|
29 |
|
30 | type Props = StackNavigationConfig & {
|
31 | state: StackNavigationState<ParamListBase>;
|
32 | navigation: StackNavigationHelpers;
|
33 | descriptors: StackDescriptorMap;
|
34 | };
|
35 |
|
36 | type State = {
|
37 |
|
38 | routes: Route<string>[];
|
39 |
|
40 | previousRoutes: Route<string>[];
|
41 |
|
42 | previousDescriptors: StackDescriptorMap;
|
43 |
|
44 | openingRouteKeys: string[];
|
45 |
|
46 | closingRouteKeys: string[];
|
47 |
|
48 | replacingRouteKeys: string[];
|
49 |
|
50 |
|
51 | descriptors: StackDescriptorMap;
|
52 | };
|
53 |
|
54 | const GestureHandlerWrapper = GestureHandlerRootView ?? View;
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 | const isArrayEqual = (a: any[], b: any[]) =>
|
61 | a.length === b.length && a.every((it, index) => it === b[index]);
|
62 |
|
63 | export default class StackView extends React.Component<Props, State> {
|
64 | static getDerivedStateFromProps(
|
65 | props: Readonly<Props>,
|
66 | state: Readonly<State>
|
67 | ) {
|
68 |
|
69 | if (
|
70 | (props.state.routes === state.previousRoutes ||
|
71 | isArrayEqual(
|
72 | props.state.routes.map((r) => r.key),
|
73 | state.previousRoutes.map((r) => r.key)
|
74 | )) &&
|
75 | state.routes.length
|
76 | ) {
|
77 | let routes = state.routes;
|
78 | let previousRoutes = state.previousRoutes;
|
79 | let descriptors = props.descriptors;
|
80 | let previousDescriptors = state.previousDescriptors;
|
81 |
|
82 | if (props.descriptors !== state.previousDescriptors) {
|
83 | descriptors = state.routes.reduce<StackDescriptorMap>((acc, route) => {
|
84 | acc[route.key] =
|
85 | props.descriptors[route.key] || state.descriptors[route.key];
|
86 |
|
87 | return acc;
|
88 | }, {});
|
89 |
|
90 | previousDescriptors = props.descriptors;
|
91 | }
|
92 |
|
93 | if (props.state.routes !== state.previousRoutes) {
|
94 | // if any route objects have changed, we should update them
|
95 | const map = props.state.routes.reduce<Record<string, Route<string>>>(
|
96 | (acc, route) => {
|
97 | acc[route.key] = route;
|
98 | return acc;
|
99 | },
|
100 | {}
|
101 | );
|
102 |
|
103 | routes = state.routes.map((route) => map[route.key] || route);
|
104 | previousRoutes = props.state.routes;
|
105 | }
|
106 |
|
107 | return {
|
108 | routes,
|
109 | previousRoutes,
|
110 | descriptors,
|
111 | previousDescriptors,
|
112 | };
|
113 | }
|
114 |
|
115 | // Here we determine which routes were added or removed to animate them
|
116 | // We keep a copy of the route being removed in local state to be able to animate it
|
117 |
|
118 | let routes =
|
119 | props.state.index < props.state.routes.length - 1
|
120 | ? // Remove any extra routes from the state
|
121 | // The last visible route should be the focused route, i.e. at current index
|
122 | props.state.routes.slice(0, props.state.index + 1)
|
123 | : props.state.routes;
|
124 |
|
125 | // Now we need to determine which routes were added and removed
|
126 | let {
|
127 | openingRouteKeys,
|
128 | closingRouteKeys,
|
129 | replacingRouteKeys,
|
130 | previousRoutes,
|
131 | } = state;
|
132 |
|
133 | const previousFocusedRoute = previousRoutes[previousRoutes.length - 1] as
|
134 | | Route<string>
|
135 | | undefined;
|
136 | const nextFocusedRoute = routes[routes.length - 1];
|
137 |
|
138 | const isAnimationEnabled = (key: string) => {
|
139 | const descriptor = props.descriptors[key] || state.descriptors[key];
|
140 |
|
141 | return descriptor ? descriptor.options.animationEnabled !== false : true;
|
142 | };
|
143 |
|
144 | const getAnimationTypeForReplace = (key: string) => {
|
145 | const descriptor = props.descriptors[key] || state.descriptors[key];
|
146 |
|
147 | return descriptor.options.animationTypeForReplace ?? 'push';
|
148 | };
|
149 |
|
150 | if (
|
151 | previousFocusedRoute &&
|
152 | previousFocusedRoute.key !== nextFocusedRoute.key
|
153 | ) {
|
154 |
|
155 |
|
156 |
|
157 | if (!previousRoutes.some((r) => r.key === nextFocusedRoute.key)) {
|
158 |
|
159 |
|
160 |
|
161 | if (
|
162 | isAnimationEnabled(nextFocusedRoute.key) &&
|
163 | !openingRouteKeys.includes(nextFocusedRoute.key)
|
164 | ) {
|
165 |
|
166 |
|
167 | openingRouteKeys = [...openingRouteKeys, nextFocusedRoute.key];
|
168 |
|
169 | closingRouteKeys = closingRouteKeys.filter(
|
170 | (key) => key !== nextFocusedRoute.key
|
171 | );
|
172 | replacingRouteKeys = replacingRouteKeys.filter(
|
173 | (key) => key !== nextFocusedRoute.key
|
174 | );
|
175 |
|
176 | if (!routes.some((r) => r.key === previousFocusedRoute.key)) {
|
177 |
|
178 |
|
179 | openingRouteKeys = openingRouteKeys.filter(
|
180 | (key) => key !== previousFocusedRoute.key
|
181 | );
|
182 |
|
183 | if (getAnimationTypeForReplace(nextFocusedRoute.key) === 'pop') {
|
184 | closingRouteKeys = [
|
185 | ...closingRouteKeys,
|
186 | previousFocusedRoute.key,
|
187 | ];
|
188 |
|
189 |
|
190 |
|
191 |
|
192 | openingRouteKeys = openingRouteKeys.filter(
|
193 | (key) => key !== nextFocusedRoute.key
|
194 | );
|
195 |
|
196 |
|
197 | routes = [...routes, previousFocusedRoute];
|
198 | } else {
|
199 | replacingRouteKeys = [
|
200 | ...replacingRouteKeys,
|
201 | previousFocusedRoute.key,
|
202 | ];
|
203 |
|
204 | closingRouteKeys = closingRouteKeys.filter(
|
205 | (key) => key !== previousFocusedRoute.key
|
206 | );
|
207 |
|
208 |
|
209 |
|
210 |
|
211 | routes = routes.slice();
|
212 | routes.splice(routes.length - 1, 0, previousFocusedRoute);
|
213 | }
|
214 | }
|
215 | }
|
216 | } else if (!routes.some((r) => r.key === previousFocusedRoute.key)) {
|
217 |
|
218 |
|
219 | if (
|
220 | isAnimationEnabled(previousFocusedRoute.key) &&
|
221 | !closingRouteKeys.includes(previousFocusedRoute.key)
|
222 | ) {
|
223 | closingRouteKeys = [...closingRouteKeys, previousFocusedRoute.key];
|
224 |
|
225 |
|
226 |
|
227 | openingRouteKeys = openingRouteKeys.filter(
|
228 | (key) => key !== previousFocusedRoute.key
|
229 | );
|
230 | replacingRouteKeys = replacingRouteKeys.filter(
|
231 | (key) => key !== previousFocusedRoute.key
|
232 | );
|
233 |
|
234 |
|
235 | routes = [...routes, previousFocusedRoute];
|
236 | }
|
237 | } else {
|
238 |
|
239 |
|
240 |
|
241 | }
|
242 | } else if (replacingRouteKeys.length || closingRouteKeys.length) {
|
243 |
|
244 | routes = routes.slice();
|
245 | routes.splice(
|
246 | routes.length - 1,
|
247 | 0,
|
248 | ...state.routes.filter(({ key }) =>
|
249 | isAnimationEnabled(key)
|
250 | ? replacingRouteKeys.includes(key) || closingRouteKeys.includes(key)
|
251 | : false
|
252 | )
|
253 | );
|
254 | }
|
255 |
|
256 | if (!routes.length) {
|
257 | throw new Error(
|
258 | 'There should always be at least one route in the navigation state.'
|
259 | );
|
260 | }
|
261 |
|
262 | const descriptors = routes.reduce<StackDescriptorMap>((acc, route) => {
|
263 | acc[route.key] =
|
264 | props.descriptors[route.key] || state.descriptors[route.key];
|
265 |
|
266 | return acc;
|
267 | }, {});
|
268 |
|
269 | return {
|
270 | routes,
|
271 | previousRoutes: props.state.routes,
|
272 | previousDescriptors: props.descriptors,
|
273 | openingRouteKeys,
|
274 | closingRouteKeys,
|
275 | replacingRouteKeys,
|
276 | descriptors,
|
277 | };
|
278 | }
|
279 |
|
280 | state: State = {
|
281 | routes: [],
|
282 | previousRoutes: [],
|
283 | previousDescriptors: {},
|
284 | openingRouteKeys: [],
|
285 | closingRouteKeys: [],
|
286 | replacingRouteKeys: [],
|
287 | descriptors: {},
|
288 | };
|
289 |
|
290 | private getPreviousRoute = ({ route }: { route: Route<string> }) => {
|
291 | const { closingRouteKeys, replacingRouteKeys } = this.state;
|
292 | const routes = this.state.routes.filter(
|
293 | (r) =>
|
294 | r.key === route.key ||
|
295 | (!closingRouteKeys.includes(r.key) &&
|
296 | !replacingRouteKeys.includes(r.key))
|
297 | );
|
298 |
|
299 | const index = routes.findIndex((r) => r.key === route.key);
|
300 |
|
301 | return routes[index - 1];
|
302 | };
|
303 |
|
304 | private renderScene = ({ route }: { route: Route<string> }) => {
|
305 | const descriptor =
|
306 | this.state.descriptors[route.key] || this.props.descriptors[route.key];
|
307 |
|
308 | if (!descriptor) {
|
309 | return null;
|
310 | }
|
311 |
|
312 | return descriptor.render();
|
313 | };
|
314 |
|
315 | private renderHeader = (props: HeaderContainerProps) => {
|
316 | return <HeaderContainer {...props} />;
|
317 | };
|
318 |
|
319 | private handleOpenRoute = ({ route }: { route: Route<string> }) => {
|
320 | const { state, navigation } = this.props;
|
321 | const { closingRouteKeys, replacingRouteKeys } = this.state;
|
322 |
|
323 | if (
|
324 | closingRouteKeys.some((key) => key === route.key) &&
|
325 | replacingRouteKeys.every((key) => key !== route.key) &&
|
326 | state.routeNames.includes(route.name) &&
|
327 | !state.routes.some((r) => r.key === route.key)
|
328 | ) {
|
329 |
|
330 |
|
331 | navigation.navigate(route);
|
332 | } else {
|
333 | this.setState((state) => ({
|
334 | routes: state.replacingRouteKeys.length
|
335 | ? state.routes.filter(
|
336 | (r) => !state.replacingRouteKeys.includes(r.key)
|
337 | )
|
338 | : state.routes,
|
339 | openingRouteKeys: state.openingRouteKeys.filter(
|
340 | (key) => key !== route.key
|
341 | ),
|
342 | closingRouteKeys: state.closingRouteKeys.filter(
|
343 | (key) => key !== route.key
|
344 | ),
|
345 | replacingRouteKeys: [],
|
346 | }));
|
347 | }
|
348 | };
|
349 |
|
350 | private handleCloseRoute = ({ route }: { route: Route<string> }) => {
|
351 | const { state, navigation } = this.props;
|
352 |
|
353 | if (state.routes.some((r) => r.key === route.key)) {
|
354 |
|
355 |
|
356 |
|
357 | navigation.dispatch({
|
358 | ...StackActions.pop(),
|
359 | source: route.key,
|
360 | target: state.key,
|
361 | });
|
362 | } else {
|
363 |
|
364 | this.setState((state) => ({
|
365 | routes: state.routes.filter((r) => r.key !== route.key),
|
366 | openingRouteKeys: state.openingRouteKeys.filter(
|
367 | (key) => key !== route.key
|
368 | ),
|
369 | closingRouteKeys: state.closingRouteKeys.filter(
|
370 | (key) => key !== route.key
|
371 | ),
|
372 | }));
|
373 | }
|
374 | };
|
375 |
|
376 | private handleTransitionStart = (
|
377 | { route }: { route: Route<string> },
|
378 | closing: boolean
|
379 | ) =>
|
380 | this.props.navigation.emit({
|
381 | type: 'transitionStart',
|
382 | data: { closing },
|
383 | target: route.key,
|
384 | });
|
385 |
|
386 | private handleTransitionEnd = (
|
387 | { route }: { route: Route<string> },
|
388 | closing: boolean
|
389 | ) =>
|
390 | this.props.navigation.emit({
|
391 | type: 'transitionEnd',
|
392 | data: { closing },
|
393 | target: route.key,
|
394 | });
|
395 |
|
396 | private handleGestureStart = ({ route }: { route: Route<string> }) => {
|
397 | this.props.navigation.emit({
|
398 | type: 'gestureStart',
|
399 | target: route.key,
|
400 | });
|
401 | };
|
402 |
|
403 | private handleGestureEnd = ({ route }: { route: Route<string> }) => {
|
404 | this.props.navigation.emit({
|
405 | type: 'gestureEnd',
|
406 | target: route.key,
|
407 | });
|
408 | };
|
409 |
|
410 | private handleGestureCancel = ({ route }: { route: Route<string> }) => {
|
411 | this.props.navigation.emit({
|
412 | type: 'gestureCancel',
|
413 | target: route.key,
|
414 | });
|
415 | };
|
416 |
|
417 | render() {
|
418 | const {
|
419 | state,
|
420 |
|
421 | descriptors: _,
|
422 | ...rest
|
423 | } = this.props;
|
424 |
|
425 | const { routes, descriptors, openingRouteKeys, closingRouteKeys } =
|
426 | this.state;
|
427 |
|
428 | return (
|
429 | <GestureHandlerWrapper style={styles.container}>
|
430 | <SafeAreaProviderCompat>
|
431 | <SafeAreaInsetsContext.Consumer>
|
432 | {(insets) => (
|
433 | <ModalPresentationContext.Consumer>
|
434 | {(isParentModal) => (
|
435 | <HeaderShownContext.Consumer>
|
436 | {(isParentHeaderShown) => (
|
437 | <CardStack
|
438 | insets={insets as EdgeInsets}
|
439 | isParentHeaderShown={isParentHeaderShown}
|
440 | isParentModal={isParentModal}
|
441 | getPreviousRoute={this.getPreviousRoute}
|
442 | routes={routes}
|
443 | openingRouteKeys={openingRouteKeys}
|
444 | closingRouteKeys={closingRouteKeys}
|
445 | onOpenRoute={this.handleOpenRoute}
|
446 | onCloseRoute={this.handleCloseRoute}
|
447 | onTransitionStart={this.handleTransitionStart}
|
448 | onTransitionEnd={this.handleTransitionEnd}
|
449 | renderHeader={this.renderHeader}
|
450 | renderScene={this.renderScene}
|
451 | state={state}
|
452 | descriptors={descriptors}
|
453 | onGestureStart={this.handleGestureStart}
|
454 | onGestureEnd={this.handleGestureEnd}
|
455 | onGestureCancel={this.handleGestureCancel}
|
456 | {...rest}
|
457 | />
|
458 | )}
|
459 | </HeaderShownContext.Consumer>
|
460 | )}
|
461 | </ModalPresentationContext.Consumer>
|
462 | )}
|
463 | </SafeAreaInsetsContext.Consumer>
|
464 | </SafeAreaProviderCompat>
|
465 | </GestureHandlerWrapper>
|
466 | );
|
467 | }
|
468 | }
|
469 |
|
470 | const styles = StyleSheet.create({
|
471 | container: {
|
472 | flex: 1,
|
473 | },
|
474 | });
|