@rn-tools/navigation 2.2.4 → 2.2.6

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
@@ -13,6 +13,7 @@ A set of useful navigation components for React Native. Built with `react-native
13
13
  - [Pushing a screen once](#pushing-a-screen-once)
14
14
  - [Targeting specific tabs](#targeting-specific-tabs)
15
15
  - [Rendering a header](#rendering-a-header)
16
+ - [Configuring screen props](#configuring-screen-props)
16
17
  - [Components](#components)
17
18
  - [Stack](#stack)
18
19
  - [Tabs](#tabs)
@@ -20,6 +21,7 @@ A set of useful navigation components for React Native. Built with `react-native
20
21
  - [Authentication](#authentication)
21
22
  - [Deep Links](#deep-links)
22
23
  - [Preventing going back](#preventing-going-back)
24
+ - [Testing](#testing)
23
25
 
24
26
  ## Installation
25
27
 
@@ -299,16 +301,12 @@ function switchMainTabsToTab(tabIndex: number) {
299
301
 
300
302
  ### Rendering a header
301
303
 
302
- Use the `Stack.Header` component to render a native header in a screen.
304
+ Use the `Stack.Header` component to render a native header in a screen by passing it as a prop to `Stack.Screen`.
303
305
 
304
306
  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
307
 
306
308
  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
309
 
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
310
  ```tsx
313
311
  import { navigation, Stack } from "@rn-tools/navigation";
314
312
  import * as React from "react";
@@ -329,20 +327,20 @@ function MyScreenWithHeader() {
329
327
  let [title, setTitle] = React.useState("");
330
328
 
331
329
  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
-
330
+ <Stack.Screen
331
+ header={
332
+ <Stack.Header
333
+ title={title}
334
+ backTitle="Custom back title"
335
+ backTitleFontSize={16}
336
+ hideBackButton={false}
337
+ >
338
+ <Stack.HeaderRight>
339
+ <Text>Custom right text!</Text>
340
+ </Stack.HeaderRight>
341
+ </Stack.Header>
342
+ }
343
+ >
346
344
  <View
347
345
  style={{
348
346
  flex: 1,
@@ -362,6 +360,27 @@ function MyScreenWithHeader() {
362
360
  }
363
361
  ```
364
362
 
363
+ ### Configuring screen props
364
+
365
+ The `Stack.Screen` component is a wrapper around the `Screen` component from `react-native-screens`. [Screen props reference](https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx#L101)
366
+
367
+ Some notable props are `stackPresentation`, `stackAnimation`, and `gestureEnabled`, however there are many more available.
368
+
369
+ ```tsx
370
+ function pushNativeModalScreen() {
371
+ navigation.pushScreen(
372
+ <Stack.Screen
373
+ stackPresentation="modal"
374
+ stackAnimation="slide_from_bottom"
375
+ gestureEnabled={true}
376
+ >
377
+ {/* If you want to push more screens inside of the modal, wrap it with a Stack */}
378
+ <Stack.Navigator rootScreen={<MyScreen title="Modal screen" />} />
379
+ </Stack.Screen>
380
+ );
381
+ }
382
+ ```
383
+
365
384
  ## Components
366
385
 
367
386
  The `Navigator` components in the previous examples are fairly straightforward wrappers around other lower level `Stack` and `Tabs` components.
@@ -550,7 +569,7 @@ export default function DeepLinksExample() {
550
569
  // let { path } = Linking.parse(url)
551
570
 
552
571
  // But it's easier to test hardcoded strings for the sake of this example
553
- let path = "/testing/home/item/2";
572
+ let path = "/home/item/2";
554
573
 
555
574
  return (
556
575
  <Stack.Navigator
@@ -559,23 +578,7 @@ export default function DeepLinksExample() {
559
578
  path={path}
560
579
  handlers={[
561
580
  {
562
- path: "/testing/home/item1/:itemId",
563
- handler: (params: { itemId: string }) => {
564
- let itemId = params.itemId;
565
-
566
- // Go to home tab
567
- navigation.setTabIndex(0);
568
-
569
- // Push the screen we want
570
- navigation.pushScreen(
571
- <Stack.Screen>
572
- <MyScreen title={`Item: ${itemId}`} />
573
- </Stack.Screen>
574
- );
575
- },
576
- },
577
- {
578
- path: "/testing/home/item/:itemId",
581
+ path: "/home/item/:itemId",
579
582
  handler: (params: { itemId: string }) => {
580
583
  let itemId = params.itemId;
581
584
 
@@ -688,7 +691,6 @@ function MyScreen({
688
691
  </View>
689
692
  );
690
693
  }
691
-
692
694
  ```
693
695
 
694
696
  ### Preventing going back
@@ -745,17 +747,19 @@ function MyScreen() {
745
747
  preventNativeDismiss={!canGoBack}
746
748
  nativeBackButtonDismissalEnabled={!canGoBack}
747
749
  gestureEnabled={canGoBack}
750
+ header={
751
+ <Stack.Header title="Prevent going back">
752
+ <Stack.HeaderLeft>
753
+ <TouchableOpacity
754
+ onPress={onPressBackButton}
755
+ style={{ opacity: canGoBack ? 1 : 0.4 }}
756
+ >
757
+ <Text>Back</Text>
758
+ </TouchableOpacity>
759
+ </Stack.HeaderLeft>
760
+ </Stack.Header>
761
+ }
748
762
  >
749
- <Stack.Header title="Prevent going back">
750
- <Stack.HeaderLeft>
751
- <TouchableOpacity
752
- onPress={onPressBackButton}
753
- style={{ opacity: canGoBack ? 1 : 0.4 }}
754
- >
755
- <Text>Back</Text>
756
- </TouchableOpacity>
757
- </Stack.HeaderLeft>
758
- </Stack.Header>
759
763
  <View style={{ paddingVertical: 48, paddingHorizontal: 16, gap: 16 }}>
760
764
  <Text style={{ fontSize: 22, fontWeight: "medium" }}>
761
765
  Enter some text and try to go back
@@ -772,3 +776,13 @@ function MyScreen() {
772
776
  );
773
777
  }
774
778
  ```
779
+
780
+ ### Testing
781
+
782
+ Recommended:
783
+
784
+ - Check out the getting started portion of [React Native Testing Library](https://callstack.github.io/react-native-testing-library/docs/start/quick-start)
785
+
786
+ - Set up [Jest Expo](https://docs.expo.dev/develop/unit-testing/)
787
+
788
+ - Reset the navigation state between each test by calling `navigation.reset()`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rn-tools/navigation",
3
- "version": "2.2.4",
3
+ "version": "2.2.6",
4
4
  "main": "./src/index.ts",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -13,9 +13,11 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "@rn-tools/eslint-config": "*",
16
+ "@testing-library/react-native": "^12.5.1",
16
17
  "@types/jest": "^27.0.3",
17
18
  "@types/react": "~18.2.79",
18
19
  "@types/react-native": "^0.72.8",
20
+ "@types/react-test-renderer": "^18",
19
21
  "@typescript-eslint/eslint-plugin": "^6.8.0",
20
22
  "@typescript-eslint/parser": "^6.8.0",
21
23
  "babel-preset-expo": "~11.0.0",
@@ -25,6 +27,7 @@
25
27
  "jest-expo": "^51.0.2",
26
28
  "lint-staged": "^15.2.0",
27
29
  "prettier": "^3.2.1",
30
+ "react-test-renderer": "18.2.0",
28
31
  "typescript": "~5.3.3"
29
32
  },
30
33
  "peerDependencies": {
@@ -218,8 +218,6 @@ describe("reducer", () => {
218
218
  };
219
219
 
220
220
  let newState = reducer(initialStateWithStack, action, context);
221
- console.log(newState);
222
-
223
221
  expect(newState.screens.ids).not.toContain("screen1");
224
222
  expect(newState.stacks.lookup.stack1.screens).not.toContain("screen1");
225
223
  expect(newState.screens.ids).toContain("screen2");
@@ -0,0 +1,48 @@
1
+ import { fireEvent, render } from "@testing-library/react-native";
2
+ import { Button, Text } from "react-native";
3
+
4
+ import { navigation } from "../navigation";
5
+ import { Stack } from "../stack";
6
+
7
+ describe("<Stack />", () => {
8
+ beforeEach(() => {
9
+ navigation.reset();
10
+ });
11
+
12
+ test("render", () => {
13
+ let { getByText } = render(
14
+ <Stack.Navigator
15
+ rootScreen={
16
+ <>
17
+ <Text>Stack works!</Text>
18
+ </>
19
+ }
20
+ />
21
+ );
22
+ let element = getByText("Stack works!");
23
+ expect(element).toBeTruthy();
24
+ });
25
+
26
+ test("pushing a screen works", () => {
27
+ let { getByText, queryByText } = render(
28
+ <Stack.Navigator
29
+ rootScreen={
30
+ <Button
31
+ title="Push"
32
+ onPress={() =>
33
+ navigation.pushScreen(
34
+ <Stack.Screen>
35
+ <Text>Pushed screen!</Text>
36
+ </Stack.Screen>
37
+ )
38
+ }
39
+ />
40
+ }
41
+ />
42
+ );
43
+
44
+ expect(queryByText("Pushed screen")).toBe(null);
45
+ fireEvent.press(getByText("Push"));
46
+ getByText("Pushed screen!");
47
+ });
48
+ });
@@ -13,8 +13,6 @@ import type { PushScreenOptions } from "./types";
13
13
  /**
14
14
  * Ideas:
15
15
  * - lifecycles / screen tracking
16
- * - testing guide
17
- * - routing example -> fragments to ids
18
16
  */
19
17
 
20
18
  export function createNavigation() {
@@ -66,6 +64,15 @@ function getNavigationFns({ store, dispatch, renderCharts }: NavigationStore) {
66
64
  0
67
65
  );
68
66
  let tabIds = renderCharts.tabsByDepth[maxDepth];
67
+
68
+ if (!tabIds || tabIds?.length === 0) {
69
+ if (store.getState().debugModeEnabled) {
70
+ console.warn("No focused tabs found");
71
+ }
72
+
73
+ return;
74
+ }
75
+
69
76
  let topTabId = tabIds[tabIds.length - 1];
70
77
  return topTabId;
71
78
  }
package/src/stack.tsx CHANGED
@@ -1,13 +1,10 @@
1
1
  import * as React from "react";
2
2
  import {
3
3
  BackHandler,
4
- PixelRatio,
5
4
  Platform,
6
5
  StyleSheet,
7
- useWindowDimensions,
8
6
  View,
9
7
  type ImageProps,
10
- type LayoutRectangle,
11
8
  type ViewProps,
12
9
  type ViewStyle,
13
10
  } from "react-native";
@@ -21,6 +18,7 @@ import {
21
18
  ScreenStackHeaderCenterView as RNScreenStackHeaderCenterView,
22
19
  ScreenStackHeaderConfigProps as RNScreenStackHeaderConfigProps,
23
20
  ScreenStackHeaderBackButtonImage as RNScreenStackHeaderBackButtonImage,
21
+ type ScreenProps,
24
22
  } from "react-native-screens";
25
23
  import ScreenStackNativeComponent from "react-native-screens/src/fabric/ScreenStackNativeComponent";
26
24
 
@@ -59,7 +57,7 @@ let RNScreenStack = React.memo(function RNScreenStack(
59
57
  );
60
58
  });
61
59
 
62
- type StackRootProps = {
60
+ export type StackRootProps = {
63
61
  children: React.ReactNode;
64
62
  id?: string;
65
63
  };
@@ -129,7 +127,9 @@ function StackRoot({ children, id }: StackRootProps) {
129
127
  );
130
128
  }
131
129
 
132
- function StackScreens({ style: styleProp, ...props }: RNScreenStackProps) {
130
+ export type StackScreensProps = RNScreenStackProps;
131
+
132
+ function StackScreens({ style: styleProp, ...props }: StackScreensProps) {
133
133
  let style = React.useMemo(
134
134
  () => styleProp || StyleSheet.absoluteFill,
135
135
  [styleProp]
@@ -143,13 +143,19 @@ let defaultScreenStyle: ViewStyle = {
143
143
  backgroundColor: "white",
144
144
  };
145
145
 
146
- export type StackScreenProps = RNScreenProps;
146
+ export type StackScreenProps = RNScreenProps & {
147
+ header?: React.ReactElement<StackScreenHeaderProps>
148
+ }
149
+
150
+ let HeaderHeightContext = React.createContext<number>(0);
147
151
 
148
152
  let StackScreen = React.memo(function StackScreen({
149
153
  children,
150
154
  style: styleProp,
151
155
  gestureEnabled = true,
152
156
  onDismissed: onDismissedProp,
157
+ onHeaderHeightChange: onHeaderHeightChangeProp,
158
+ header,
153
159
  ...props
154
160
  }: StackScreenProps) {
155
161
  let stackId = React.useContext(StackIdContext);
@@ -185,19 +191,44 @@ let StackScreen = React.memo(function StackScreen({
185
191
  };
186
192
  }, [gestureEnabled, stack, screenId, isActive, dispatch]);
187
193
 
188
- let style = React.useMemo(() => styleProp || defaultScreenStyle, [styleProp]);
194
+ let parentHeaderHeight = React.useContext(HeaderHeightContext);
195
+ let [headerHeight, setHeaderHeight] = React.useState(parentHeaderHeight);
196
+
197
+ let onHeaderHeightChange: ScreenProps["onHeaderHeightChange"] =
198
+ React.useCallback(
199
+ (e) => {
200
+ Platform.OS === "ios" &&
201
+ e.nativeEvent.headerHeight > 0 &&
202
+ setHeaderHeight(e.nativeEvent.headerHeight);
203
+ onHeaderHeightChangeProp?.(e);
204
+ },
205
+ [onHeaderHeightChangeProp]
206
+ );
207
+
208
+ let style = React.useMemo(
209
+ () => [
210
+ defaultScreenStyle,
211
+ { paddingTop: headerHeight || parentHeaderHeight },
212
+ styleProp,
213
+ ],
214
+ [styleProp, headerHeight, parentHeaderHeight]
215
+ );
189
216
 
190
217
  return (
191
- // @ts-expect-error - Ref typings in RNScreens
192
- <RNScreen
193
- {...props}
194
- style={style}
195
- activityState={isActive ? 2 : 0}
196
- gestureEnabled={gestureEnabled}
197
- onDismissed={onDismissed}
198
- >
199
- {children}
200
- </RNScreen>
218
+ <HeaderHeightContext.Provider value={headerHeight}>
219
+ {/* @ts-expect-error - Ref typings in RNScreens */}
220
+ <RNScreen
221
+ {...props}
222
+ style={style}
223
+ activityState={isActive ? 2 : 0}
224
+ gestureEnabled={gestureEnabled}
225
+ onDismissed={onDismissed}
226
+ onHeaderHeightChange={onHeaderHeightChange}
227
+ >
228
+ {header}
229
+ {children}
230
+ </RNScreen>
231
+ </HeaderHeightContext.Provider>
201
232
  );
202
233
  });
203
234
 
@@ -233,26 +264,12 @@ let StackSlot = React.memo(function StackSlot({
233
264
  );
234
265
  });
235
266
 
267
+ export type StackScreenHeaderProps = RNScreenStackHeaderConfigProps;
268
+
236
269
  let StackScreenHeader = React.memo(function StackScreenHeader({
237
270
  ...props
238
- }: RNScreenStackHeaderConfigProps) {
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
- );
271
+ }: StackScreenHeaderProps) {
272
+ return <RNScreenStackHeaderConfig {...props} />;
256
273
  });
257
274
 
258
275
  let StackScreenHeaderLeft = React.memo(function StackScreenHeaderLeft({
@@ -279,7 +296,7 @@ let ScreenStackHeaderBackButtonImage = React.memo(
279
296
  }
280
297
  );
281
298
 
282
- type StackNavigatorProps = Omit<StackRootProps, "children"> & {
299
+ export type StackNavigatorProps = Omit<StackRootProps, "children"> & {
283
300
  rootScreen: React.ReactElement<unknown>;
284
301
  };
285
302
 
@@ -309,47 +326,3 @@ export let Stack = {
309
326
  Slot: StackSlot,
310
327
  Navigator: StackNavigator,
311
328
  };
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
@@ -28,7 +28,7 @@ import {
28
28
  } from "./navigation-store";
29
29
  import { generateTabId, useSafeAreaInsetsSafe } from "./utils";
30
30
 
31
- type TabsRootProps = {
31
+ export type TabsRootProps = {
32
32
  children: React.ReactNode;
33
33
  id?: string;
34
34
  };
@@ -44,13 +44,7 @@ let useTabsInternal = (tabId = "") =>
44
44
  return tab;
45
45
  });
46
46
 
47
- let TabsRoot = React.memo(function TabsRoot({
48
- children,
49
- id,
50
- }: {
51
- children: React.ReactNode;
52
- id?: string;
53
- }) {
47
+ let TabsRoot = React.memo(function TabsRoot({ children, id }: TabsRootProps) {
54
48
  let tabIdRef = React.useRef(id || generateTabId());
55
49
  let tabId = tabIdRef.current;
56
50
  let tabs = useTabsInternal(tabId);
@@ -99,10 +93,9 @@ let defaultScreenContainerStyle = {
99
93
  flex: 1,
100
94
  };
101
95
 
102
- function TabsScreens({
103
- children,
104
- ...props
105
- }: { children: React.ReactNode } & RNScreenContainerProps) {
96
+ export type TabsScreensProps = RNScreenContainerProps;
97
+
98
+ function TabsScreens({ children, ...props }: TabsScreensProps) {
106
99
  return (
107
100
  <RNScreenContainer style={defaultScreenContainerStyle} {...props}>
108
101
  {React.Children.map(children, (child, index) => {
@@ -116,11 +109,13 @@ function TabsScreens({
116
109
  );
117
110
  }
118
111
 
112
+ export type TabsScreenProps = RNScreenProps;
113
+
119
114
  let TabsScreen = React.memo(function TabsScreen({
120
115
  children,
121
116
  style: styleProp,
122
117
  ...props
123
- }: { children: React.ReactNode } & RNScreenProps) {
118
+ }: TabsScreenProps) {
124
119
  let dispatch = useNavigationDispatch();
125
120
 
126
121
  let tabId = React.useContext(TabIdContext);
@@ -177,6 +172,7 @@ export let defaultTabbarStyle: ViewStyle = {
177
172
  backgroundColor: "white",
178
173
  };
179
174
 
175
+
180
176
  let TabsTabbar = React.memo(function TabsTabbar({
181
177
  children,
182
178
  style: styleProp,