UNPKG

8.82 kBTypeScriptView Raw
1import {
2 getHeaderTitle,
3 HeaderBackContext,
4 HeaderHeightContext,
5 HeaderShownContext,
6} from '@react-navigation/elements';
7import { Route, useTheme } from '@react-navigation/native';
8import * as React from 'react';
9import { Animated, StyleSheet, View } from 'react-native';
10
11import type { Layout, Scene } from '../../types';
12import ModalPresentationContext from '../../utils/ModalPresentationContext';
13import useKeyboardManager from '../../utils/useKeyboardManager';
14import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
15import Card from './Card';
16
17type 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
57const EPSILON = 0.1;
58
59function 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 // This is necessary to avoid unfocused larger pages increasing scroll area
264 // The issue can be seen on the web when a smaller screen is pushed over a larger one
265 overflow: active ? undefined : 'hidden',
266 display:
267 // Hide unfocused screens when animation isn't enabled
268 // This is also necessary for a11y on web
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
310export default React.memo(CardContainer);
311
312const styles = StyleSheet.create({
313 container: {
314 flex: 1,
315 flexDirection: 'column-reverse',
316 },
317 scene: {
318 flex: 1,
319 },
320});