@react-navigation/native-stack 6.2.0
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/LICENSE +21 -0
- package/README.md +5 -0
- package/lib/commonjs/index.js +16 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/navigators/createNativeStackNavigator.js +67 -0
- package/lib/commonjs/navigators/createNativeStackNavigator.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/views/DebugContainer.js +24 -0
- package/lib/commonjs/views/DebugContainer.js.map +1 -0
- package/lib/commonjs/views/DebugContainer.native.js +43 -0
- package/lib/commonjs/views/DebugContainer.native.js.map +1 -0
- package/lib/commonjs/views/FontProcessor.js +11 -0
- package/lib/commonjs/views/FontProcessor.js.map +1 -0
- package/lib/commonjs/views/FontProcessor.native.js +25 -0
- package/lib/commonjs/views/FontProcessor.native.js.map +1 -0
- package/lib/commonjs/views/HeaderConfig.js +157 -0
- package/lib/commonjs/views/HeaderConfig.js.map +1 -0
- package/lib/commonjs/views/NativeStackView.js +133 -0
- package/lib/commonjs/views/NativeStackView.js.map +1 -0
- package/lib/commonjs/views/NativeStackView.native.js +247 -0
- package/lib/commonjs/views/NativeStackView.native.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/navigators/createNativeStackNavigator.js +50 -0
- package/lib/module/navigators/createNativeStackNavigator.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/views/DebugContainer.js +11 -0
- package/lib/module/views/DebugContainer.js.map +1 -0
- package/lib/module/views/DebugContainer.native.js +26 -0
- package/lib/module/views/DebugContainer.native.js.map +1 -0
- package/lib/module/views/FontProcessor.js +4 -0
- package/lib/module/views/FontProcessor.js.map +1 -0
- package/lib/module/views/FontProcessor.native.js +15 -0
- package/lib/module/views/FontProcessor.native.js.map +1 -0
- package/lib/module/views/HeaderConfig.js +138 -0
- package/lib/module/views/HeaderConfig.js.map +1 -0
- package/lib/module/views/NativeStackView.js +118 -0
- package/lib/module/views/NativeStackView.js.map +1 -0
- package/lib/module/views/NativeStackView.native.js +224 -0
- package/lib/module/views/NativeStackView.native.js.map +1 -0
- package/lib/typescript/src/index.d.ts +8 -0
- package/lib/typescript/src/navigators/createNativeStackNavigator.d.ts +6 -0
- package/lib/typescript/src/types.d.ts +373 -0
- package/lib/typescript/src/views/DebugContainer.d.ts +9 -0
- package/lib/typescript/src/views/DebugContainer.native.d.ts +9 -0
- package/lib/typescript/src/views/FontProcessor.d.ts +1 -0
- package/lib/typescript/src/views/FontProcessor.native.d.ts +1 -0
- package/lib/typescript/src/views/HeaderConfig.d.ts +9 -0
- package/lib/typescript/src/views/NativeStackView.d.ts +10 -0
- package/lib/typescript/src/views/NativeStackView.native.d.ts +10 -0
- package/package.json +80 -0
- package/src/index.tsx +14 -0
- package/src/navigators/createNativeStackNavigator.tsx +81 -0
- package/src/types.tsx +425 -0
- package/src/views/DebugContainer.native.tsx +33 -0
- package/src/views/DebugContainer.tsx +14 -0
- package/src/views/FontProcessor.native.tsx +13 -0
- package/src/views/FontProcessor.tsx +5 -0
- package/src/views/HeaderConfig.tsx +234 -0
- package/src/views/NativeStackView.native.tsx +331 -0
- package/src/views/NativeStackView.tsx +173 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { HeaderTitle } from '@react-navigation/elements';
|
|
2
|
+
import { Route, useTheme } from '@react-navigation/native';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
I18nManager,
|
|
6
|
+
Platform,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
TextStyle,
|
|
9
|
+
View,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
12
|
+
import {
|
|
13
|
+
ScreenStackHeaderBackButtonImage,
|
|
14
|
+
ScreenStackHeaderCenterView,
|
|
15
|
+
ScreenStackHeaderConfig,
|
|
16
|
+
ScreenStackHeaderLeftView,
|
|
17
|
+
ScreenStackHeaderRightView,
|
|
18
|
+
ScreenStackHeaderSearchBarView,
|
|
19
|
+
SearchBar,
|
|
20
|
+
} from 'react-native-screens';
|
|
21
|
+
|
|
22
|
+
import type { NativeStackNavigationOptions } from '../types';
|
|
23
|
+
import { processFonts } from './FontProcessor';
|
|
24
|
+
|
|
25
|
+
type Props = NativeStackNavigationOptions & {
|
|
26
|
+
route: Route<string>;
|
|
27
|
+
canGoBack: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default function HeaderConfig({
|
|
31
|
+
headerBackImageSource,
|
|
32
|
+
headerBackButtonMenuEnabled,
|
|
33
|
+
headerBackTitle,
|
|
34
|
+
headerBackTitleStyle,
|
|
35
|
+
headerBackTitleVisible = true,
|
|
36
|
+
headerBackVisible,
|
|
37
|
+
headerShadowVisible,
|
|
38
|
+
headerLargeStyle,
|
|
39
|
+
headerLargeTitle,
|
|
40
|
+
headerLargeTitleShadowVisible,
|
|
41
|
+
headerLargeTitleStyle,
|
|
42
|
+
headerLeft,
|
|
43
|
+
headerRight,
|
|
44
|
+
headerShown,
|
|
45
|
+
headerStyle,
|
|
46
|
+
headerBlurEffect,
|
|
47
|
+
headerTintColor,
|
|
48
|
+
headerTitle,
|
|
49
|
+
headerTitleAlign,
|
|
50
|
+
headerTitleStyle,
|
|
51
|
+
headerTransparent,
|
|
52
|
+
headerSearchBarOptions,
|
|
53
|
+
route,
|
|
54
|
+
title,
|
|
55
|
+
canGoBack,
|
|
56
|
+
}: Props): JSX.Element {
|
|
57
|
+
const insets = useSafeAreaInsets();
|
|
58
|
+
const { colors } = useTheme();
|
|
59
|
+
const tintColor =
|
|
60
|
+
headerTintColor ?? Platform.OS === 'ios' ? colors.primary : colors.text;
|
|
61
|
+
|
|
62
|
+
const headerBackTitleStyleFlattened =
|
|
63
|
+
StyleSheet.flatten(headerBackTitleStyle) || {};
|
|
64
|
+
const headerLargeTitleStyleFlattened =
|
|
65
|
+
StyleSheet.flatten(headerLargeTitleStyle) || {};
|
|
66
|
+
const headerTitleStyleFlattened = StyleSheet.flatten(headerTitleStyle) || {};
|
|
67
|
+
const headerStyleFlattened = StyleSheet.flatten(headerStyle) || {};
|
|
68
|
+
const headerLargeStyleFlattened = StyleSheet.flatten(headerLargeStyle) || {};
|
|
69
|
+
|
|
70
|
+
const [backTitleFontFamily, largeTitleFontFamily, titleFontFamily] =
|
|
71
|
+
processFonts([
|
|
72
|
+
headerBackTitleStyleFlattened.fontFamily,
|
|
73
|
+
headerLargeTitleStyleFlattened.fontFamily,
|
|
74
|
+
headerTitleStyleFlattened.fontFamily,
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const titleText = title !== undefined ? title : route.name;
|
|
78
|
+
const titleColor =
|
|
79
|
+
headerTitleStyleFlattened.color ?? headerTintColor ?? colors.text;
|
|
80
|
+
const titleFontSize = headerTitleStyleFlattened.fontSize;
|
|
81
|
+
const titleFontWeight = headerTitleStyleFlattened.fontWeight;
|
|
82
|
+
|
|
83
|
+
const headerTitleStyleSupported: TextStyle = { color: titleColor };
|
|
84
|
+
|
|
85
|
+
if (headerTitleStyleFlattened.fontFamily != null) {
|
|
86
|
+
headerTitleStyleSupported.fontFamily = headerTitleStyleFlattened.fontFamily;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (titleFontSize != null) {
|
|
90
|
+
headerTitleStyleSupported.fontSize = titleFontSize;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (titleFontWeight != null) {
|
|
94
|
+
headerTitleStyleSupported.fontWeight = titleFontWeight;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const headerLeftElement = headerLeft?.({
|
|
98
|
+
tintColor,
|
|
99
|
+
label: headerBackTitle,
|
|
100
|
+
canGoBack,
|
|
101
|
+
});
|
|
102
|
+
const headerRightElement = headerRight?.({ tintColor });
|
|
103
|
+
const headerTitleElement =
|
|
104
|
+
typeof headerTitle === 'function'
|
|
105
|
+
? headerTitle({ tintColor, children: titleText })
|
|
106
|
+
: null;
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
Platform.OS === 'ios' &&
|
|
110
|
+
headerSearchBarOptions != null &&
|
|
111
|
+
SearchBar == null
|
|
112
|
+
) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`The current version of 'react-native-screens' doesn't support SearchBar in the header. Please update to the latest version to use this option.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* We need to set this in if:
|
|
120
|
+
* - Back button should stay visible when `headerLeft` is specified
|
|
121
|
+
* - If `headerTitle` for Android is specified, so we only need to remove the title and keep the back button
|
|
122
|
+
*/
|
|
123
|
+
const backButtonInCustomView = headerBackVisible
|
|
124
|
+
? headerLeftElement != null
|
|
125
|
+
: Platform.OS === 'android' && headerTitleElement != null;
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<ScreenStackHeaderConfig
|
|
129
|
+
backButtonInCustomView={backButtonInCustomView}
|
|
130
|
+
backgroundColor={
|
|
131
|
+
headerStyleFlattened.backgroundColor ??
|
|
132
|
+
(headerTransparent ? 'transparent' : colors.card)
|
|
133
|
+
}
|
|
134
|
+
backTitle={headerBackTitleVisible ? headerBackTitle : ' '}
|
|
135
|
+
backTitleFontFamily={backTitleFontFamily}
|
|
136
|
+
backTitleFontSize={headerBackTitleStyleFlattened.fontSize}
|
|
137
|
+
blurEffect={headerBlurEffect}
|
|
138
|
+
color={tintColor}
|
|
139
|
+
direction={I18nManager.isRTL ? 'rtl' : 'ltr'}
|
|
140
|
+
disableBackButtonMenu={headerBackButtonMenuEnabled === false}
|
|
141
|
+
hidden={headerShown === false}
|
|
142
|
+
hideBackButton={headerBackVisible === false}
|
|
143
|
+
hideShadow={headerShadowVisible === false}
|
|
144
|
+
largeTitle={headerLargeTitle}
|
|
145
|
+
largeTitleBackgroundColor={headerLargeStyleFlattened.backgroundColor}
|
|
146
|
+
largeTitleColor={headerLargeTitleStyleFlattened.color}
|
|
147
|
+
largeTitleFontFamily={largeTitleFontFamily}
|
|
148
|
+
largeTitleFontSize={headerLargeTitleStyleFlattened.fontSize}
|
|
149
|
+
largeTitleFontWeight={headerLargeTitleStyleFlattened.fontWeight}
|
|
150
|
+
largeTitleHideShadow={headerLargeTitleShadowVisible === false}
|
|
151
|
+
title={typeof headerTitle === 'string' ? headerTitle : titleText}
|
|
152
|
+
titleColor={titleColor}
|
|
153
|
+
titleFontFamily={titleFontFamily}
|
|
154
|
+
titleFontSize={titleFontSize}
|
|
155
|
+
titleFontWeight={titleFontWeight}
|
|
156
|
+
topInsetEnabled={insets.top !== 0}
|
|
157
|
+
translucent={
|
|
158
|
+
// This defaults to `true`, so we can't pass `undefined`
|
|
159
|
+
headerTransparent === true
|
|
160
|
+
}
|
|
161
|
+
>
|
|
162
|
+
{Platform.OS === 'ios' ? (
|
|
163
|
+
<>
|
|
164
|
+
{headerLeftElement != null ? (
|
|
165
|
+
<ScreenStackHeaderLeftView>
|
|
166
|
+
{headerLeftElement}
|
|
167
|
+
</ScreenStackHeaderLeftView>
|
|
168
|
+
) : null}
|
|
169
|
+
{headerTitleElement != null ? (
|
|
170
|
+
<ScreenStackHeaderCenterView>
|
|
171
|
+
{headerTitleElement}
|
|
172
|
+
</ScreenStackHeaderCenterView>
|
|
173
|
+
) : null}
|
|
174
|
+
</>
|
|
175
|
+
) : (
|
|
176
|
+
<>
|
|
177
|
+
{headerLeftElement != null || typeof headerTitle === 'function' ? (
|
|
178
|
+
<ScreenStackHeaderLeftView>
|
|
179
|
+
<View style={styles.row}>
|
|
180
|
+
{headerLeftElement}
|
|
181
|
+
{headerTitleAlign !== 'center' ? (
|
|
182
|
+
typeof headerTitle === 'function' ? (
|
|
183
|
+
headerTitleElement
|
|
184
|
+
) : (
|
|
185
|
+
<HeaderTitle
|
|
186
|
+
tintColor={tintColor}
|
|
187
|
+
style={headerTitleStyleSupported}
|
|
188
|
+
>
|
|
189
|
+
{titleText}
|
|
190
|
+
</HeaderTitle>
|
|
191
|
+
)
|
|
192
|
+
) : null}
|
|
193
|
+
</View>
|
|
194
|
+
</ScreenStackHeaderLeftView>
|
|
195
|
+
) : null}
|
|
196
|
+
{headerTitleAlign === 'center' ? (
|
|
197
|
+
<ScreenStackHeaderCenterView>
|
|
198
|
+
{typeof headerTitle === 'function' ? (
|
|
199
|
+
headerTitleElement
|
|
200
|
+
) : (
|
|
201
|
+
<HeaderTitle
|
|
202
|
+
tintColor={tintColor}
|
|
203
|
+
style={headerTitleStyleSupported}
|
|
204
|
+
>
|
|
205
|
+
{titleText}
|
|
206
|
+
</HeaderTitle>
|
|
207
|
+
)}
|
|
208
|
+
</ScreenStackHeaderCenterView>
|
|
209
|
+
) : null}
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
{headerBackImageSource !== undefined ? (
|
|
213
|
+
<ScreenStackHeaderBackButtonImage source={headerBackImageSource} />
|
|
214
|
+
) : null}
|
|
215
|
+
{headerRightElement != null ? (
|
|
216
|
+
<ScreenStackHeaderRightView>
|
|
217
|
+
{headerRightElement}
|
|
218
|
+
</ScreenStackHeaderRightView>
|
|
219
|
+
) : null}
|
|
220
|
+
{Platform.OS === 'ios' && headerSearchBarOptions != null ? (
|
|
221
|
+
<ScreenStackHeaderSearchBarView>
|
|
222
|
+
<SearchBar {...headerSearchBarOptions} />
|
|
223
|
+
</ScreenStackHeaderSearchBarView>
|
|
224
|
+
) : null}
|
|
225
|
+
</ScreenStackHeaderConfig>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const styles = StyleSheet.create({
|
|
230
|
+
row: {
|
|
231
|
+
flexDirection: 'row',
|
|
232
|
+
alignItems: 'center',
|
|
233
|
+
},
|
|
234
|
+
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDefaultHeaderHeight,
|
|
3
|
+
getHeaderTitle,
|
|
4
|
+
HeaderHeightContext,
|
|
5
|
+
HeaderShownContext,
|
|
6
|
+
SafeAreaProviderCompat,
|
|
7
|
+
} from '@react-navigation/elements';
|
|
8
|
+
import {
|
|
9
|
+
ParamListBase,
|
|
10
|
+
Route,
|
|
11
|
+
StackActions,
|
|
12
|
+
StackNavigationState,
|
|
13
|
+
useTheme,
|
|
14
|
+
} from '@react-navigation/native';
|
|
15
|
+
import * as React from 'react';
|
|
16
|
+
import { Platform, PlatformIOSStatic, StyleSheet } from 'react-native';
|
|
17
|
+
import {
|
|
18
|
+
useSafeAreaFrame,
|
|
19
|
+
useSafeAreaInsets,
|
|
20
|
+
} from 'react-native-safe-area-context';
|
|
21
|
+
import {
|
|
22
|
+
Screen,
|
|
23
|
+
ScreenStack,
|
|
24
|
+
StackPresentationTypes,
|
|
25
|
+
} from 'react-native-screens';
|
|
26
|
+
import warnOnce from 'warn-once';
|
|
27
|
+
|
|
28
|
+
import type {
|
|
29
|
+
NativeStackDescriptor,
|
|
30
|
+
NativeStackDescriptorMap,
|
|
31
|
+
NativeStackNavigationHelpers,
|
|
32
|
+
NativeStackNavigationOptions,
|
|
33
|
+
} from '../types';
|
|
34
|
+
import DebugContainer from './DebugContainer';
|
|
35
|
+
import HeaderConfig from './HeaderConfig';
|
|
36
|
+
|
|
37
|
+
const isAndroid = Platform.OS === 'android';
|
|
38
|
+
|
|
39
|
+
const MaybeNestedStack = ({
|
|
40
|
+
options,
|
|
41
|
+
route,
|
|
42
|
+
presentation,
|
|
43
|
+
children,
|
|
44
|
+
}: {
|
|
45
|
+
options: NativeStackNavigationOptions;
|
|
46
|
+
route: Route<string>;
|
|
47
|
+
presentation: Exclude<StackPresentationTypes, 'push'> | 'card';
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
}) => {
|
|
50
|
+
const { colors } = useTheme();
|
|
51
|
+
const { header, headerShown = true, contentStyle } = options;
|
|
52
|
+
|
|
53
|
+
const isHeaderInModal = isAndroid
|
|
54
|
+
? false
|
|
55
|
+
: presentation !== 'card' && headerShown === true && header === undefined;
|
|
56
|
+
|
|
57
|
+
const headerShownPreviousRef = React.useRef(headerShown);
|
|
58
|
+
|
|
59
|
+
React.useEffect(() => {
|
|
60
|
+
warnOnce(
|
|
61
|
+
!isAndroid &&
|
|
62
|
+
presentation !== 'card' &&
|
|
63
|
+
headerShownPreviousRef.current !== headerShown,
|
|
64
|
+
`Dynamically changing 'headerShown' in modals will result in remounting the screen and losing all local state. See options for the screen '${route.name}'.`
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
headerShownPreviousRef.current = headerShown;
|
|
68
|
+
}, [headerShown, presentation, route.name]);
|
|
69
|
+
|
|
70
|
+
const content = (
|
|
71
|
+
<DebugContainer
|
|
72
|
+
style={[
|
|
73
|
+
styles.container,
|
|
74
|
+
presentation !== 'transparentModal' &&
|
|
75
|
+
presentation !== 'containedTransparentModal' && {
|
|
76
|
+
backgroundColor: colors.background,
|
|
77
|
+
},
|
|
78
|
+
contentStyle,
|
|
79
|
+
]}
|
|
80
|
+
stackPresentation={presentation === 'card' ? 'push' : presentation}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</DebugContainer>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const insets = useSafeAreaInsets();
|
|
87
|
+
const dimensions = useSafeAreaFrame();
|
|
88
|
+
// landscape is meaningful only for iPhone
|
|
89
|
+
const isLandscape =
|
|
90
|
+
dimensions.width > dimensions.height &&
|
|
91
|
+
!(Platform as PlatformIOSStatic).isPad &&
|
|
92
|
+
!(Platform as PlatformIOSStatic).isTVOS;
|
|
93
|
+
// `modal` and `formSheet` presentations do not take whole screen, so should not take the inset.
|
|
94
|
+
const isFullScreenModal =
|
|
95
|
+
presentation !== 'modal' && presentation !== 'formSheet';
|
|
96
|
+
const topInset = isFullScreenModal && !isLandscape ? insets.top : 0;
|
|
97
|
+
const headerHeight = getDefaultHeaderHeight(
|
|
98
|
+
dimensions,
|
|
99
|
+
!isFullScreenModal,
|
|
100
|
+
topInset
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (isHeaderInModal) {
|
|
104
|
+
return (
|
|
105
|
+
<ScreenStack style={styles.container}>
|
|
106
|
+
<Screen enabled style={StyleSheet.absoluteFill}>
|
|
107
|
+
<HeaderShownContext.Provider value>
|
|
108
|
+
<HeaderHeightContext.Provider value={headerHeight}>
|
|
109
|
+
<HeaderConfig {...options} route={route} canGoBack />
|
|
110
|
+
{content}
|
|
111
|
+
</HeaderHeightContext.Provider>
|
|
112
|
+
</HeaderShownContext.Provider>
|
|
113
|
+
</Screen>
|
|
114
|
+
</ScreenStack>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return content;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type SceneViewProps = {
|
|
122
|
+
index: number;
|
|
123
|
+
descriptor: NativeStackDescriptor;
|
|
124
|
+
previousDescriptor?: NativeStackDescriptor;
|
|
125
|
+
onWillDisappear: () => void;
|
|
126
|
+
onAppear: () => void;
|
|
127
|
+
onDisappear: () => void;
|
|
128
|
+
onDismissed: () => void;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const SceneView = ({
|
|
132
|
+
descriptor,
|
|
133
|
+
previousDescriptor,
|
|
134
|
+
index,
|
|
135
|
+
onWillDisappear,
|
|
136
|
+
onAppear,
|
|
137
|
+
onDisappear,
|
|
138
|
+
onDismissed,
|
|
139
|
+
}: SceneViewProps) => {
|
|
140
|
+
const { route, navigation, options, render } = descriptor;
|
|
141
|
+
const {
|
|
142
|
+
gestureEnabled,
|
|
143
|
+
header,
|
|
144
|
+
headerShown,
|
|
145
|
+
animationTypeForReplace = 'pop',
|
|
146
|
+
animation,
|
|
147
|
+
orientation,
|
|
148
|
+
statusBarAnimation,
|
|
149
|
+
statusBarHidden,
|
|
150
|
+
statusBarStyle,
|
|
151
|
+
} = options;
|
|
152
|
+
|
|
153
|
+
let { presentation = 'card' } = options;
|
|
154
|
+
|
|
155
|
+
if (index === 0) {
|
|
156
|
+
// first screen should always be treated as `card`, it resolves problems with no header animation
|
|
157
|
+
// for navigator with first screen as `modal` and the next as `card`
|
|
158
|
+
presentation = 'card';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const isHeaderInPush = isAndroid
|
|
162
|
+
? headerShown
|
|
163
|
+
: presentation === 'card' && headerShown !== false;
|
|
164
|
+
|
|
165
|
+
const isParentHeaderShown = React.useContext(HeaderShownContext);
|
|
166
|
+
const insets = useSafeAreaInsets();
|
|
167
|
+
const parentHeaderHeight = React.useContext(HeaderHeightContext);
|
|
168
|
+
const headerHeight = getDefaultHeaderHeight(
|
|
169
|
+
useSafeAreaFrame(),
|
|
170
|
+
false,
|
|
171
|
+
insets.top
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<Screen
|
|
176
|
+
key={route.key}
|
|
177
|
+
enabled
|
|
178
|
+
style={StyleSheet.absoluteFill}
|
|
179
|
+
gestureEnabled={
|
|
180
|
+
isAndroid
|
|
181
|
+
? // This prop enables handling of system back gestures on Android
|
|
182
|
+
// Since we handle them in JS side, we disable this
|
|
183
|
+
false
|
|
184
|
+
: gestureEnabled
|
|
185
|
+
}
|
|
186
|
+
replaceAnimation={animationTypeForReplace}
|
|
187
|
+
stackPresentation={presentation === 'card' ? 'push' : presentation}
|
|
188
|
+
stackAnimation={animation}
|
|
189
|
+
screenOrientation={orientation}
|
|
190
|
+
statusBarAnimation={statusBarAnimation}
|
|
191
|
+
statusBarHidden={statusBarHidden}
|
|
192
|
+
statusBarStyle={statusBarStyle}
|
|
193
|
+
onWillDisappear={onWillDisappear}
|
|
194
|
+
onAppear={onAppear}
|
|
195
|
+
onDisappear={onDisappear}
|
|
196
|
+
onDismissed={onDismissed}
|
|
197
|
+
>
|
|
198
|
+
<HeaderShownContext.Provider
|
|
199
|
+
value={isParentHeaderShown || isHeaderInPush !== false}
|
|
200
|
+
>
|
|
201
|
+
<HeaderHeightContext.Provider
|
|
202
|
+
value={
|
|
203
|
+
isHeaderInPush !== false ? headerHeight : parentHeaderHeight ?? 0
|
|
204
|
+
}
|
|
205
|
+
>
|
|
206
|
+
{header !== undefined && headerShown !== false ? (
|
|
207
|
+
// TODO: expose custom header height
|
|
208
|
+
header({
|
|
209
|
+
back: previousDescriptor
|
|
210
|
+
? {
|
|
211
|
+
title: getHeaderTitle(
|
|
212
|
+
previousDescriptor.options,
|
|
213
|
+
previousDescriptor.route.name
|
|
214
|
+
),
|
|
215
|
+
}
|
|
216
|
+
: undefined,
|
|
217
|
+
options,
|
|
218
|
+
route,
|
|
219
|
+
navigation,
|
|
220
|
+
})
|
|
221
|
+
) : (
|
|
222
|
+
<HeaderConfig
|
|
223
|
+
{...options}
|
|
224
|
+
route={route}
|
|
225
|
+
headerShown={isHeaderInPush}
|
|
226
|
+
canGoBack={index !== 0}
|
|
227
|
+
/>
|
|
228
|
+
)}
|
|
229
|
+
<MaybeNestedStack
|
|
230
|
+
options={options}
|
|
231
|
+
route={route}
|
|
232
|
+
presentation={presentation}
|
|
233
|
+
>
|
|
234
|
+
{render()}
|
|
235
|
+
</MaybeNestedStack>
|
|
236
|
+
</HeaderHeightContext.Provider>
|
|
237
|
+
</HeaderShownContext.Provider>
|
|
238
|
+
</Screen>
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
type Props = {
|
|
243
|
+
state: StackNavigationState<ParamListBase>;
|
|
244
|
+
navigation: NativeStackNavigationHelpers;
|
|
245
|
+
descriptors: NativeStackDescriptorMap;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
function NativeStackViewInner({ state, navigation, descriptors }: Props) {
|
|
249
|
+
const [nextDismissedKey, setNextDismissedKey] =
|
|
250
|
+
React.useState<string | null>(null);
|
|
251
|
+
|
|
252
|
+
const dismissedRouteName = nextDismissedKey
|
|
253
|
+
? state.routes.find((route) => route.key === nextDismissedKey)?.name
|
|
254
|
+
: null;
|
|
255
|
+
|
|
256
|
+
React.useEffect(() => {
|
|
257
|
+
if (dismissedRouteName) {
|
|
258
|
+
const message =
|
|
259
|
+
`The screen '${dismissedRouteName}' was removed natively but didn't get removed from JS state. ` +
|
|
260
|
+
`This can happen if the action was prevented in a 'beforeRemove' listener, which is not fully supported in native-stack.\n\n` +
|
|
261
|
+
`Consider using 'gestureEnabled: false' to prevent back gesture and use a custom back button with 'headerLeft' option to override the native behavior.`;
|
|
262
|
+
|
|
263
|
+
console.error(message);
|
|
264
|
+
}
|
|
265
|
+
}, [dismissedRouteName]);
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<ScreenStack style={styles.container}>
|
|
269
|
+
{state.routes.map((route, index) => {
|
|
270
|
+
const descriptor = descriptors[route.key];
|
|
271
|
+
const previousKey = state.routes[index - 1]?.key;
|
|
272
|
+
const previousDescriptor = previousKey
|
|
273
|
+
? descriptors[previousKey]
|
|
274
|
+
: undefined;
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<SceneView
|
|
278
|
+
key={route.key}
|
|
279
|
+
index={index}
|
|
280
|
+
descriptor={descriptor}
|
|
281
|
+
previousDescriptor={previousDescriptor}
|
|
282
|
+
onWillDisappear={() => {
|
|
283
|
+
navigation.emit({
|
|
284
|
+
type: 'transitionStart',
|
|
285
|
+
data: { closing: true },
|
|
286
|
+
target: route.key,
|
|
287
|
+
});
|
|
288
|
+
}}
|
|
289
|
+
onAppear={() => {
|
|
290
|
+
navigation.emit({
|
|
291
|
+
type: 'transitionEnd',
|
|
292
|
+
data: { closing: false },
|
|
293
|
+
target: route.key,
|
|
294
|
+
});
|
|
295
|
+
}}
|
|
296
|
+
onDisappear={() => {
|
|
297
|
+
navigation.emit({
|
|
298
|
+
type: 'transitionEnd',
|
|
299
|
+
data: { closing: true },
|
|
300
|
+
target: route.key,
|
|
301
|
+
});
|
|
302
|
+
}}
|
|
303
|
+
onDismissed={() => {
|
|
304
|
+
navigation.dispatch({
|
|
305
|
+
...StackActions.pop(),
|
|
306
|
+
source: route.key,
|
|
307
|
+
target: state.key,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
setNextDismissedKey(route.key);
|
|
311
|
+
}}
|
|
312
|
+
/>
|
|
313
|
+
);
|
|
314
|
+
})}
|
|
315
|
+
</ScreenStack>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export default function NativeStackView(props: Props) {
|
|
320
|
+
return (
|
|
321
|
+
<SafeAreaProviderCompat>
|
|
322
|
+
<NativeStackViewInner {...props} />
|
|
323
|
+
</SafeAreaProviderCompat>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const styles = StyleSheet.create({
|
|
328
|
+
container: {
|
|
329
|
+
flex: 1,
|
|
330
|
+
},
|
|
331
|
+
});
|