@rn-tools/navigation 2.0.0 → 2.1.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/README.md +148 -46
- package/package.json +2 -1
- package/src/contexts.tsx +1 -1
- package/src/navigation-reducer.ts +12 -6
- package/src/navigation-store.ts +12 -9
- package/src/navigation.tsx +1 -0
- package/src/stack.tsx +80 -14
- package/src/tabs.tsx +37 -16
- package/src/utils.ts +27 -0
package/README.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# @rn-tools/navigation
|
|
2
2
|
|
|
3
|
-
A set of useful navigation components for React Native. Built with `react-native-screens
|
|
3
|
+
A set of useful navigation components for React Native. Built with `react-native-screens` and designed with flexibility in mind.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Basic Usage](#basic-usage)
|
|
9
|
+
- [Stack Navigator](#stack-navigator)
|
|
10
|
+
- [Tab Navigator](#tab-navigator)
|
|
11
|
+
- [Rendering a stack inside of a tabbed screen](#rendering-a-stack-inside-of-a-tabbed-screen)
|
|
12
|
+
- [Targeting a specific stack](#targeting-a-specific-stack)
|
|
13
|
+
- [Pushing a screen once](#pushing-a-screen-once)
|
|
14
|
+
- [Targeting specific tabs](#targeting-specific-tabs)
|
|
15
|
+
- [Rendering a header](#rendering-a-header)
|
|
16
|
+
- [Components](#components)
|
|
17
|
+
- [Stack](#stack)
|
|
18
|
+
- [Tabs](#tabs)
|
|
19
|
+
- [Guides](#guides)
|
|
20
|
+
- [Authentication](#authentication)
|
|
4
21
|
|
|
5
22
|
## Installation
|
|
6
23
|
|
|
@@ -8,13 +25,23 @@ A set of useful navigation components for React Native. Built with `react-native
|
|
|
8
25
|
yarn expo install @rn-tools/navigation react-native-screens
|
|
9
26
|
```
|
|
10
27
|
|
|
28
|
+
**Note:** It's recommended that you install and wrap your app in a `SafeAreaProvider` to ensure components are rendered correctly based on the device's insets:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
yarn expo install react-native-safe-area-context
|
|
32
|
+
```
|
|
33
|
+
|
|
11
34
|
## Basic Usage
|
|
12
35
|
|
|
13
|
-
For basic usage, the exported `Stack.Navigator` and `Tabs.Navigator` will get you up and running quickly.
|
|
36
|
+
For basic usage, the exported `Stack.Navigator` and `Tabs.Navigator` will get you up and running quickly.
|
|
37
|
+
|
|
38
|
+
The [Guides](#guides) section covers how to use lower-level `Stack` and `Tabs` components in a variety of navigation patterns.
|
|
39
|
+
|
|
40
|
+
`Stack` and `Tabs` are composable components that can be safely nested within each other without any additional configuration or setup.
|
|
14
41
|
|
|
15
42
|
### Stack Navigator
|
|
16
43
|
|
|
17
|
-
The `Stack.Navigator` component manages
|
|
44
|
+
The `Stack.Navigator` component manages screens. Under the hood this is using `react-native-screens` to handle pushing and popping natively.
|
|
18
45
|
|
|
19
46
|
Screens are pushed and popped by the exported navigation methods:
|
|
20
47
|
|
|
@@ -22,6 +49,8 @@ Screens are pushed and popped by the exported navigation methods:
|
|
|
22
49
|
|
|
23
50
|
- `navigation.popScreen(numberOfScreens: number) => void`
|
|
24
51
|
|
|
52
|
+
In the majority of cases, these methods will determine the right stack without you needing to specify. But you can target a specific stacks as well if you need to! This is covered in the [Targeting a specific stack](#targeting-a-specific-stack) section.
|
|
53
|
+
|
|
25
54
|
```tsx
|
|
26
55
|
import { Stack, navigation } from "@rn-tools/navigation";
|
|
27
56
|
import * as React from "react";
|
|
@@ -33,15 +62,17 @@ export function BasicStack() {
|
|
|
33
62
|
|
|
34
63
|
function MyScreen({
|
|
35
64
|
title,
|
|
36
|
-
|
|
65
|
+
children,
|
|
37
66
|
}: {
|
|
38
67
|
title: string;
|
|
39
|
-
|
|
68
|
+
children?: React.ReactNode;
|
|
40
69
|
}) {
|
|
41
70
|
function pushScreen() {
|
|
42
71
|
navigation.pushScreen(
|
|
43
72
|
<Stack.Screen>
|
|
44
|
-
<MyScreen title="Pushed screen"
|
|
73
|
+
<MyScreen title="Pushed screen">
|
|
74
|
+
<Button title="Pop screen" onPress={popScreen} />
|
|
75
|
+
</MyScreen>
|
|
45
76
|
</Stack.Screen>
|
|
46
77
|
);
|
|
47
78
|
}
|
|
@@ -54,13 +85,13 @@ function MyScreen({
|
|
|
54
85
|
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
|
55
86
|
<Text>{title}</Text>
|
|
56
87
|
<Button title="Push screen" onPress={pushScreen} />
|
|
57
|
-
{
|
|
88
|
+
{children}
|
|
58
89
|
</View>
|
|
59
90
|
);
|
|
60
91
|
}
|
|
61
92
|
```
|
|
62
93
|
|
|
63
|
-
**Note**: The components passed to `navigation.pushScreen` need to be wrapped in a `Stack.Screen
|
|
94
|
+
**Note**: The components passed to `navigation.pushScreen` need to be wrapped in a `Stack.Screen`. Create a wrapper to simplify your usage if you'd like:
|
|
64
95
|
|
|
65
96
|
```tsx
|
|
66
97
|
function myPushScreen(
|
|
@@ -73,39 +104,31 @@ function myPushScreen(
|
|
|
73
104
|
|
|
74
105
|
### Tab Navigator
|
|
75
106
|
|
|
76
|
-
The `Tabs.Navigator` component also uses `react-native-screens` to handle
|
|
107
|
+
The `Tabs.Navigator` component also uses `react-native-screens` to handle switching between tabs natively.
|
|
108
|
+
|
|
109
|
+
The active tab can be changed via the `navigation.setTabIndex` method, however the built in tabbar handles switching between screens out of the box.
|
|
77
110
|
|
|
78
111
|
```tsx
|
|
79
|
-
import {
|
|
80
|
-
Stack,
|
|
81
|
-
Tabs,
|
|
82
|
-
navigation,
|
|
83
|
-
Stack,
|
|
84
|
-
defaultTabbarStyle,
|
|
85
|
-
} from "@rn-tools/navigation";
|
|
112
|
+
import { Tabs, navigation, Stack } from "@rn-tools/navigation";
|
|
86
113
|
import * as React from "react";
|
|
87
114
|
import { View, Text, Button } from "react-native";
|
|
88
|
-
|
|
115
|
+
|
|
116
|
+
// It's recommended to wrap your App in a SafeAreaProvider once
|
|
117
|
+
import { SafeAreaProvider } from "react-native-safe-area-context";
|
|
89
118
|
|
|
90
119
|
export function BasicTabs() {
|
|
91
|
-
return
|
|
120
|
+
return (
|
|
121
|
+
<SafeAreaProvider>
|
|
122
|
+
<Stack.Navigator rootScreen={<MyTabs />} />
|
|
123
|
+
</SafeAreaProvider>
|
|
124
|
+
);
|
|
92
125
|
}
|
|
93
126
|
|
|
94
127
|
function MyTabs() {
|
|
95
|
-
// This hook requires you to wrap your app in a SafeAreaProvider component - see the `react-native-safe-area-context` package
|
|
96
|
-
let insets = useSafeAreaInsets();
|
|
97
|
-
|
|
98
|
-
let tabbarStyle = React.useMemo(() => {
|
|
99
|
-
return {
|
|
100
|
-
...defaultTabbarStyle,
|
|
101
|
-
bottom: insets.bottom,
|
|
102
|
-
};
|
|
103
|
-
}, [insets.bottom]);
|
|
104
|
-
|
|
105
128
|
return (
|
|
106
129
|
<Tabs.Navigator
|
|
107
130
|
tabbarPosition="bottom"
|
|
108
|
-
tabbarStyle={
|
|
131
|
+
tabbarStyle={{ backgroundColor: "blue" }}
|
|
109
132
|
screens={[
|
|
110
133
|
{
|
|
111
134
|
key: "1",
|
|
@@ -147,17 +170,19 @@ function MyTab({
|
|
|
147
170
|
|
|
148
171
|
function MyScreen({
|
|
149
172
|
title,
|
|
150
|
-
|
|
173
|
+
children,
|
|
151
174
|
bg,
|
|
152
175
|
}: {
|
|
153
176
|
title: string;
|
|
154
|
-
|
|
177
|
+
children?: React.ReactNode;
|
|
155
178
|
bg?: string;
|
|
156
179
|
}) {
|
|
157
180
|
function pushScreen() {
|
|
158
181
|
navigation.pushScreen(
|
|
159
182
|
<Stack.Screen>
|
|
160
|
-
<MyScreen title="Pushed screen"
|
|
183
|
+
<MyScreen title="Pushed screen" bg={bg}>
|
|
184
|
+
<Button title="Pop screen" onPress={popScreen} />
|
|
185
|
+
</MyScreen>
|
|
161
186
|
</Stack.Screen>
|
|
162
187
|
);
|
|
163
188
|
}
|
|
@@ -177,7 +202,7 @@ function MyScreen({
|
|
|
177
202
|
>
|
|
178
203
|
<Text>{title}</Text>
|
|
179
204
|
<Button title="Push screen" onPress={pushScreen} />
|
|
180
|
-
{
|
|
205
|
+
{children}
|
|
181
206
|
</View>
|
|
182
207
|
);
|
|
183
208
|
}
|
|
@@ -187,8 +212,6 @@ function MyScreen({
|
|
|
187
212
|
|
|
188
213
|
Each tab can have its own stack by nesting the `Stack.Navigator` component.
|
|
189
214
|
|
|
190
|
-
- `navigation.pushScreen` will still work relative by pushing to the relative parent stack of the screen. See the next section for how to push a screen onto a specific stack.
|
|
191
|
-
|
|
192
215
|
```tsx
|
|
193
216
|
function MyTabs() {
|
|
194
217
|
return (
|
|
@@ -272,9 +295,70 @@ function switchMainTabsToTab(tabIndex: number) {
|
|
|
272
295
|
}
|
|
273
296
|
```
|
|
274
297
|
|
|
298
|
+
### Rendering a header
|
|
299
|
+
|
|
300
|
+
Use the `Stack.Header` component to render a native header in a screen.
|
|
301
|
+
|
|
302
|
+
Under the hood this is using `react-native-screens` header - [here is a reference for the available props](https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md#screenstackheaderconfig)
|
|
303
|
+
|
|
304
|
+
**Note:** Wrap your App in a `SafeAreaProvider` to ensure your screen components are rendered correctly with the header
|
|
305
|
+
|
|
306
|
+
**Note:**: The header component **has to be the first child** of a `Stack.Screen` component.
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
import { navigation, Stack } from "@rn-tools/navigation";
|
|
310
|
+
import * as React from "react";
|
|
311
|
+
import { Button, View, TextInput } from "react-native";
|
|
312
|
+
|
|
313
|
+
export function HeaderExample() {
|
|
314
|
+
return (
|
|
315
|
+
<View>
|
|
316
|
+
<Button
|
|
317
|
+
title="Push screen with header"
|
|
318
|
+
onPress={() => navigation.pushScreen(<MyScreenWithHeader />)}
|
|
319
|
+
/>
|
|
320
|
+
</View>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function MyScreenWithHeader() {
|
|
325
|
+
let [title, setTitle] = React.useState("");
|
|
326
|
+
|
|
327
|
+
return (
|
|
328
|
+
<Stack.Screen>
|
|
329
|
+
{/* Header must be the first child */}
|
|
330
|
+
<Stack.Header
|
|
331
|
+
title={title}
|
|
332
|
+
// Some potentially useful props - see the reference posted above for all available props
|
|
333
|
+
backTitle="Custom back title"
|
|
334
|
+
backTitleFontSize={16}
|
|
335
|
+
hideBackButton={false}
|
|
336
|
+
/>
|
|
337
|
+
|
|
338
|
+
<View
|
|
339
|
+
style={{
|
|
340
|
+
flex: 1,
|
|
341
|
+
alignItems: "center",
|
|
342
|
+
paddingVertical: 48,
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
<TextInput
|
|
346
|
+
style={{ fontSize: 26, fontWeight: "semibold" }}
|
|
347
|
+
value={title}
|
|
348
|
+
onChangeText={setTitle}
|
|
349
|
+
placeholder="Enter header text"
|
|
350
|
+
/>
|
|
351
|
+
</View>
|
|
352
|
+
</Stack.Screen>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
275
357
|
## Components
|
|
276
358
|
|
|
277
|
-
The `Navigator` components in the previous examples are
|
|
359
|
+
The `Navigator` components in the previous examples are fairly straightforward wrappers around other lower level `Stack` and `Tabs` components.
|
|
360
|
+
|
|
361
|
+
If you need to customize behaviour, design a component API you prefer to use, or just enjoy writing your own components, you can use these implementations as a reference to build your own.
|
|
278
362
|
|
|
279
363
|
### Stack
|
|
280
364
|
|
|
@@ -311,30 +395,47 @@ export function StackNavigator({
|
|
|
311
395
|
- Reference for props that can be passed: [Screen Props](https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md#screen)
|
|
312
396
|
- `Stack.Slot` - A slot for screens to be pushed into.
|
|
313
397
|
- This component is used to render screens that are pushed using `navigation.pushScreen` - don't forget to render this somewhere in `Stack.Screens`!
|
|
398
|
+
- `Stack.Header` - A header for a screen.
|
|
399
|
+
- **Must be rendered as the first child of a `Stack.Screen` component.**
|
|
400
|
+
- This is a `react-native-screens` StackHeader component under the hood.
|
|
401
|
+
- Reference for props that can be passed: [Header Props](https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md#screenstackheaderconfig)
|
|
314
402
|
|
|
315
403
|
## Tabs
|
|
316
404
|
|
|
317
405
|
This is the implementation of the exported `Tabs.Navigator` component:
|
|
318
406
|
|
|
319
407
|
```tsx
|
|
320
|
-
type
|
|
321
|
-
screens:
|
|
408
|
+
export type TabNavigatorProps = Omit<TabsRootProps, "children"> & {
|
|
409
|
+
screens: TabNavigatorScreenOptions[];
|
|
322
410
|
tabbarPosition?: "top" | "bottom";
|
|
323
411
|
tabbarStyle?: ViewProps["style"];
|
|
324
412
|
};
|
|
325
413
|
|
|
326
|
-
type
|
|
414
|
+
export type TabNavigatorScreenOptions = {
|
|
327
415
|
key: string;
|
|
328
|
-
screen: React.ReactElement<
|
|
416
|
+
screen: React.ReactElement<unknown>;
|
|
329
417
|
tab: (props: { isActive: boolean; onPress: () => void }) => React.ReactNode;
|
|
330
418
|
};
|
|
331
419
|
|
|
332
|
-
|
|
420
|
+
let TabNavigator = React.memo(function TabNavigator({
|
|
333
421
|
screens,
|
|
334
422
|
tabbarPosition = "bottom",
|
|
335
|
-
tabbarStyle,
|
|
423
|
+
tabbarStyle: tabbarStyleProp,
|
|
336
424
|
...rootProps
|
|
337
|
-
}:
|
|
425
|
+
}: TabNavigatorProps) {
|
|
426
|
+
let insets = useSafeAreaInsetsSafe();
|
|
427
|
+
|
|
428
|
+
let tabbarStyle = React.useMemo(() => {
|
|
429
|
+
return [
|
|
430
|
+
defaultTabbarStyle,
|
|
431
|
+
{
|
|
432
|
+
paddingBottom: tabbarPosition === "bottom" ? insets.bottom : 0,
|
|
433
|
+
paddingTop: tabbarPosition === "top" ? insets.top : 0,
|
|
434
|
+
},
|
|
435
|
+
tabbarStyleProp,
|
|
436
|
+
];
|
|
437
|
+
}, [tabbarPosition, tabbarStyleProp, insets]);
|
|
438
|
+
|
|
338
439
|
return (
|
|
339
440
|
<Tabs.Root {...rootProps}>
|
|
340
441
|
{tabbarPosition === "top" && (
|
|
@@ -360,7 +461,8 @@ export function Tabs.Navigator({
|
|
|
360
461
|
)}
|
|
361
462
|
</Tabs.Root>
|
|
362
463
|
);
|
|
363
|
-
}
|
|
464
|
+
});
|
|
465
|
+
|
|
364
466
|
```
|
|
365
467
|
|
|
366
468
|
- `Tabs.Root` - The root component for a tabs navigator.
|
|
@@ -373,6 +475,8 @@ export function Tabs.Navigator({
|
|
|
373
475
|
- `Tabs.Tab` - A tab in a tabs navigator
|
|
374
476
|
- This is a Pressable component that switches the active screen
|
|
375
477
|
|
|
478
|
+
|
|
479
|
+
|
|
376
480
|
## Guides
|
|
377
481
|
|
|
378
482
|
### Authentication
|
|
@@ -418,5 +522,3 @@ let useUser = () => {
|
|
|
418
522
|
return user;
|
|
419
523
|
};
|
|
420
524
|
```
|
|
421
|
-
|
|
422
|
-
**Note:** Screens that are pushed using `pushScreen` are rendered in the `Slot` component
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rn-tools/navigation",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"main": "./src/index.ts",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"react": "*",
|
|
32
32
|
"react-native": "*",
|
|
33
|
+
"react-native-safe-area-context": "*",
|
|
33
34
|
"react-native-screens": "*"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
package/src/contexts.tsx
CHANGED
|
@@ -365,12 +365,13 @@ export function reducer(
|
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
let nextState: NavigationState = Object.assign({}, state);
|
|
368
|
+
let currentIndex = tab.activeIndex;
|
|
368
369
|
nextState.tabs.lookup[tabId] = Object.assign(
|
|
369
370
|
{},
|
|
370
371
|
{
|
|
371
372
|
...nextState.tabs.lookup[tabId],
|
|
372
373
|
activeIndex: index,
|
|
373
|
-
history: tab.history.filter((i) => i !== index).concat(
|
|
374
|
+
history: tab.history.filter((i) => i !== index).concat(currentIndex),
|
|
374
375
|
}
|
|
375
376
|
);
|
|
376
377
|
|
|
@@ -441,11 +442,16 @@ export function reducer(
|
|
|
441
442
|
let { tabId } = action;
|
|
442
443
|
let nextState: NavigationState = Object.assign({}, state);
|
|
443
444
|
|
|
444
|
-
let tab
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
445
|
+
let tab = nextState.tabs.lookup[tabId];
|
|
446
|
+
|
|
447
|
+
let lastActiveIndex = tab.history[tab.history.length - 1];
|
|
448
|
+
|
|
449
|
+
nextState.tabs.lookup = Object.assign({}, nextState.tabs.lookup, {
|
|
450
|
+
[tabId]: Object.assign({}, nextState.tabs.lookup[tabId], {
|
|
451
|
+
activeIndex: lastActiveIndex,
|
|
452
|
+
history: tab.history.filter((i) => i !== lastActiveIndex),
|
|
453
|
+
}),
|
|
454
|
+
});
|
|
449
455
|
|
|
450
456
|
return nextState;
|
|
451
457
|
}
|
package/src/navigation-store.ts
CHANGED
|
@@ -17,20 +17,18 @@ export function createNavigationStore() {
|
|
|
17
17
|
|
|
18
18
|
let reducer = (state: NavigationState, action: NavigationAction) => {
|
|
19
19
|
let nextState = navigationReducer(state, action, { renderCharts });
|
|
20
|
+
if (nextState.debugModeEnabled) {
|
|
21
|
+
console.debug(
|
|
22
|
+
`[@rntoolkit/navigation] action: ${action.type}`,
|
|
23
|
+
state,
|
|
24
|
+
nextState
|
|
25
|
+
);
|
|
26
|
+
}
|
|
20
27
|
return { ...nextState };
|
|
21
28
|
};
|
|
22
29
|
|
|
23
30
|
let store = createStore(devtools(redux(reducer, initialState)));
|
|
24
31
|
|
|
25
|
-
store.subscribe((state) => {
|
|
26
|
-
if (state.debugModeEnabled) {
|
|
27
|
-
console.debug("[@rntoolkit/navigation] state updated: ", {
|
|
28
|
-
state,
|
|
29
|
-
renderCharts,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
32
|
return {
|
|
35
33
|
store: store,
|
|
36
34
|
dispatch: store.dispatch,
|
|
@@ -53,3 +51,8 @@ export function useNavigationDispatch() {
|
|
|
53
51
|
let dispatch = React.useContext(NavigationDispatchContext);
|
|
54
52
|
return dispatch;
|
|
55
53
|
}
|
|
54
|
+
|
|
55
|
+
export function useGetNavigationStore() {
|
|
56
|
+
let context = React.useContext(NavigationStateContext);
|
|
57
|
+
return context.getState
|
|
58
|
+
}
|
package/src/navigation.tsx
CHANGED
package/src/stack.tsx
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BackHandler,
|
|
4
|
+
PixelRatio,
|
|
5
|
+
Platform,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
useWindowDimensions,
|
|
8
|
+
View,
|
|
9
|
+
type LayoutRectangle,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
} from "react-native";
|
|
3
12
|
import {
|
|
4
13
|
ScreenStackProps as RNScreenStackProps,
|
|
5
14
|
Screen as RNScreen,
|
|
@@ -18,16 +27,18 @@ import {
|
|
|
18
27
|
import { DEFAULT_SLOT_NAME } from "./navigation-reducer";
|
|
19
28
|
import { useNavigationDispatch, useNavigationState } from "./navigation-store";
|
|
20
29
|
import type { StackItem } from "./types";
|
|
21
|
-
import { generateStackId } from "./utils";
|
|
30
|
+
import { generateStackId, useSafeAreaInsetsSafe } from "./utils";
|
|
22
31
|
|
|
23
32
|
let StackIdContext = React.createContext<string>("");
|
|
24
33
|
let ScreenIdContext = React.createContext<string>("");
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
// Component returned from `react-native-screens` references `react-navigation` data structures in recent updates
|
|
36
|
+
// This is a workaround to make it work with our custom navigation
|
|
37
|
+
let RNScreenStack = React.memo(function RNScreenStack(
|
|
27
38
|
props: RNScreenStackProps
|
|
28
39
|
) {
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
let { children, gestureDetectorBridge, ...rest } = props;
|
|
41
|
+
let ref = React.useRef(null);
|
|
31
42
|
|
|
32
43
|
React.useEffect(() => {
|
|
33
44
|
if (gestureDetectorBridge) {
|
|
@@ -112,18 +123,13 @@ function StackRoot({ children, id }: StackRootProps) {
|
|
|
112
123
|
);
|
|
113
124
|
}
|
|
114
125
|
|
|
115
|
-
function StackScreens({
|
|
116
|
-
style: styleProp,
|
|
117
|
-
...props
|
|
118
|
-
}: RNScreenStackProps) {
|
|
126
|
+
function StackScreens({ style: styleProp, ...props }: RNScreenStackProps) {
|
|
119
127
|
let style = React.useMemo(
|
|
120
128
|
() => styleProp || StyleSheet.absoluteFill,
|
|
121
129
|
[styleProp]
|
|
122
130
|
);
|
|
123
131
|
|
|
124
|
-
return
|
|
125
|
-
<RNScreenStack {...props} style={style} />
|
|
126
|
-
);
|
|
132
|
+
return <RNScreenStack {...props} style={style} />;
|
|
127
133
|
}
|
|
128
134
|
|
|
129
135
|
let defaultScreenStyle: ViewStyle = {
|
|
@@ -180,7 +186,7 @@ let StackScreen = React.memo(function StackScreen({
|
|
|
180
186
|
<RNScreen
|
|
181
187
|
{...props}
|
|
182
188
|
style={style}
|
|
183
|
-
|
|
189
|
+
activityState={isActive ? 2 : 0}
|
|
184
190
|
gestureEnabled={gestureEnabled}
|
|
185
191
|
onDismissed={onDismissed}
|
|
186
192
|
>
|
|
@@ -224,7 +230,23 @@ let StackSlot = React.memo(function StackSlot({
|
|
|
224
230
|
let StackScreenHeader = React.memo(function StackScreenHeader({
|
|
225
231
|
...props
|
|
226
232
|
}: RNScreenStackHeaderConfigProps) {
|
|
227
|
-
|
|
233
|
+
let layout = useWindowDimensions();
|
|
234
|
+
let insets = useSafeAreaInsetsSafe();
|
|
235
|
+
|
|
236
|
+
let headerHeight = React.useMemo(() => {
|
|
237
|
+
if (Platform.OS === "android") {
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return getDefaultHeaderHeight(layout, false, insets.top);
|
|
242
|
+
}, [layout, insets]);
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<React.Fragment>
|
|
246
|
+
<RNScreenStackHeaderConfig {...props} />
|
|
247
|
+
<View style={{ height: headerHeight }} />
|
|
248
|
+
</React.Fragment>
|
|
249
|
+
);
|
|
228
250
|
});
|
|
229
251
|
|
|
230
252
|
type StackNavigatorProps = Omit<StackRootProps, "children"> & {
|
|
@@ -253,3 +275,47 @@ export let Stack = {
|
|
|
253
275
|
Slot: StackSlot,
|
|
254
276
|
Navigator: StackNavigator,
|
|
255
277
|
};
|
|
278
|
+
|
|
279
|
+
// `onLayout` event does not return a value for the native header component
|
|
280
|
+
// This function is copied from react-navigation to get the default header heights
|
|
281
|
+
// Ref: https://github.com/react-navigation/react-navigation/blob/main/packages/elements/src/Header/getDefaultHeaderHeight.tsx#L5
|
|
282
|
+
function getDefaultHeaderHeight(
|
|
283
|
+
layout: Pick<LayoutRectangle, "width" | "height">,
|
|
284
|
+
// TODO - handle modal headers and substacks
|
|
285
|
+
modalPresentation: boolean,
|
|
286
|
+
topInset: number
|
|
287
|
+
): number {
|
|
288
|
+
let headerHeight;
|
|
289
|
+
|
|
290
|
+
// On models with Dynamic Island the status bar height is smaller than the safe area top inset.
|
|
291
|
+
let hasDynamicIsland = Platform.OS === "ios" && topInset > 50;
|
|
292
|
+
let statusBarHeight = hasDynamicIsland
|
|
293
|
+
? topInset - (5 + 1 / PixelRatio.get())
|
|
294
|
+
: topInset;
|
|
295
|
+
|
|
296
|
+
let isLandscape = layout.width > layout.height;
|
|
297
|
+
|
|
298
|
+
if (Platform.OS === "ios") {
|
|
299
|
+
if (Platform.isPad || Platform.isTV) {
|
|
300
|
+
if (modalPresentation) {
|
|
301
|
+
headerHeight = 56;
|
|
302
|
+
} else {
|
|
303
|
+
headerHeight = 50;
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
if (isLandscape) {
|
|
307
|
+
headerHeight = 32;
|
|
308
|
+
} else {
|
|
309
|
+
if (modalPresentation) {
|
|
310
|
+
headerHeight = 56;
|
|
311
|
+
} else {
|
|
312
|
+
headerHeight = 44;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
headerHeight = 64;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return headerHeight + statusBarHeight;
|
|
321
|
+
}
|
package/src/tabs.tsx
CHANGED
|
@@ -21,8 +21,12 @@ import {
|
|
|
21
21
|
TabIdContext,
|
|
22
22
|
TabScreenIndexContext,
|
|
23
23
|
} from "./contexts";
|
|
24
|
-
import {
|
|
25
|
-
|
|
24
|
+
import {
|
|
25
|
+
useGetNavigationStore,
|
|
26
|
+
useNavigationDispatch,
|
|
27
|
+
useNavigationState,
|
|
28
|
+
} from "./navigation-store";
|
|
29
|
+
import { generateTabId, useSafeAreaInsetsSafe } from "./utils";
|
|
26
30
|
|
|
27
31
|
type TabsRootProps = {
|
|
28
32
|
children: React.ReactNode;
|
|
@@ -121,6 +125,7 @@ let TabsScreen = React.memo(function TabsScreen({
|
|
|
121
125
|
|
|
122
126
|
let tabId = React.useContext(TabIdContext);
|
|
123
127
|
let tabs = useTabsInternal(tabId);
|
|
128
|
+
let getNavigationStore = useGetNavigationStore();
|
|
124
129
|
let index = React.useContext(TabScreenIndexContext);
|
|
125
130
|
|
|
126
131
|
let parentIsActive = React.useContext(ActiveContext);
|
|
@@ -129,6 +134,9 @@ let TabsScreen = React.memo(function TabsScreen({
|
|
|
129
134
|
|
|
130
135
|
React.useEffect(() => {
|
|
131
136
|
function backHandler() {
|
|
137
|
+
// Use getter to register the handler once on mount
|
|
138
|
+
// Prevents it from overriding child screen handlers
|
|
139
|
+
let tabs = getNavigationStore().tabs.lookup[tabId];
|
|
132
140
|
if (tabs && tabs.history.length > 0) {
|
|
133
141
|
dispatch({ type: "TAB_BACK", tabId });
|
|
134
142
|
return true;
|
|
@@ -142,7 +150,7 @@ let TabsScreen = React.memo(function TabsScreen({
|
|
|
142
150
|
return () => {
|
|
143
151
|
BackHandler.removeEventListener("hardwareBackPress", backHandler);
|
|
144
152
|
};
|
|
145
|
-
}, [tabId, dispatch,
|
|
153
|
+
}, [tabId, dispatch, getNavigationStore]);
|
|
146
154
|
|
|
147
155
|
let style = React.useMemo(
|
|
148
156
|
() => styleProp || StyleSheet.absoluteFill,
|
|
@@ -242,12 +250,37 @@ let TabsTab = React.memo(function TabsTab({
|
|
|
242
250
|
});
|
|
243
251
|
|
|
244
252
|
|
|
253
|
+
export type TabNavigatorProps = Omit<TabsRootProps, "children"> & {
|
|
254
|
+
screens: TabNavigatorScreenOptions[];
|
|
255
|
+
tabbarPosition?: "top" | "bottom";
|
|
256
|
+
tabbarStyle?: ViewProps["style"];
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export type TabNavigatorScreenOptions = {
|
|
260
|
+
key: string;
|
|
261
|
+
screen: React.ReactElement<unknown>;
|
|
262
|
+
tab: (props: { isActive: boolean; onPress: () => void }) => React.ReactNode;
|
|
263
|
+
};
|
|
264
|
+
|
|
245
265
|
let TabNavigator = React.memo(function TabNavigator({
|
|
246
266
|
screens,
|
|
247
267
|
tabbarPosition = "bottom",
|
|
248
|
-
tabbarStyle,
|
|
268
|
+
tabbarStyle: tabbarStyleProp,
|
|
249
269
|
...rootProps
|
|
250
270
|
}: TabNavigatorProps) {
|
|
271
|
+
let insets = useSafeAreaInsetsSafe();
|
|
272
|
+
|
|
273
|
+
let tabbarStyle = React.useMemo(() => {
|
|
274
|
+
return [
|
|
275
|
+
defaultTabbarStyle,
|
|
276
|
+
{
|
|
277
|
+
paddingBottom: tabbarPosition === "bottom" ? insets.bottom : 0,
|
|
278
|
+
paddingTop: tabbarPosition === "top" ? insets.top : 0,
|
|
279
|
+
},
|
|
280
|
+
tabbarStyleProp,
|
|
281
|
+
];
|
|
282
|
+
}, [tabbarPosition, tabbarStyleProp, insets]);
|
|
283
|
+
|
|
251
284
|
return (
|
|
252
285
|
<Tabs.Root {...rootProps}>
|
|
253
286
|
{tabbarPosition === "top" && (
|
|
@@ -283,15 +316,3 @@ export let Tabs = {
|
|
|
283
316
|
Tab: TabsTab,
|
|
284
317
|
Navigator: TabNavigator,
|
|
285
318
|
};
|
|
286
|
-
|
|
287
|
-
export type TabNavigatorProps = Omit<TabsRootProps, "children"> & {
|
|
288
|
-
screens: TabNavigatorScreenOptions[];
|
|
289
|
-
tabbarPosition?: "top" | "bottom";
|
|
290
|
-
tabbarStyle?: ViewProps["style"];
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
export type TabNavigatorScreenOptions = {
|
|
294
|
-
key: string;
|
|
295
|
-
screen: React.ReactElement<unknown>;
|
|
296
|
-
tab: (props: { isActive: boolean; onPress: () => void }) => React.ReactNode;
|
|
297
|
-
};
|
package/src/utils.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import { useSafeAreaInsets, type EdgeInsets } from 'react-native-safe-area-context'
|
|
3
|
+
|
|
1
4
|
export let generateStackId = createIdGenerator("stack");
|
|
2
5
|
export let generateScreenId = createIdGenerator("screen");
|
|
3
6
|
export let generateTabId = createIdGenerator("tab");
|
|
@@ -12,3 +15,27 @@ function createIdGenerator(name: string) {
|
|
|
12
15
|
|
|
13
16
|
export let serializeTabIndexKey = (tabId: string, index: number) =>
|
|
14
17
|
`${tabId}-${index}`;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
let baseInsets: EdgeInsets = {
|
|
23
|
+
top: Platform.OS === "ios" ? 59 : 49,
|
|
24
|
+
bottom: Platform.OS === "ios" ? 34 : 0,
|
|
25
|
+
right: 0,
|
|
26
|
+
left: 0,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function useSafeAreaInsetsSafe() {
|
|
30
|
+
let insets = baseInsets;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Linter thinks this is conditional but it seems fine
|
|
34
|
+
// eslint-disable-next-line
|
|
35
|
+
insets = useSafeAreaInsets();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.log("useSafeAreaInsets is not available");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return insets;
|
|
41
|
+
}
|