1 | import {
|
2 | getHeaderTitle,
|
3 | HeaderBackContext,
|
4 | HeaderHeightContext,
|
5 | HeaderShownContext,
|
6 | } from '@react-navigation/elements';
|
7 | import { Route, useTheme } from '@react-navigation/native';
|
8 | import * as React from 'react';
|
9 | import { Animated, StyleSheet, View } from 'react-native';
|
10 |
|
11 | import type { Layout, Scene } from '../../types';
|
12 | import ModalPresentationContext from '../../utils/ModalPresentationContext';
|
13 | import useKeyboardManager from '../../utils/useKeyboardManager';
|
14 | import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
|
15 | import Card from './Card';
|
16 |
|
17 | type Props = {
|
18 | interpolationIndex: number;
|
19 | index: number;
|
20 | active: boolean;
|
21 | focused: boolean;
|
22 | closing: boolean;
|
23 | modal: boolean;
|
24 | layout: Layout;
|
25 | gesture: Animated.Value;
|
26 | scene: Scene;
|
27 | headerDarkContent: boolean | undefined;
|
28 | safeAreaInsetTop: number;
|
29 | safeAreaInsetRight: number;
|
30 | safeAreaInsetBottom: number;
|
31 | safeAreaInsetLeft: number;
|
32 | getPreviousScene: (props: { route: Route<string> }) => Scene | undefined;
|
33 | getFocusedRoute: () => Route<string>;
|
34 | renderHeader: (props: HeaderContainerProps) => React.ReactNode;
|
35 | renderScene: (props: { route: Route<string> }) => React.ReactNode;
|
36 | onOpenRoute: (props: { route: Route<string> }) => void;
|
37 | onCloseRoute: (props: { route: Route<string> }) => void;
|
38 | onTransitionStart: (
|
39 | props: { route: Route<string> },
|
40 | closing: boolean
|
41 | ) => void;
|
42 | onTransitionEnd: (props: { route: Route<string> }, closing: boolean) => void;
|
43 | onGestureStart: (props: { route: Route<string> }) => void;
|
44 | onGestureEnd: (props: { route: Route<string> }) => void;
|
45 | onGestureCancel: (props: { route: Route<string> }) => void;
|
46 | hasAbsoluteFloatHeader: boolean;
|
47 | headerHeight: number;
|
48 | onHeaderHeightChange: (props: {
|
49 | route: Route<string>;
|
50 | height: number;
|
51 | }) => void;
|
52 | isParentHeaderShown: boolean;
|
53 | isNextScreenTransparent: boolean;
|
54 | detachCurrentScreen: boolean;
|
55 | };
|
56 |
|
57 | const EPSILON = 0.1;
|
58 |
|
59 | function CardContainer({
|
60 | interpolationIndex,
|
61 | index,
|
62 | active,
|
63 | closing,
|
64 | gesture,
|
65 | focused,
|
66 | modal,
|
67 | getPreviousScene,
|
68 | getFocusedRoute,
|
69 | headerDarkContent,
|
70 | hasAbsoluteFloatHeader,
|
71 | headerHeight,
|
72 | onHeaderHeightChange,
|
73 | isParentHeaderShown,
|
74 | isNextScreenTransparent,
|
75 | detachCurrentScreen,
|
76 | layout,
|
77 | onCloseRoute,
|
78 | onOpenRoute,
|
79 | onGestureCancel,
|
80 | onGestureEnd,
|
81 | onGestureStart,
|
82 | onTransitionEnd,
|
83 | onTransitionStart,
|
84 | renderHeader,
|
85 | renderScene,
|
86 | safeAreaInsetBottom,
|
87 | safeAreaInsetLeft,
|
88 | safeAreaInsetRight,
|
89 | safeAreaInsetTop,
|
90 | scene,
|
91 | }: Props) {
|
92 | const parentHeaderHeight = React.useContext(HeaderHeightContext);
|
93 |
|
94 | const { onPageChangeStart, onPageChangeCancel, onPageChangeConfirm } =
|
95 | useKeyboardManager(
|
96 | React.useCallback(() => {
|
97 | const { options, navigation } = scene.descriptor;
|
98 |
|
99 | return (
|
100 | navigation.isFocused() && options.keyboardHandlingEnabled !== false
|
101 | );
|
102 | }, [scene.descriptor])
|
103 | );
|
104 |
|
105 | const handleOpen = () => {
|
106 | const { route } = scene.descriptor;
|
107 |
|
108 | onTransitionEnd({ route }, false);
|
109 | onOpenRoute({ route });
|
110 | };
|
111 |
|
112 | const handleClose = () => {
|
113 | const { route } = scene.descriptor;
|
114 |
|
115 | onTransitionEnd({ route }, true);
|
116 | onCloseRoute({ route });
|
117 | };
|
118 |
|
119 | const handleGestureBegin = () => {
|
120 | const { route } = scene.descriptor;
|
121 |
|
122 | onPageChangeStart();
|
123 | onGestureStart({ route });
|
124 | };
|
125 |
|
126 | const handleGestureCanceled = () => {
|
127 | const { route } = scene.descriptor;
|
128 |
|
129 | onPageChangeCancel();
|
130 | onGestureCancel({ route });
|
131 | };
|
132 |
|
133 | const handleGestureEnd = () => {
|
134 | const { route } = scene.descriptor;
|
135 |
|
136 | onGestureEnd({ route });
|
137 | };
|
138 |
|
139 | const handleTransition = ({
|
140 | closing,
|
141 | gesture,
|
142 | }: {
|
143 | closing: boolean;
|
144 | gesture: boolean;
|
145 | }) => {
|
146 | const { route } = scene.descriptor;
|
147 |
|
148 | if (!gesture) {
|
149 | onPageChangeConfirm?.(true);
|
150 | } else if (active && closing) {
|
151 | onPageChangeConfirm?.(false);
|
152 | } else {
|
153 | onPageChangeCancel?.();
|
154 | }
|
155 |
|
156 | onTransitionStart?.({ route }, closing);
|
157 | };
|
158 |
|
159 | const insets = {
|
160 | top: safeAreaInsetTop,
|
161 | right: safeAreaInsetRight,
|
162 | bottom: safeAreaInsetBottom,
|
163 | left: safeAreaInsetLeft,
|
164 | };
|
165 |
|
166 | const { colors } = useTheme();
|
167 |
|
168 | const [pointerEvents, setPointerEvents] = React.useState<'box-none' | 'none'>(
|
169 | 'box-none'
|
170 | );
|
171 |
|
172 | React.useEffect(() => {
|
173 | const listener = scene.progress.next?.addListener?.(
|
174 | ({ value }: { value: number }) => {
|
175 | setPointerEvents(value <= EPSILON ? 'box-none' : 'none');
|
176 | }
|
177 | );
|
178 |
|
179 | return () => {
|
180 | if (listener) {
|
181 | scene.progress.next?.removeListener?.(listener);
|
182 | }
|
183 | };
|
184 | }, [pointerEvents, scene.progress.next]);
|
185 |
|
186 | const {
|
187 | presentation,
|
188 | animationEnabled,
|
189 | cardOverlay,
|
190 | cardOverlayEnabled,
|
191 | cardShadowEnabled,
|
192 | cardStyle,
|
193 | cardStyleInterpolator,
|
194 | gestureDirection,
|
195 | gestureEnabled,
|
196 | gestureResponseDistance,
|
197 | gestureVelocityImpact,
|
198 | headerMode,
|
199 | headerShown,
|
200 | transitionSpec,
|
201 | } = scene.descriptor.options;
|
202 |
|
203 | const previousScene = getPreviousScene({ route: scene.descriptor.route });
|
204 |
|
205 | let backTitle: string | undefined;
|
206 |
|
207 | if (previousScene) {
|
208 | const { options, route } = previousScene.descriptor;
|
209 |
|
210 | backTitle = getHeaderTitle(options, route.name);
|
211 | }
|
212 |
|
213 | const headerBack = React.useMemo(
|
214 | () => (backTitle !== undefined ? { title: backTitle } : undefined),
|
215 | [backTitle]
|
216 | );
|
217 |
|
218 | return (
|
219 | <Card
|
220 | interpolationIndex={interpolationIndex}
|
221 | gestureDirection={gestureDirection}
|
222 | layout={layout}
|
223 | insets={insets}
|
224 | gesture={gesture}
|
225 | current={scene.progress.current}
|
226 | next={scene.progress.next}
|
227 | closing={closing}
|
228 | onOpen={handleOpen}
|
229 | onClose={handleClose}
|
230 | overlay={cardOverlay}
|
231 | overlayEnabled={cardOverlayEnabled}
|
232 | shadowEnabled={cardShadowEnabled}
|
233 | onTransition={handleTransition}
|
234 | onGestureBegin={handleGestureBegin}
|
235 | onGestureCanceled={handleGestureCanceled}
|
236 | onGestureEnd={handleGestureEnd}
|
237 | gestureEnabled={index === 0 ? false : gestureEnabled}
|
238 | gestureResponseDistance={gestureResponseDistance}
|
239 | gestureVelocityImpact={gestureVelocityImpact}
|
240 | transitionSpec={transitionSpec}
|
241 | styleInterpolator={cardStyleInterpolator}
|
242 | accessibilityElementsHidden={!focused}
|
243 | importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
|
244 | pointerEvents={active ? 'box-none' : pointerEvents}
|
245 | pageOverflowEnabled={headerMode !== 'float' && presentation !== 'modal'}
|
246 | headerDarkContent={headerDarkContent}
|
247 | containerStyle={
|
248 | hasAbsoluteFloatHeader && headerMode !== 'screen'
|
249 | ? { marginTop: headerHeight }
|
250 | : null
|
251 | }
|
252 | contentStyle={[
|
253 | {
|
254 | backgroundColor:
|
255 | presentation === 'transparentModal'
|
256 | ? 'transparent'
|
257 | : colors.background,
|
258 | },
|
259 | cardStyle,
|
260 | ]}
|
261 | style={[
|
262 | {
|
263 |
|
264 |
|
265 | overflow: active ? undefined : 'hidden',
|
266 | display:
|
267 |
|
268 |
|
269 | animationEnabled === false &&
|
270 | isNextScreenTransparent === false &&
|
271 | detachCurrentScreen !== false &&
|
272 | !focused
|
273 | ? 'none'
|
274 | : 'flex',
|
275 | },
|
276 | StyleSheet.absoluteFill,
|
277 | ]}
|
278 | >
|
279 | <View style={styles.container}>
|
280 | <ModalPresentationContext.Provider value={modal}>
|
281 | <View style={styles.scene}>
|
282 | <HeaderBackContext.Provider value={headerBack}>
|
283 | <HeaderShownContext.Provider
|
284 | value={isParentHeaderShown || headerShown !== false}
|
285 | >
|
286 | <HeaderHeightContext.Provider
|
287 | value={headerShown ? headerHeight : parentHeaderHeight ?? 0}
|
288 | >
|
289 | {renderScene({ route: scene.descriptor.route })}
|
290 | </HeaderHeightContext.Provider>
|
291 | </HeaderShownContext.Provider>
|
292 | </HeaderBackContext.Provider>
|
293 | </View>
|
294 | {headerMode !== 'float'
|
295 | ? renderHeader({
|
296 | mode: 'screen',
|
297 | layout,
|
298 | scenes: [previousScene, scene],
|
299 | getPreviousScene,
|
300 | getFocusedRoute,
|
301 | onContentHeightChange: onHeaderHeightChange,
|
302 | })
|
303 | : null}
|
304 | </ModalPresentationContext.Provider>
|
305 | </View>
|
306 | </Card>
|
307 | );
|
308 | }
|
309 |
|
310 | export default React.memo(CardContainer);
|
311 |
|
312 | const styles = StyleSheet.create({
|
313 | container: {
|
314 | flex: 1,
|
315 | flexDirection: 'column-reverse',
|
316 | },
|
317 | scene: {
|
318 | flex: 1,
|
319 | },
|
320 | });
|