@rn-tools/navigation 2.2.4 → 2.2.5

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
 
@@ -362,6 +364,27 @@ function MyScreenWithHeader() {
362
364
  }
363
365
  ```
364
366
 
367
+ ### Configuring screen props
368
+
369
+ 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)
370
+
371
+ Some notable props are `stackPresentation`, `stackAnimation`, and `gestureEnabled`, however there are many more available.
372
+
373
+ ```tsx
374
+ function pushNativeModalScreen() {
375
+ navigation.pushScreen(
376
+ <Stack.Screen
377
+ stackPresentation="modal"
378
+ stackAnimation="slide_from_bottom"
379
+ gestureEnabled={true}
380
+ >
381
+ {/* If you want to push more screens inside of the modal, wrap it with a Stack */}
382
+ <Stack.Navigator rootScreen={<MyScreen title="Modal screen" />} />
383
+ </Stack.Screen>
384
+ );
385
+ }
386
+ ```
387
+
365
388
  ## Components
366
389
 
367
390
  The `Navigator` components in the previous examples are fairly straightforward wrappers around other lower level `Stack` and `Tabs` components.
@@ -550,7 +573,7 @@ export default function DeepLinksExample() {
550
573
  // let { path } = Linking.parse(url)
551
574
 
552
575
  // But it's easier to test hardcoded strings for the sake of this example
553
- let path = "/testing/home/item/2";
576
+ let path = "/home/item/2";
554
577
 
555
578
  return (
556
579
  <Stack.Navigator
@@ -559,23 +582,7 @@ export default function DeepLinksExample() {
559
582
  path={path}
560
583
  handlers={[
561
584
  {
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",
585
+ path: "/home/item/:itemId",
579
586
  handler: (params: { itemId: string }) => {
580
587
  let itemId = params.itemId;
581
588
 
@@ -688,7 +695,6 @@ function MyScreen({
688
695
  </View>
689
696
  );
690
697
  }
691
-
692
698
  ```
693
699
 
694
700
  ### Preventing going back
@@ -772,3 +778,13 @@ function MyScreen() {
772
778
  );
773
779
  }
774
780
  ```
781
+
782
+ ### Testing
783
+
784
+ Recommended:
785
+
786
+ - Check out the getting started portion of [React Native Testing Library](https://callstack.github.io/react-native-testing-library/docs/start/quick-start)
787
+
788
+ - Set up [Jest Expo](https://docs.expo.dev/develop/unit-testing/)
789
+
790
+ - 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.5",
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]
@@ -145,11 +145,14 @@ let defaultScreenStyle: ViewStyle = {
145
145
 
146
146
  export type StackScreenProps = RNScreenProps;
147
147
 
148
+ let HeaderHeightContext = React.createContext<number>(0);
149
+
148
150
  let StackScreen = React.memo(function StackScreen({
149
151
  children,
150
152
  style: styleProp,
151
153
  gestureEnabled = true,
152
154
  onDismissed: onDismissedProp,
155
+ onHeaderHeightChange: onHeaderHeightChangeProp,
153
156
  ...props
154
157
  }: StackScreenProps) {
155
158
  let stackId = React.useContext(StackIdContext);
@@ -187,17 +190,31 @@ let StackScreen = React.memo(function StackScreen({
187
190
 
188
191
  let style = React.useMemo(() => styleProp || defaultScreenStyle, [styleProp]);
189
192
 
193
+ let [headerHeight, setHeaderHeight] = React.useState(0);
194
+
195
+ let onHeaderHeightChange: ScreenProps["onHeaderHeightChange"] =
196
+ React.useCallback(
197
+ (e) => {
198
+ Platform.OS === "ios" && setHeaderHeight(e.nativeEvent.headerHeight);
199
+ onHeaderHeightChangeProp?.(e);
200
+ },
201
+ [onHeaderHeightChangeProp]
202
+ );
203
+
190
204
  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>
205
+ <HeaderHeightContext.Provider value={headerHeight}>
206
+ {/* @ts-expect-error - Ref typings in RNScreens */}
207
+ <RNScreen
208
+ {...props}
209
+ style={style}
210
+ activityState={isActive ? 2 : 0}
211
+ gestureEnabled={gestureEnabled}
212
+ onDismissed={onDismissed}
213
+ onHeaderHeightChange={onHeaderHeightChange}
214
+ >
215
+ {children}
216
+ </RNScreen>
217
+ </HeaderHeightContext.Provider>
201
218
  );
202
219
  });
203
220
 
@@ -233,24 +250,23 @@ let StackSlot = React.memo(function StackSlot({
233
250
  );
234
251
  });
235
252
 
253
+ export type StackScreenHeaderProps = RNScreenStackHeaderConfigProps;
254
+
236
255
  let StackScreenHeader = React.memo(function StackScreenHeader({
237
256
  ...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
- }
257
+ }: StackScreenHeaderProps) {
258
+ let headerHeight = React.useContext(HeaderHeightContext);
246
259
 
247
- return getDefaultHeaderHeight(layout, false, insets.top);
248
- }, [layout, insets]);
260
+ let placeholderStyle = React.useMemo<ViewStyle>(() => {
261
+ return {
262
+ height: headerHeight,
263
+ };
264
+ }, [headerHeight]);
249
265
 
250
266
  return (
251
267
  <React.Fragment>
252
268
  <RNScreenStackHeaderConfig {...props} />
253
- <View style={{ height: headerHeight }} />
269
+ <View style={placeholderStyle} />
254
270
  </React.Fragment>
255
271
  );
256
272
  });
@@ -279,7 +295,7 @@ let ScreenStackHeaderBackButtonImage = React.memo(
279
295
  }
280
296
  );
281
297
 
282
- type StackNavigatorProps = Omit<StackRootProps, "children"> & {
298
+ export type StackNavigatorProps = Omit<StackRootProps, "children"> & {
283
299
  rootScreen: React.ReactElement<unknown>;
284
300
  };
285
301
 
@@ -309,47 +325,3 @@ export let Stack = {
309
325
  Slot: StackSlot,
310
326
  Navigator: StackNavigator,
311
327
  };
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,