@rn-tools/navigation 2.0.0 → 2.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/README.md +368 -45
- package/package.json +3 -1
- package/src/contexts.tsx +1 -1
- package/src/deep-links.tsx +39 -0
- package/src/index.ts +2 -1
- package/src/navigation-reducer.ts +43 -23
- package/src/navigation-store.ts +12 -9
- package/src/navigation.tsx +2 -1
- package/src/stack.tsx +114 -14
- package/src/tabs.tsx +42 -17
- package/src/utils.ts +27 -0
package/README.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
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)
|
|
21
|
+
- [Deep Links](#deep-links)
|
|
22
|
+
- [Preventing going back](#preventing-going-back)
|
|
4
23
|
|
|
5
24
|
## Installation
|
|
6
25
|
|
|
@@ -8,13 +27,23 @@ A set of useful navigation components for React Native. Built with `react-native
|
|
|
8
27
|
yarn expo install @rn-tools/navigation react-native-screens
|
|
9
28
|
```
|
|
10
29
|
|
|
30
|
+
**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:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
yarn expo install react-native-safe-area-context
|
|
34
|
+
```
|
|
35
|
+
|
|
11
36
|
## Basic Usage
|
|
12
37
|
|
|
13
|
-
For basic usage, the exported `Stack.Navigator` and `Tabs.Navigator` will get you up and running quickly.
|
|
38
|
+
For basic usage, the exported `Stack.Navigator` and `Tabs.Navigator` will get you up and running quickly.
|
|
39
|
+
|
|
40
|
+
The [Guides](#guides) section covers how to use lower-level `Stack` and `Tabs` components in a variety of navigation patterns.
|
|
41
|
+
|
|
42
|
+
`Stack` and `Tabs` are composable components that can be safely nested within each other without any additional configuration or setup.
|
|
14
43
|
|
|
15
44
|
### Stack Navigator
|
|
16
45
|
|
|
17
|
-
The `Stack.Navigator` component manages
|
|
46
|
+
The `Stack.Navigator` component manages screens. Under the hood this is using `react-native-screens` to handle pushing and popping natively.
|
|
18
47
|
|
|
19
48
|
Screens are pushed and popped by the exported navigation methods:
|
|
20
49
|
|
|
@@ -22,6 +51,8 @@ Screens are pushed and popped by the exported navigation methods:
|
|
|
22
51
|
|
|
23
52
|
- `navigation.popScreen(numberOfScreens: number) => void`
|
|
24
53
|
|
|
54
|
+
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.
|
|
55
|
+
|
|
25
56
|
```tsx
|
|
26
57
|
import { Stack, navigation } from "@rn-tools/navigation";
|
|
27
58
|
import * as React from "react";
|
|
@@ -33,15 +64,17 @@ export function BasicStack() {
|
|
|
33
64
|
|
|
34
65
|
function MyScreen({
|
|
35
66
|
title,
|
|
36
|
-
|
|
67
|
+
children,
|
|
37
68
|
}: {
|
|
38
69
|
title: string;
|
|
39
|
-
|
|
70
|
+
children?: React.ReactNode;
|
|
40
71
|
}) {
|
|
41
72
|
function pushScreen() {
|
|
42
73
|
navigation.pushScreen(
|
|
43
74
|
<Stack.Screen>
|
|
44
|
-
<MyScreen title="Pushed screen"
|
|
75
|
+
<MyScreen title="Pushed screen">
|
|
76
|
+
<Button title="Pop screen" onPress={popScreen} />
|
|
77
|
+
</MyScreen>
|
|
45
78
|
</Stack.Screen>
|
|
46
79
|
);
|
|
47
80
|
}
|
|
@@ -54,13 +87,13 @@ function MyScreen({
|
|
|
54
87
|
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
|
55
88
|
<Text>{title}</Text>
|
|
56
89
|
<Button title="Push screen" onPress={pushScreen} />
|
|
57
|
-
{
|
|
90
|
+
{children}
|
|
58
91
|
</View>
|
|
59
92
|
);
|
|
60
93
|
}
|
|
61
94
|
```
|
|
62
95
|
|
|
63
|
-
**Note**: The components passed to `navigation.pushScreen` need to be wrapped in a `Stack.Screen
|
|
96
|
+
**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
97
|
|
|
65
98
|
```tsx
|
|
66
99
|
function myPushScreen(
|
|
@@ -73,39 +106,31 @@ function myPushScreen(
|
|
|
73
106
|
|
|
74
107
|
### Tab Navigator
|
|
75
108
|
|
|
76
|
-
The `Tabs.Navigator` component also uses `react-native-screens` to handle
|
|
109
|
+
The `Tabs.Navigator` component also uses `react-native-screens` to handle switching between tabs natively.
|
|
110
|
+
|
|
111
|
+
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
112
|
|
|
78
113
|
```tsx
|
|
79
|
-
import {
|
|
80
|
-
Stack,
|
|
81
|
-
Tabs,
|
|
82
|
-
navigation,
|
|
83
|
-
Stack,
|
|
84
|
-
defaultTabbarStyle,
|
|
85
|
-
} from "@rn-tools/navigation";
|
|
114
|
+
import { Tabs, navigation, Stack } from "@rn-tools/navigation";
|
|
86
115
|
import * as React from "react";
|
|
87
116
|
import { View, Text, Button } from "react-native";
|
|
88
|
-
|
|
117
|
+
|
|
118
|
+
// It's recommended to wrap your App in a SafeAreaProvider once
|
|
119
|
+
import { SafeAreaProvider } from "react-native-safe-area-context";
|
|
89
120
|
|
|
90
121
|
export function BasicTabs() {
|
|
91
|
-
return
|
|
122
|
+
return (
|
|
123
|
+
<SafeAreaProvider>
|
|
124
|
+
<Stack.Navigator rootScreen={<MyTabs />} />
|
|
125
|
+
</SafeAreaProvider>
|
|
126
|
+
);
|
|
92
127
|
}
|
|
93
128
|
|
|
94
129
|
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
130
|
return (
|
|
106
131
|
<Tabs.Navigator
|
|
107
132
|
tabbarPosition="bottom"
|
|
108
|
-
tabbarStyle={
|
|
133
|
+
tabbarStyle={{ backgroundColor: "blue" }}
|
|
109
134
|
screens={[
|
|
110
135
|
{
|
|
111
136
|
key: "1",
|
|
@@ -147,17 +172,19 @@ function MyTab({
|
|
|
147
172
|
|
|
148
173
|
function MyScreen({
|
|
149
174
|
title,
|
|
150
|
-
|
|
175
|
+
children,
|
|
151
176
|
bg,
|
|
152
177
|
}: {
|
|
153
178
|
title: string;
|
|
154
|
-
|
|
179
|
+
children?: React.ReactNode;
|
|
155
180
|
bg?: string;
|
|
156
181
|
}) {
|
|
157
182
|
function pushScreen() {
|
|
158
183
|
navigation.pushScreen(
|
|
159
184
|
<Stack.Screen>
|
|
160
|
-
<MyScreen title="Pushed screen"
|
|
185
|
+
<MyScreen title="Pushed screen" bg={bg}>
|
|
186
|
+
<Button title="Pop screen" onPress={popScreen} />
|
|
187
|
+
</MyScreen>
|
|
161
188
|
</Stack.Screen>
|
|
162
189
|
);
|
|
163
190
|
}
|
|
@@ -177,7 +204,7 @@ function MyScreen({
|
|
|
177
204
|
>
|
|
178
205
|
<Text>{title}</Text>
|
|
179
206
|
<Button title="Push screen" onPress={pushScreen} />
|
|
180
|
-
{
|
|
207
|
+
{children}
|
|
181
208
|
</View>
|
|
182
209
|
);
|
|
183
210
|
}
|
|
@@ -187,8 +214,6 @@ function MyScreen({
|
|
|
187
214
|
|
|
188
215
|
Each tab can have its own stack by nesting the `Stack.Navigator` component.
|
|
189
216
|
|
|
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
217
|
```tsx
|
|
193
218
|
function MyTabs() {
|
|
194
219
|
return (
|
|
@@ -272,9 +297,76 @@ function switchMainTabsToTab(tabIndex: number) {
|
|
|
272
297
|
}
|
|
273
298
|
```
|
|
274
299
|
|
|
300
|
+
### Rendering a header
|
|
301
|
+
|
|
302
|
+
Use the `Stack.Header` component to render a native header in a screen.
|
|
303
|
+
|
|
304
|
+
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)
|
|
305
|
+
|
|
306
|
+
You can provide custom left, center, and right views in the header by using the `Stack.HeaderLeft`, `Stack.HeaderCenter`, and `Stack.HeaderRight` view container components as children of `Stack.Header`.
|
|
307
|
+
|
|
308
|
+
**Note:** Wrap your App in a `SafeAreaProvider` to ensure your screen components are rendered correctly with the header
|
|
309
|
+
|
|
310
|
+
**Note:**: The header component **has to be the first child** of a `Stack.Screen` component.
|
|
311
|
+
|
|
312
|
+
```tsx
|
|
313
|
+
import { navigation, Stack } from "@rn-tools/navigation";
|
|
314
|
+
import * as React from "react";
|
|
315
|
+
import { Button, View, TextInput, Text } from "react-native";
|
|
316
|
+
|
|
317
|
+
export function HeaderExample() {
|
|
318
|
+
return (
|
|
319
|
+
<View>
|
|
320
|
+
<Button
|
|
321
|
+
title="Push screen with header"
|
|
322
|
+
onPress={() => navigation.pushScreen(<MyScreenWithHeader />)}
|
|
323
|
+
/>
|
|
324
|
+
</View>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function MyScreenWithHeader() {
|
|
329
|
+
let [title, setTitle] = React.useState("");
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<Stack.Screen>
|
|
333
|
+
{/* Header must be the first child */}
|
|
334
|
+
<Stack.Header
|
|
335
|
+
title={title}
|
|
336
|
+
// Some potentially useful props - see the reference posted above for all available props
|
|
337
|
+
backTitle="Custom back title"
|
|
338
|
+
backTitleFontSize={16}
|
|
339
|
+
hideBackButton={false}
|
|
340
|
+
>
|
|
341
|
+
<Stack.HeaderRight>
|
|
342
|
+
<Text>Custom right text!</Text>
|
|
343
|
+
</Stack.HeaderRight>
|
|
344
|
+
</Stack.Header>
|
|
345
|
+
|
|
346
|
+
<View
|
|
347
|
+
style={{
|
|
348
|
+
flex: 1,
|
|
349
|
+
alignItems: "center",
|
|
350
|
+
paddingVertical: 48,
|
|
351
|
+
}}
|
|
352
|
+
>
|
|
353
|
+
<TextInput
|
|
354
|
+
style={{ fontSize: 26, fontWeight: "semibold" }}
|
|
355
|
+
value={title}
|
|
356
|
+
onChangeText={setTitle}
|
|
357
|
+
placeholder="Enter header text"
|
|
358
|
+
/>
|
|
359
|
+
</View>
|
|
360
|
+
</Stack.Screen>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
275
365
|
## Components
|
|
276
366
|
|
|
277
|
-
The `Navigator` components in the previous examples are
|
|
367
|
+
The `Navigator` components in the previous examples are fairly straightforward wrappers around other lower level `Stack` and `Tabs` components.
|
|
368
|
+
|
|
369
|
+
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
370
|
|
|
279
371
|
### Stack
|
|
280
372
|
|
|
@@ -311,30 +403,47 @@ export function StackNavigator({
|
|
|
311
403
|
- 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
404
|
- `Stack.Slot` - A slot for screens to be pushed into.
|
|
313
405
|
- This component is used to render screens that are pushed using `navigation.pushScreen` - don't forget to render this somewhere in `Stack.Screens`!
|
|
406
|
+
- `Stack.Header` - A header for a screen.
|
|
407
|
+
- **Must be rendered as the first child of a `Stack.Screen` component.**
|
|
408
|
+
- This is a `react-native-screens` StackHeader component under the hood.
|
|
409
|
+
- 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
410
|
|
|
315
411
|
## Tabs
|
|
316
412
|
|
|
317
413
|
This is the implementation of the exported `Tabs.Navigator` component:
|
|
318
414
|
|
|
319
415
|
```tsx
|
|
320
|
-
type
|
|
321
|
-
screens:
|
|
416
|
+
export type TabNavigatorProps = Omit<TabsRootProps, "children"> & {
|
|
417
|
+
screens: TabNavigatorScreenOptions[];
|
|
322
418
|
tabbarPosition?: "top" | "bottom";
|
|
323
419
|
tabbarStyle?: ViewProps["style"];
|
|
324
420
|
};
|
|
325
421
|
|
|
326
|
-
type
|
|
422
|
+
export type TabNavigatorScreenOptions = {
|
|
327
423
|
key: string;
|
|
328
|
-
screen: React.ReactElement<
|
|
424
|
+
screen: React.ReactElement<unknown>;
|
|
329
425
|
tab: (props: { isActive: boolean; onPress: () => void }) => React.ReactNode;
|
|
330
426
|
};
|
|
331
427
|
|
|
332
|
-
|
|
428
|
+
let TabNavigator = React.memo(function TabNavigator({
|
|
333
429
|
screens,
|
|
334
430
|
tabbarPosition = "bottom",
|
|
335
|
-
tabbarStyle,
|
|
431
|
+
tabbarStyle: tabbarStyleProp,
|
|
336
432
|
...rootProps
|
|
337
|
-
}:
|
|
433
|
+
}: TabNavigatorProps) {
|
|
434
|
+
let insets = useSafeAreaInsetsSafe();
|
|
435
|
+
|
|
436
|
+
let tabbarStyle = React.useMemo(() => {
|
|
437
|
+
return [
|
|
438
|
+
defaultTabbarStyle,
|
|
439
|
+
{
|
|
440
|
+
paddingBottom: tabbarPosition === "bottom" ? insets.bottom : 0,
|
|
441
|
+
paddingTop: tabbarPosition === "top" ? insets.top : 0,
|
|
442
|
+
},
|
|
443
|
+
tabbarStyleProp,
|
|
444
|
+
];
|
|
445
|
+
}, [tabbarPosition, tabbarStyleProp, insets]);
|
|
446
|
+
|
|
338
447
|
return (
|
|
339
448
|
<Tabs.Root {...rootProps}>
|
|
340
449
|
{tabbarPosition === "top" && (
|
|
@@ -360,7 +469,7 @@ export function Tabs.Navigator({
|
|
|
360
469
|
)}
|
|
361
470
|
</Tabs.Root>
|
|
362
471
|
);
|
|
363
|
-
}
|
|
472
|
+
});
|
|
364
473
|
```
|
|
365
474
|
|
|
366
475
|
- `Tabs.Root` - The root component for a tabs navigator.
|
|
@@ -419,4 +528,218 @@ let useUser = () => {
|
|
|
419
528
|
};
|
|
420
529
|
```
|
|
421
530
|
|
|
422
|
-
|
|
531
|
+
### Deep Links
|
|
532
|
+
|
|
533
|
+
This section will cover how to respond to deep links in your app. Deep links usually have some extra setup required - use Expo's [Deep Linking Guide](https://docs.expo.dev/guides/deep-linking/) to get started.
|
|
534
|
+
|
|
535
|
+
Once you are able to receive deep links, use the `DeepLinks` component exported from this library to handle them. In this example we will have a basic 3 tab view. We want to response to the link `home/items/:id` by navigating to the home tab and then pushing a detail screen with the corresponding id.
|
|
536
|
+
|
|
537
|
+
The deep link component takes an array of handlers which are functions that will be invoked when their `path` matches the deep link that was opened. The handler function will receive the params from the deep link - these use the same token syntax as libraries like `react-router` and `express` for path params.
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
```tsx
|
|
541
|
+
import { DeepLinks, navigation, Stack, Tabs } from "@rn-tools/navigation";
|
|
542
|
+
import * as React from "react";
|
|
543
|
+
import { View, Text, TouchableOpacity } from "react-native";
|
|
544
|
+
import * as Linking from "expo-linking";
|
|
545
|
+
|
|
546
|
+
export function DeepLinksExample() {
|
|
547
|
+
// You'll likely want to use Expo's Linking API to get the current URL and path
|
|
548
|
+
// let url = Linking.useURL()
|
|
549
|
+
// let { path } = Linking.parse(url)
|
|
550
|
+
|
|
551
|
+
// But it's easier to test hardcoded strings for the sake of this example
|
|
552
|
+
let path = "/testing/home/item/4";
|
|
553
|
+
|
|
554
|
+
return (
|
|
555
|
+
<DeepLinks
|
|
556
|
+
path={path}
|
|
557
|
+
handlers={[
|
|
558
|
+
{
|
|
559
|
+
path: "/testing/home/item/:itemId",
|
|
560
|
+
handler: (params: { itemId: string }) => {
|
|
561
|
+
let itemId = params.itemId;
|
|
562
|
+
|
|
563
|
+
// Go to home tab
|
|
564
|
+
navigation.setTabIndex(0);
|
|
565
|
+
|
|
566
|
+
// Push the screen we want
|
|
567
|
+
navigation.pushScreen(
|
|
568
|
+
<Stack.Screen>
|
|
569
|
+
<MyScreen title={`Item: ${itemId}`} />
|
|
570
|
+
</Stack.Screen>
|
|
571
|
+
);
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
]}
|
|
575
|
+
>
|
|
576
|
+
<MyTabs />
|
|
577
|
+
</DeepLinks>
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function MyTabs() {
|
|
582
|
+
return (
|
|
583
|
+
<Tabs.Navigator
|
|
584
|
+
tabbarPosition="bottom"
|
|
585
|
+
screens={[
|
|
586
|
+
{
|
|
587
|
+
key: "1",
|
|
588
|
+
screen: (
|
|
589
|
+
<Stack.Navigator
|
|
590
|
+
rootScreen={<MyScreen bg="red" title="Home screen" isRoot />}
|
|
591
|
+
/>
|
|
592
|
+
),
|
|
593
|
+
tab: ({ isActive }) => <MyTab text="Home" isActive={isActive} />,
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
key: "2",
|
|
597
|
+
screen: (
|
|
598
|
+
<Stack.Navigator
|
|
599
|
+
rootScreen={<MyScreen bg="blue" title="Search screen" isRoot />}
|
|
600
|
+
/>
|
|
601
|
+
),
|
|
602
|
+
tab: ({ isActive }) => <MyTab text="Search" isActive={isActive} />,
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
key: "3",
|
|
606
|
+
screen: (
|
|
607
|
+
<Stack.Navigator
|
|
608
|
+
rootScreen={
|
|
609
|
+
<MyScreen bg="purple" title="Settings screen" isRoot />
|
|
610
|
+
}
|
|
611
|
+
/>
|
|
612
|
+
),
|
|
613
|
+
tab: ({ isActive }) => <MyTab text="Settings" isActive={isActive} />,
|
|
614
|
+
},
|
|
615
|
+
]}
|
|
616
|
+
/>
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function MyTab({ isActive, text }: { isActive?: boolean; text: string }) {
|
|
621
|
+
return (
|
|
622
|
+
<View
|
|
623
|
+
style={{
|
|
624
|
+
padding: 16,
|
|
625
|
+
justifyContent: "center",
|
|
626
|
+
alignItems: "center",
|
|
627
|
+
}}
|
|
628
|
+
>
|
|
629
|
+
<Text style={{ fontSize: 12, fontWeight: isActive ? "bold" : "normal" }}>
|
|
630
|
+
{text}
|
|
631
|
+
</Text>
|
|
632
|
+
</View>
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function MyScreen({
|
|
637
|
+
bg = "white",
|
|
638
|
+
title = "",
|
|
639
|
+
isRoot = false,
|
|
640
|
+
}: {
|
|
641
|
+
title?: string;
|
|
642
|
+
bg?: string;
|
|
643
|
+
isRoot?: boolean;
|
|
644
|
+
}) {
|
|
645
|
+
return (
|
|
646
|
+
<View style={{ flex: 1, backgroundColor: bg }}>
|
|
647
|
+
<View className="flex-1 items-center justify-center gap-4">
|
|
648
|
+
<Text style={{ fontSize: 26, fontWeight: "semibold" }}>{title}</Text>
|
|
649
|
+
|
|
650
|
+
{!isRoot && (
|
|
651
|
+
<TouchableOpacity
|
|
652
|
+
onPress={() => {
|
|
653
|
+
navigation.popScreen();
|
|
654
|
+
}}
|
|
655
|
+
>
|
|
656
|
+
<Text>Pop</Text>
|
|
657
|
+
</TouchableOpacity>
|
|
658
|
+
)}
|
|
659
|
+
</View>
|
|
660
|
+
</View>
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Preventing going back
|
|
666
|
+
|
|
667
|
+
If you want to prevent users from popping a screen and potentially losing unsaved data, you can stop the screen from being dismissed by a gesture or pressing the back button.
|
|
668
|
+
|
|
669
|
+
**Note:**: The native header component does not provide a reliable way to prevent going back on iOS, so you'll have to provide your own custom back button by using the `Stack.HeaderLeft` component
|
|
670
|
+
|
|
671
|
+
```tsx
|
|
672
|
+
import { navigation, Stack } from "@rn-tools/navigation";
|
|
673
|
+
import * as React from "react";
|
|
674
|
+
import {
|
|
675
|
+
Text,
|
|
676
|
+
TextInput,
|
|
677
|
+
TouchableOpacity,
|
|
678
|
+
Button,
|
|
679
|
+
View,
|
|
680
|
+
Alert,
|
|
681
|
+
} from "react-native";
|
|
682
|
+
|
|
683
|
+
export function PreventGoingBack() {
|
|
684
|
+
return (
|
|
685
|
+
<Button
|
|
686
|
+
title="Push screen"
|
|
687
|
+
onPress={() => navigation.pushScreen(<MyScreen />)}
|
|
688
|
+
/>
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function MyScreen() {
|
|
693
|
+
let [input, setInput] = React.useState("");
|
|
694
|
+
|
|
695
|
+
let canGoBack = input.length === 0;
|
|
696
|
+
|
|
697
|
+
let onPressBackButton = React.useCallback(() => {
|
|
698
|
+
if (canGoBack) {
|
|
699
|
+
navigation.popScreen();
|
|
700
|
+
} else {
|
|
701
|
+
Alert.alert("Are you sure you want to go back?", "", [
|
|
702
|
+
{
|
|
703
|
+
text: "Cancel",
|
|
704
|
+
style: "cancel",
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
text: "Yes",
|
|
708
|
+
onPress: () => navigation.popScreen(),
|
|
709
|
+
},
|
|
710
|
+
]);
|
|
711
|
+
}
|
|
712
|
+
}, [canGoBack]);
|
|
713
|
+
|
|
714
|
+
return (
|
|
715
|
+
<Stack.Screen
|
|
716
|
+
preventNativeDismiss={!canGoBack}
|
|
717
|
+
nativeBackButtonDismissalEnabled={!canGoBack}
|
|
718
|
+
gestureEnabled={canGoBack}
|
|
719
|
+
>
|
|
720
|
+
<Stack.Header title="Prevent going back">
|
|
721
|
+
<Stack.HeaderLeft>
|
|
722
|
+
<TouchableOpacity
|
|
723
|
+
onPress={onPressBackButton}
|
|
724
|
+
style={{ opacity: canGoBack ? 1 : 0.4 }}
|
|
725
|
+
>
|
|
726
|
+
<Text>Back</Text>
|
|
727
|
+
</TouchableOpacity>
|
|
728
|
+
</Stack.HeaderLeft>
|
|
729
|
+
</Stack.Header>
|
|
730
|
+
<View style={{ paddingVertical: 48, paddingHorizontal: 16, gap: 16 }}>
|
|
731
|
+
<Text style={{ fontSize: 22, fontWeight: "medium" }}>
|
|
732
|
+
Enter some text and try to go back
|
|
733
|
+
</Text>
|
|
734
|
+
<TextInput
|
|
735
|
+
value={input}
|
|
736
|
+
onChangeText={setInput}
|
|
737
|
+
placeholder="Enter some text"
|
|
738
|
+
onSubmitEditing={() => setInput("")}
|
|
739
|
+
/>
|
|
740
|
+
<Button title="Submit" onPress={() => setInput("")} />
|
|
741
|
+
</View>
|
|
742
|
+
</Stack.Screen>
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rn-tools/navigation",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"main": "./src/index.ts",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -30,9 +30,11 @@
|
|
|
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": {
|
|
37
|
+
"path-to-regexp": "^6.2.2",
|
|
36
38
|
"zustand": "^4.5.2"
|
|
37
39
|
},
|
|
38
40
|
"jest": {
|
package/src/contexts.tsx
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { match } from "path-to-regexp";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export type DeepLinkHandler<T> = {
|
|
5
|
+
path: string;
|
|
6
|
+
handler: (params: T) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function buildMatchers<T>(handlers: DeepLinkHandler<T>[]) {
|
|
10
|
+
return handlers.map(({ path, handler }) => {
|
|
11
|
+
let fn = match(path, { decode: decodeURIComponent });
|
|
12
|
+
return { fn, handler };
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type DeepLinksProps<T> = {
|
|
17
|
+
path: string;
|
|
18
|
+
handlers: DeepLinkHandler<T>[];
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function DeepLinks<T>({ path, handlers, children }: DeepLinksProps<T>) {
|
|
23
|
+
let matchers = React.useRef(buildMatchers(handlers));
|
|
24
|
+
|
|
25
|
+
React.useLayoutEffect(() => {
|
|
26
|
+
matchers.current = buildMatchers(handlers);
|
|
27
|
+
}, [handlers]);
|
|
28
|
+
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
matchers.current.forEach(({ fn, handler }) => {
|
|
31
|
+
let match = fn(path);
|
|
32
|
+
if (match) {
|
|
33
|
+
return setImmediate(() => handler(match.params as T));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}, [path]);
|
|
37
|
+
|
|
38
|
+
return <>{children}</>;
|
|
39
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -103,6 +103,12 @@ type SetTabIndexAction = {
|
|
|
103
103
|
tabId: string;
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
type PopActiveTabAction = {
|
|
107
|
+
type: "POP_ACTIVE_TAB";
|
|
108
|
+
tabId: string;
|
|
109
|
+
index: number;
|
|
110
|
+
};
|
|
111
|
+
|
|
106
112
|
type RegisterTabAction = {
|
|
107
113
|
type: "REGISTER_TAB";
|
|
108
114
|
depth: number;
|
|
@@ -126,7 +132,8 @@ type TabActions =
|
|
|
126
132
|
| SetTabIndexAction
|
|
127
133
|
| RegisterTabAction
|
|
128
134
|
| UnregisterTabAction
|
|
129
|
-
| TabBackAction
|
|
135
|
+
| TabBackAction
|
|
136
|
+
| PopActiveTabAction;
|
|
130
137
|
|
|
131
138
|
type SetDebugModeAction = {
|
|
132
139
|
type: "SET_DEBUG_MODE";
|
|
@@ -365,37 +372,45 @@ export function reducer(
|
|
|
365
372
|
}
|
|
366
373
|
|
|
367
374
|
let nextState: NavigationState = Object.assign({}, state);
|
|
375
|
+
let currentIndex = tab.activeIndex;
|
|
368
376
|
nextState.tabs.lookup[tabId] = Object.assign(
|
|
369
377
|
{},
|
|
370
378
|
{
|
|
371
379
|
...nextState.tabs.lookup[tabId],
|
|
372
380
|
activeIndex: index,
|
|
373
|
-
history: tab.history.filter((i) => i !== index).concat(
|
|
381
|
+
history: tab.history.filter((i) => i !== index).concat(currentIndex),
|
|
374
382
|
}
|
|
375
383
|
);
|
|
376
384
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
385
|
+
return nextState;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
case "POP_ACTIVE_TAB": {
|
|
389
|
+
let { tabId, index } = action;
|
|
390
|
+
let { renderCharts } = context;
|
|
391
|
+
|
|
392
|
+
let tabKey = serializeTabIndexKey(tabId, index);
|
|
393
|
+
let stackIds = renderCharts.stacksByTabIndex[tabKey];
|
|
380
394
|
|
|
381
|
-
|
|
382
|
-
stackIds.forEach((stackId) => {
|
|
383
|
-
let stack = nextState.stacks.lookup[stackId];
|
|
384
|
-
let screenIdsToRemove = stack.screens;
|
|
395
|
+
let nextState: NavigationState = Object.assign({}, state);
|
|
385
396
|
|
|
386
|
-
|
|
397
|
+
if (stackIds?.length > 0) {
|
|
398
|
+
stackIds.forEach((stackId) => {
|
|
399
|
+
let stack = nextState.stacks.lookup[stackId];
|
|
400
|
+
let screenIdsToRemove = stack.screens;
|
|
387
401
|
|
|
388
|
-
|
|
389
|
-
delete nextScreensLookup[id];
|
|
390
|
-
});
|
|
402
|
+
let nextScreensLookup = Object.assign({}, nextState.screens.lookup);
|
|
391
403
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
(id) => !screenIdsToRemove.includes(id)
|
|
395
|
-
);
|
|
396
|
-
nextState.screens.lookup = nextScreensLookup;
|
|
404
|
+
screenIdsToRemove.forEach((id) => {
|
|
405
|
+
delete nextScreensLookup[id];
|
|
397
406
|
});
|
|
398
|
-
|
|
407
|
+
|
|
408
|
+
nextState.stacks.lookup[stackId].screens = [];
|
|
409
|
+
nextState.screens.ids = nextState.screens.ids.filter(
|
|
410
|
+
(id) => !screenIdsToRemove.includes(id)
|
|
411
|
+
);
|
|
412
|
+
nextState.screens.lookup = nextScreensLookup;
|
|
413
|
+
});
|
|
399
414
|
}
|
|
400
415
|
|
|
401
416
|
return nextState;
|
|
@@ -442,10 +457,15 @@ export function reducer(
|
|
|
442
457
|
let nextState: NavigationState = Object.assign({}, state);
|
|
443
458
|
|
|
444
459
|
let tab = nextState.tabs.lookup[tabId];
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
460
|
+
|
|
461
|
+
let lastActiveIndex = tab.history[tab.history.length - 1];
|
|
462
|
+
|
|
463
|
+
nextState.tabs.lookup = Object.assign({}, nextState.tabs.lookup, {
|
|
464
|
+
[tabId]: Object.assign({}, nextState.tabs.lookup[tabId], {
|
|
465
|
+
activeIndex: lastActiveIndex,
|
|
466
|
+
history: tab.history.filter((i) => i !== lastActiveIndex),
|
|
467
|
+
}),
|
|
468
|
+
});
|
|
449
469
|
|
|
450
470
|
return nextState;
|
|
451
471
|
}
|
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
|
@@ -13,7 +13,8 @@ import type { PushScreenOptions } from "./types";
|
|
|
13
13
|
/**
|
|
14
14
|
* Ideas:
|
|
15
15
|
* - lifecycles / screen tracking
|
|
16
|
-
* - testing
|
|
16
|
+
* - testing guide
|
|
17
|
+
* - routing example -> fragments to ids
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
export function createNavigation() {
|
package/src/stack.tsx
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
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 ImageProps,
|
|
10
|
+
type LayoutRectangle,
|
|
11
|
+
type ViewProps,
|
|
12
|
+
type ViewStyle,
|
|
13
|
+
} from "react-native";
|
|
3
14
|
import {
|
|
4
15
|
ScreenStackProps as RNScreenStackProps,
|
|
5
16
|
Screen as RNScreen,
|
|
6
17
|
ScreenProps as RNScreenProps,
|
|
7
18
|
ScreenStackHeaderConfig as RNScreenStackHeaderConfig,
|
|
19
|
+
ScreenStackHeaderLeftView as RNScreenStackHeaderLeftView,
|
|
20
|
+
ScreenStackHeaderRightView as RNScreenStackHeaderRightView,
|
|
21
|
+
ScreenStackHeaderCenterView as RNScreenStackHeaderCenterView,
|
|
8
22
|
ScreenStackHeaderConfigProps as RNScreenStackHeaderConfigProps,
|
|
23
|
+
ScreenStackHeaderBackButtonImage as RNScreenStackHeaderBackButtonImage,
|
|
9
24
|
} from "react-native-screens";
|
|
10
25
|
import ScreenStackNativeComponent from "react-native-screens/src/fabric/ScreenStackNativeComponent";
|
|
11
26
|
|
|
@@ -18,16 +33,18 @@ import {
|
|
|
18
33
|
import { DEFAULT_SLOT_NAME } from "./navigation-reducer";
|
|
19
34
|
import { useNavigationDispatch, useNavigationState } from "./navigation-store";
|
|
20
35
|
import type { StackItem } from "./types";
|
|
21
|
-
import { generateStackId } from "./utils";
|
|
36
|
+
import { generateStackId, useSafeAreaInsetsSafe } from "./utils";
|
|
22
37
|
|
|
23
38
|
let StackIdContext = React.createContext<string>("");
|
|
24
39
|
let ScreenIdContext = React.createContext<string>("");
|
|
25
40
|
|
|
26
|
-
|
|
41
|
+
// Component returned from `react-native-screens` references `react-navigation` data structures in recent updates
|
|
42
|
+
// This is a workaround to make it work with our custom navigation
|
|
43
|
+
let RNScreenStack = React.memo(function RNScreenStack(
|
|
27
44
|
props: RNScreenStackProps
|
|
28
45
|
) {
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
let { children, gestureDetectorBridge, ...rest } = props;
|
|
47
|
+
let ref = React.useRef(null);
|
|
31
48
|
|
|
32
49
|
React.useEffect(() => {
|
|
33
50
|
if (gestureDetectorBridge) {
|
|
@@ -112,18 +129,13 @@ function StackRoot({ children, id }: StackRootProps) {
|
|
|
112
129
|
);
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
function StackScreens({
|
|
116
|
-
style: styleProp,
|
|
117
|
-
...props
|
|
118
|
-
}: RNScreenStackProps) {
|
|
132
|
+
function StackScreens({ style: styleProp, ...props }: RNScreenStackProps) {
|
|
119
133
|
let style = React.useMemo(
|
|
120
134
|
() => styleProp || StyleSheet.absoluteFill,
|
|
121
135
|
[styleProp]
|
|
122
136
|
);
|
|
123
137
|
|
|
124
|
-
return
|
|
125
|
-
<RNScreenStack {...props} style={style} />
|
|
126
|
-
);
|
|
138
|
+
return <RNScreenStack {...props} style={style} />;
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
let defaultScreenStyle: ViewStyle = {
|
|
@@ -180,7 +192,7 @@ let StackScreen = React.memo(function StackScreen({
|
|
|
180
192
|
<RNScreen
|
|
181
193
|
{...props}
|
|
182
194
|
style={style}
|
|
183
|
-
|
|
195
|
+
activityState={isActive ? 2 : 0}
|
|
184
196
|
gestureEnabled={gestureEnabled}
|
|
185
197
|
onDismissed={onDismissed}
|
|
186
198
|
>
|
|
@@ -224,9 +236,49 @@ let StackSlot = React.memo(function StackSlot({
|
|
|
224
236
|
let StackScreenHeader = React.memo(function StackScreenHeader({
|
|
225
237
|
...props
|
|
226
238
|
}: RNScreenStackHeaderConfigProps) {
|
|
227
|
-
|
|
239
|
+
let layout = useWindowDimensions();
|
|
240
|
+
let insets = useSafeAreaInsetsSafe();
|
|
241
|
+
|
|
242
|
+
let headerHeight = React.useMemo(() => {
|
|
243
|
+
if (Platform.OS === "android") {
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return getDefaultHeaderHeight(layout, false, insets.top);
|
|
248
|
+
}, [layout, insets]);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<React.Fragment>
|
|
252
|
+
<RNScreenStackHeaderConfig {...props} />
|
|
253
|
+
<View style={{ height: headerHeight }} />
|
|
254
|
+
</React.Fragment>
|
|
255
|
+
);
|
|
228
256
|
});
|
|
229
257
|
|
|
258
|
+
let StackScreenHeaderLeft = React.memo(function StackScreenHeaderLeft({
|
|
259
|
+
...props
|
|
260
|
+
}: ViewProps) {
|
|
261
|
+
return <RNScreenStackHeaderLeftView {...props} />;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
let StackScreenHeaderCenter = React.memo(function StackScreenHeaderCenter({
|
|
265
|
+
...props
|
|
266
|
+
}: ViewProps) {
|
|
267
|
+
return <RNScreenStackHeaderCenterView {...props} />;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
let StackScreenHeaderRight = React.memo(function StackScreenHeaderRight({
|
|
271
|
+
...props
|
|
272
|
+
}: ViewProps) {
|
|
273
|
+
return <RNScreenStackHeaderRightView {...props} />;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
let ScreenStackHeaderBackButtonImage = React.memo(
|
|
277
|
+
function ScreenStackHeaderBackButtonImage(props: ImageProps) {
|
|
278
|
+
return <RNScreenStackHeaderBackButtonImage {...props} />;
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
|
|
230
282
|
type StackNavigatorProps = Omit<StackRootProps, "children"> & {
|
|
231
283
|
rootScreen: React.ReactElement<unknown>;
|
|
232
284
|
};
|
|
@@ -250,6 +302,54 @@ export let Stack = {
|
|
|
250
302
|
Screens: StackScreens,
|
|
251
303
|
Screen: StackScreen,
|
|
252
304
|
Header: StackScreenHeader,
|
|
305
|
+
HeaderLeft: StackScreenHeaderLeft,
|
|
306
|
+
HeaderCenter: StackScreenHeaderCenter,
|
|
307
|
+
HeaderRight: StackScreenHeaderRight,
|
|
308
|
+
HeaderBackImage: ScreenStackHeaderBackButtonImage,
|
|
253
309
|
Slot: StackSlot,
|
|
254
310
|
Navigator: StackNavigator,
|
|
255
311
|
};
|
|
312
|
+
|
|
313
|
+
// `onLayout` event does not return a value for the native header component
|
|
314
|
+
// This function is copied from react-navigation to get the default header heights
|
|
315
|
+
// Ref: https://github.com/react-navigation/react-navigation/blob/main/packages/elements/src/Header/getDefaultHeaderHeight.tsx#L5
|
|
316
|
+
function getDefaultHeaderHeight(
|
|
317
|
+
layout: Pick<LayoutRectangle, "width" | "height">,
|
|
318
|
+
// TODO - handle modal headers and substacks
|
|
319
|
+
modalPresentation: boolean,
|
|
320
|
+
topInset: number
|
|
321
|
+
): number {
|
|
322
|
+
let headerHeight;
|
|
323
|
+
|
|
324
|
+
// On models with Dynamic Island the status bar height is smaller than the safe area top inset.
|
|
325
|
+
let hasDynamicIsland = Platform.OS === "ios" && topInset > 50;
|
|
326
|
+
let statusBarHeight = hasDynamicIsland
|
|
327
|
+
? topInset - (5 + 1 / PixelRatio.get())
|
|
328
|
+
: topInset;
|
|
329
|
+
|
|
330
|
+
let isLandscape = layout.width > layout.height;
|
|
331
|
+
|
|
332
|
+
if (Platform.OS === "ios") {
|
|
333
|
+
if (Platform.isPad || Platform.isTV) {
|
|
334
|
+
if (modalPresentation) {
|
|
335
|
+
headerHeight = 56;
|
|
336
|
+
} else {
|
|
337
|
+
headerHeight = 50;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
if (isLandscape) {
|
|
341
|
+
headerHeight = 32;
|
|
342
|
+
} else {
|
|
343
|
+
if (modalPresentation) {
|
|
344
|
+
headerHeight = 56;
|
|
345
|
+
} else {
|
|
346
|
+
headerHeight = 44;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
headerHeight = 64;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return headerHeight + statusBarHeight;
|
|
355
|
+
}
|
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,
|
|
@@ -217,7 +225,11 @@ let TabsTab = React.memo(function TabsTab({
|
|
|
217
225
|
|
|
218
226
|
let onPress: () => void = React.useCallback(() => {
|
|
219
227
|
dispatch({ type: "SET_TAB_INDEX", tabId, index });
|
|
220
|
-
|
|
228
|
+
|
|
229
|
+
if (isActive) {
|
|
230
|
+
dispatch({ type: "POP_ACTIVE_TAB", tabId, index });
|
|
231
|
+
}
|
|
232
|
+
}, [tabId, index, dispatch, isActive]);
|
|
221
233
|
|
|
222
234
|
let style = React.useMemo(() => {
|
|
223
235
|
let baseStyle = props.style || defaultTabStyle;
|
|
@@ -242,12 +254,37 @@ let TabsTab = React.memo(function TabsTab({
|
|
|
242
254
|
});
|
|
243
255
|
|
|
244
256
|
|
|
257
|
+
export type TabNavigatorProps = Omit<TabsRootProps, "children"> & {
|
|
258
|
+
screens: TabNavigatorScreenOptions[];
|
|
259
|
+
tabbarPosition?: "top" | "bottom";
|
|
260
|
+
tabbarStyle?: ViewProps["style"];
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export type TabNavigatorScreenOptions = {
|
|
264
|
+
key: string;
|
|
265
|
+
screen: React.ReactElement<unknown>;
|
|
266
|
+
tab: (props: { isActive: boolean; onPress: () => void }) => React.ReactNode;
|
|
267
|
+
};
|
|
268
|
+
|
|
245
269
|
let TabNavigator = React.memo(function TabNavigator({
|
|
246
270
|
screens,
|
|
247
271
|
tabbarPosition = "bottom",
|
|
248
|
-
tabbarStyle,
|
|
272
|
+
tabbarStyle: tabbarStyleProp,
|
|
249
273
|
...rootProps
|
|
250
274
|
}: TabNavigatorProps) {
|
|
275
|
+
let insets = useSafeAreaInsetsSafe();
|
|
276
|
+
|
|
277
|
+
let tabbarStyle = React.useMemo(() => {
|
|
278
|
+
return [
|
|
279
|
+
defaultTabbarStyle,
|
|
280
|
+
{
|
|
281
|
+
paddingBottom: tabbarPosition === "bottom" ? insets.bottom : 0,
|
|
282
|
+
paddingTop: tabbarPosition === "top" ? insets.top : 0,
|
|
283
|
+
},
|
|
284
|
+
tabbarStyleProp,
|
|
285
|
+
];
|
|
286
|
+
}, [tabbarPosition, tabbarStyleProp, insets]);
|
|
287
|
+
|
|
251
288
|
return (
|
|
252
289
|
<Tabs.Root {...rootProps}>
|
|
253
290
|
{tabbarPosition === "top" && (
|
|
@@ -283,15 +320,3 @@ export let Tabs = {
|
|
|
283
320
|
Tab: TabsTab,
|
|
284
321
|
Navigator: TabNavigator,
|
|
285
322
|
};
|
|
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
|
+
}
|