@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 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`. Designed with flexibility in mind.
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. The [Guides](#guides) section covers how to use lower-level `Stack` and `Tabs` components in a variety of navigation patterns.
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 stacks of screens. Under the hood this is using `react-native-screens` to handle pushing and popping natively.
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
- showPopButton = false,
65
+ children,
37
66
  }: {
38
67
  title: string;
39
- showPopButton?: boolean;
68
+ children?: React.ReactNode;
40
69
  }) {
41
70
  function pushScreen() {
42
71
  navigation.pushScreen(
43
72
  <Stack.Screen>
44
- <MyScreen title="Pushed screen" showPopButton />
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
- {showPopButton && <Button title="Pop screen" onPress={popScreen} />}
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` component. Create a wrapper to simplify your usage if you'd like:
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 the tab switching natively. The active tab can be changed via the `navigation.setTabIndex` method, however the build in tabbar already handles switching between screens.
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
- import { useSafeAreaInsets } from "react-native-safe-area-context";
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 <Stack.Navigator rootScreen={<MyTabs />} />;
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={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
- showPopButton = false,
173
+ children,
151
174
  bg,
152
175
  }: {
153
176
  title: string;
154
- showPopButton?: boolean;
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" showPopButton />
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
- {showPopButton && <Button title="Pop screen" onPress={popScreen} />}
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 convenience wrappers around other lower level `Stack` and `Tabs` components. This section will briefly cover each of the underlying components so that you can build your own wrappers if needed
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 TabsNavigatorProps = Omit<TabsRootProps, "children"> & {
321
- screens: Tabs.NavigatorScreenOptions[];
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 TabsNavigatorScreenOptions = {
414
+ export type TabNavigatorScreenOptions = {
327
415
  key: string;
328
- screen: React.ReactElement<ScreenProps>;
416
+ screen: React.ReactElement<unknown>;
329
417
  tab: (props: { isActive: boolean; onPress: () => void }) => React.ReactNode;
330
418
  };
331
419
 
332
- export function Tabs.Navigator({
420
+ let TabNavigator = React.memo(function TabNavigator({
333
421
  screens,
334
422
  tabbarPosition = "bottom",
335
- tabbarStyle,
423
+ tabbarStyle: tabbarStyleProp,
336
424
  ...rootProps
337
- }: TabsNavigatorProps) {
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.0.0",
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
@@ -1,4 +1,4 @@
1
- import * as React from 'react'
1
+ import * as React from "react";
2
2
 
3
3
  export let StackIdContext = React.createContext<string>("");
4
4
  export let ScreenIdContext = React.createContext<string>("");
@@ -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(index),
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 = nextState.tabs.lookup[tabId];
445
- let last = tab.history.pop();
446
- if (last != null) {
447
- tab.activeIndex = last;
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
  }
@@ -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
+ }
@@ -12,6 +12,7 @@ import type { PushScreenOptions } from "./types";
12
12
 
13
13
  /**
14
14
  * Ideas:
15
+ * - pull in safe area provider and use as default props to tabs
15
16
  * - lifecycles / screen tracking
16
17
  * - testing - internal and jest plugin
17
18
  */
package/src/stack.tsx CHANGED
@@ -1,5 +1,14 @@
1
1
  import * as React from "react";
2
- import { BackHandler, StyleSheet, type ViewStyle } from "react-native";
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
- const RNScreenStack = React.memo(function RNScreenStack(
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
- const { children, gestureDetectorBridge, ...rest } = props;
30
- const ref = React.useRef(null);
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
- // activityState={isActive ? 2 : 0}
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
- return <RNScreenStackHeaderConfig {...props} />;
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 { useNavigationDispatch, useNavigationState } from "./navigation-store";
25
- import { generateTabId } from "./utils";
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, tabs]);
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
+ }