@rn-tools/navigation 2.0.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/src/stack.tsx ADDED
@@ -0,0 +1,255 @@
1
+ import * as React from "react";
2
+ import { BackHandler, StyleSheet, type ViewStyle } from "react-native";
3
+ import {
4
+ ScreenStackProps as RNScreenStackProps,
5
+ Screen as RNScreen,
6
+ ScreenProps as RNScreenProps,
7
+ ScreenStackHeaderConfig as RNScreenStackHeaderConfig,
8
+ ScreenStackHeaderConfigProps as RNScreenStackHeaderConfigProps,
9
+ } from "react-native-screens";
10
+ import ScreenStackNativeComponent from "react-native-screens/src/fabric/ScreenStackNativeComponent";
11
+
12
+ import {
13
+ ActiveContext,
14
+ DepthContext,
15
+ TabIdContext,
16
+ TabScreenIndexContext,
17
+ } from "./contexts";
18
+ import { DEFAULT_SLOT_NAME } from "./navigation-reducer";
19
+ import { useNavigationDispatch, useNavigationState } from "./navigation-store";
20
+ import type { StackItem } from "./types";
21
+ import { generateStackId } from "./utils";
22
+
23
+ let StackIdContext = React.createContext<string>("");
24
+ let ScreenIdContext = React.createContext<string>("");
25
+
26
+ const RNScreenStack = React.memo(function RNScreenStack(
27
+ props: RNScreenStackProps
28
+ ) {
29
+ const { children, gestureDetectorBridge, ...rest } = props;
30
+ const ref = React.useRef(null);
31
+
32
+ React.useEffect(() => {
33
+ if (gestureDetectorBridge) {
34
+ gestureDetectorBridge.current.stackUseEffectCallback(ref);
35
+ }
36
+ });
37
+
38
+ return (
39
+ <ScreenStackNativeComponent {...rest} ref={ref}>
40
+ {children}
41
+ </ScreenStackNativeComponent>
42
+ );
43
+ });
44
+
45
+ type StackRootProps = {
46
+ children: React.ReactNode;
47
+ id?: string;
48
+ };
49
+
50
+ let useStackInternal = (stackId = "") => {
51
+ let stack: StackItem | undefined = useNavigationState(
52
+ (state) => state.stacks.lookup[stackId]
53
+ );
54
+ return stack;
55
+ };
56
+
57
+ function StackRoot({ children, id }: StackRootProps) {
58
+ let idRef = React.useRef(id || generateStackId());
59
+ let stack = useStackInternal(idRef.current);
60
+
61
+ let isActive = React.useContext(ActiveContext);
62
+ let parentDepth = React.useContext(DepthContext);
63
+ let parentStackId = React.useContext(StackIdContext);
64
+
65
+ let depth = parentDepth + 1;
66
+ let stackId = idRef.current;
67
+ let parentTabId = React.useContext(TabIdContext);
68
+ let tabIndex = React.useContext(TabScreenIndexContext);
69
+
70
+ let dispatch = useNavigationDispatch();
71
+
72
+ React.useLayoutEffect(() => {
73
+ if (!stack) {
74
+ dispatch({ type: "CREATE_STACK_INSTANCE", stackId: idRef.current });
75
+ }
76
+ }, [stack, dispatch]);
77
+
78
+ React.useEffect(() => {
79
+ if (stack != null) {
80
+ dispatch({
81
+ type: "REGISTER_STACK",
82
+ depth,
83
+ isActive,
84
+ stackId: stack.id,
85
+ parentStackId,
86
+ parentTabId,
87
+ tabIndex,
88
+ });
89
+ }
90
+ }, [stack, depth, isActive, parentStackId, parentTabId, tabIndex, dispatch]);
91
+
92
+ React.useEffect(() => {
93
+ return () => {
94
+ if (stackId != null) {
95
+ dispatch({ type: "UNREGISTER_STACK", stackId });
96
+ }
97
+ };
98
+ }, [stackId, dispatch]);
99
+
100
+ if (!stack) {
101
+ return null;
102
+ }
103
+
104
+ return (
105
+ <StackIdContext.Provider value={stack.id}>
106
+ <DepthContext.Provider value={depth}>
107
+ <ActiveContext.Provider value={isActive}>
108
+ {children}
109
+ </ActiveContext.Provider>
110
+ </DepthContext.Provider>
111
+ </StackIdContext.Provider>
112
+ );
113
+ }
114
+
115
+ function StackScreens({
116
+ style: styleProp,
117
+ ...props
118
+ }: RNScreenStackProps) {
119
+ let style = React.useMemo(
120
+ () => styleProp || StyleSheet.absoluteFill,
121
+ [styleProp]
122
+ );
123
+
124
+ return (
125
+ <RNScreenStack {...props} style={style} />
126
+ );
127
+ }
128
+
129
+ let defaultScreenStyle: ViewStyle = {
130
+ ...StyleSheet.absoluteFillObject,
131
+ backgroundColor: "white",
132
+ };
133
+
134
+ export type StackScreenProps = RNScreenProps;
135
+
136
+ let StackScreen = React.memo(function StackScreen({
137
+ children,
138
+ style: styleProp,
139
+ gestureEnabled = true,
140
+ onDismissed: onDismissedProp,
141
+ ...props
142
+ }: StackScreenProps) {
143
+ let stackId = React.useContext(StackIdContext);
144
+ let screenId = React.useContext(ScreenIdContext);
145
+ let stack = useStackInternal(stackId);
146
+
147
+ let dispatch = useNavigationDispatch();
148
+
149
+ let isActive = React.useContext(ActiveContext);
150
+
151
+ let onDismissed: RNScreenProps["onDismissed"] = React.useCallback(
152
+ (e) => {
153
+ dispatch({ type: "POP_SCREEN_BY_KEY", key: screenId });
154
+ onDismissedProp?.(e);
155
+ },
156
+ [onDismissedProp, dispatch, screenId]
157
+ );
158
+
159
+ React.useEffect(() => {
160
+ function backHandler() {
161
+ if (gestureEnabled && isActive && stack?.screens.length > 0) {
162
+ dispatch({ type: "POP_SCREEN_BY_KEY", key: screenId });
163
+ return true;
164
+ }
165
+
166
+ return false;
167
+ }
168
+
169
+ BackHandler.addEventListener("hardwareBackPress", backHandler);
170
+
171
+ return () => {
172
+ BackHandler.removeEventListener("hardwareBackPress", backHandler);
173
+ };
174
+ }, [gestureEnabled, stack, screenId, isActive, dispatch]);
175
+
176
+ let style = React.useMemo(() => styleProp || defaultScreenStyle, [styleProp]);
177
+
178
+ return (
179
+ // @ts-expect-error - Ref typings in RNScreens
180
+ <RNScreen
181
+ {...props}
182
+ style={style}
183
+ // activityState={isActive ? 2 : 0}
184
+ gestureEnabled={gestureEnabled}
185
+ onDismissed={onDismissed}
186
+ >
187
+ {children}
188
+ </RNScreen>
189
+ );
190
+ });
191
+
192
+ let useStackScreens = (stackId = "", slotName: string) => {
193
+ return useNavigationState((state) => {
194
+ let stack = state.stacks.lookup[stackId];
195
+ return (
196
+ stack?.screens
197
+ .map((screenId) => state.screens.lookup[screenId])
198
+ .filter((s) => s && s.slotName === slotName) ?? []
199
+ );
200
+ });
201
+ };
202
+
203
+ let StackSlot = React.memo(function StackSlot({
204
+ slotName = DEFAULT_SLOT_NAME,
205
+ }: {
206
+ slotName?: string;
207
+ }) {
208
+ let stackId = React.useContext(StackIdContext);
209
+ let screens = useStackScreens(stackId, slotName);
210
+
211
+ return (
212
+ <>
213
+ {screens.map((screen) => {
214
+ return (
215
+ <ScreenIdContext.Provider value={screen.id} key={screen.id}>
216
+ {screen.element}
217
+ </ScreenIdContext.Provider>
218
+ );
219
+ })}
220
+ </>
221
+ );
222
+ });
223
+
224
+ let StackScreenHeader = React.memo(function StackScreenHeader({
225
+ ...props
226
+ }: RNScreenStackHeaderConfigProps) {
227
+ return <RNScreenStackHeaderConfig {...props} />;
228
+ });
229
+
230
+ type StackNavigatorProps = Omit<StackRootProps, "children"> & {
231
+ rootScreen: React.ReactElement<unknown>;
232
+ };
233
+
234
+ let StackNavigator = React.memo(function StackNavigator({
235
+ rootScreen,
236
+ ...rootProps
237
+ }: StackNavigatorProps) {
238
+ return (
239
+ <Stack.Root {...rootProps}>
240
+ <Stack.Screens>
241
+ <Stack.Screen>{rootScreen}</Stack.Screen>
242
+ <Stack.Slot />
243
+ </Stack.Screens>
244
+ </Stack.Root>
245
+ );
246
+ });
247
+
248
+ export let Stack = {
249
+ Root: StackRoot,
250
+ Screens: StackScreens,
251
+ Screen: StackScreen,
252
+ Header: StackScreenHeader,
253
+ Slot: StackSlot,
254
+ Navigator: StackNavigator,
255
+ };
package/src/tabs.tsx ADDED
@@ -0,0 +1,297 @@
1
+ import * as React from "react";
2
+ import {
3
+ BackHandler,
4
+ Pressable,
5
+ StyleSheet,
6
+ View,
7
+ type PressableProps,
8
+ type ViewProps,
9
+ type ViewStyle,
10
+ } from "react-native";
11
+ import {
12
+ Screen as RNScreen,
13
+ ScreenProps as RNScreenProps,
14
+ ScreenContainer as RNScreenContainer,
15
+ ScreenContainerProps as RNScreenContainerProps,
16
+ } from "react-native-screens";
17
+
18
+ import {
19
+ ActiveContext,
20
+ DepthContext,
21
+ TabIdContext,
22
+ TabScreenIndexContext,
23
+ } from "./contexts";
24
+ import { useNavigationDispatch, useNavigationState } from "./navigation-store";
25
+ import { generateTabId } from "./utils";
26
+
27
+ type TabsRootProps = {
28
+ children: React.ReactNode;
29
+ id?: string;
30
+ };
31
+
32
+ let useTabsInternal = (tabId = "") =>
33
+ useNavigationState((state) => {
34
+ let tab = state.tabs.lookup[tabId];
35
+
36
+ if (!tab) {
37
+ return null;
38
+ }
39
+
40
+ return tab;
41
+ });
42
+
43
+ let TabsRoot = React.memo(function TabsRoot({
44
+ children,
45
+ id,
46
+ }: {
47
+ children: React.ReactNode;
48
+ id?: string;
49
+ }) {
50
+ let tabIdRef = React.useRef(id || generateTabId());
51
+ let tabId = tabIdRef.current;
52
+ let tabs = useTabsInternal(tabId);
53
+ let dispatch = useNavigationDispatch();
54
+
55
+ React.useEffect(() => {
56
+ if (!tabs) {
57
+ dispatch({ type: "CREATE_TAB_INSTANCE", tabId: tabId });
58
+ }
59
+ }, [tabs, tabId, dispatch]);
60
+
61
+ let depth = React.useContext(DepthContext);
62
+ let isActive = React.useContext(ActiveContext);
63
+ let parentTabId = React.useContext(TabIdContext);
64
+
65
+ React.useEffect(() => {
66
+ if (tabs != null) {
67
+ dispatch({
68
+ type: "REGISTER_TAB",
69
+ depth,
70
+ isActive,
71
+ tabId: tabs.id,
72
+ parentTabId,
73
+ });
74
+ }
75
+ }, [tabs, depth, isActive, parentTabId, dispatch]);
76
+
77
+ React.useEffect(() => {
78
+ return () => {
79
+ if (tabId != null) {
80
+ dispatch({ type: "UNREGISTER_TAB", tabId });
81
+ }
82
+ };
83
+ }, [tabId, dispatch]);
84
+
85
+ if (!tabs) {
86
+ return null;
87
+ }
88
+
89
+ return (
90
+ <TabIdContext.Provider value={tabs.id}>{children}</TabIdContext.Provider>
91
+ );
92
+ });
93
+
94
+ let defaultScreenContainerStyle = {
95
+ flex: 1,
96
+ };
97
+
98
+ function TabsScreens({
99
+ children,
100
+ ...props
101
+ }: { children: React.ReactNode } & RNScreenContainerProps) {
102
+ return (
103
+ <RNScreenContainer style={defaultScreenContainerStyle} {...props}>
104
+ {React.Children.map(children, (child, index) => {
105
+ return (
106
+ <TabScreenIndexContext.Provider value={index}>
107
+ {child}
108
+ </TabScreenIndexContext.Provider>
109
+ );
110
+ })}
111
+ </RNScreenContainer>
112
+ );
113
+ }
114
+
115
+ let TabsScreen = React.memo(function TabsScreen({
116
+ children,
117
+ style: styleProp,
118
+ ...props
119
+ }: { children: React.ReactNode } & RNScreenProps) {
120
+ let dispatch = useNavigationDispatch();
121
+
122
+ let tabId = React.useContext(TabIdContext);
123
+ let tabs = useTabsInternal(tabId);
124
+ let index = React.useContext(TabScreenIndexContext);
125
+
126
+ let parentIsActive = React.useContext(ActiveContext);
127
+ let activeIndex = tabs?.activeIndex;
128
+ let isActive = index === activeIndex;
129
+
130
+ React.useEffect(() => {
131
+ function backHandler() {
132
+ if (tabs && tabs.history.length > 0) {
133
+ dispatch({ type: "TAB_BACK", tabId });
134
+ return true;
135
+ }
136
+
137
+ return false;
138
+ }
139
+
140
+ BackHandler.addEventListener("hardwareBackPress", backHandler);
141
+
142
+ return () => {
143
+ BackHandler.removeEventListener("hardwareBackPress", backHandler);
144
+ };
145
+ }, [tabId, dispatch, tabs]);
146
+
147
+ let style = React.useMemo(
148
+ () => styleProp || StyleSheet.absoluteFill,
149
+ [styleProp]
150
+ );
151
+
152
+ return (
153
+ // @ts-expect-error - Ref typings in RNScreens
154
+ <RNScreen
155
+ active={isActive ? 1 : 0}
156
+ activityState={isActive ? 2 : 0}
157
+ style={style}
158
+ {...props}
159
+ >
160
+ <ActiveContext.Provider value={parentIsActive && isActive}>
161
+ {children}
162
+ </ActiveContext.Provider>
163
+ </RNScreen>
164
+ );
165
+ });
166
+
167
+ export let defaultTabbarStyle: ViewStyle = {
168
+ flexDirection: "row",
169
+ backgroundColor: "white",
170
+ };
171
+
172
+ let TabsTabbar = React.memo(function TabsTabbar({
173
+ children,
174
+ style: styleProp,
175
+ ...props
176
+ }: { children: React.ReactNode } & ViewProps) {
177
+ let style = React.useMemo(() => styleProp || defaultTabbarStyle, [styleProp]);
178
+
179
+ return (
180
+ <View style={style} {...props}>
181
+ {React.Children.map(children, (child, index) => {
182
+ return (
183
+ <TabScreenIndexContext.Provider value={index}>
184
+ {child}
185
+ </TabScreenIndexContext.Provider>
186
+ );
187
+ })}
188
+ </View>
189
+ );
190
+ });
191
+
192
+ type TabbarTabProps = {
193
+ activeStyle?: PressableProps["style"];
194
+ inactiveStyle?: PressableProps["style"];
195
+ style?: PressableProps["style"];
196
+ children:
197
+ | React.ReactNode
198
+ | ((props: { isActive: boolean; onPress: () => void }) => React.ReactNode);
199
+ } & Omit<PressableProps, "children">;
200
+
201
+ let defaultTabStyle: ViewStyle = {
202
+ flex: 1,
203
+ };
204
+
205
+ let TabsTab = React.memo(function TabsTab({
206
+ children,
207
+ ...props
208
+ }: TabbarTabProps) {
209
+ let dispatch = useNavigationDispatch();
210
+
211
+ let tabId = React.useContext(TabIdContext);
212
+ let index = React.useContext(TabScreenIndexContext);
213
+ let tabs = useTabsInternal(tabId);
214
+
215
+ let activeIndex = tabs?.activeIndex;
216
+ let isActive = index === activeIndex;
217
+
218
+ let onPress: () => void = React.useCallback(() => {
219
+ dispatch({ type: "SET_TAB_INDEX", tabId, index });
220
+ }, [tabId, index, dispatch]);
221
+
222
+ let style = React.useMemo(() => {
223
+ let baseStyle = props.style || defaultTabStyle;
224
+ let activeStyle = isActive ? props.activeStyle : props.inactiveStyle;
225
+ return [baseStyle, activeStyle];
226
+ }, [isActive, props.activeStyle, props.inactiveStyle, props.style]);
227
+
228
+ let renderChildren = React.useMemo(() => {
229
+ if (typeof children === "function") {
230
+ return children({ isActive, onPress });
231
+ }
232
+
233
+ return children;
234
+ }, [isActive, onPress, children]);
235
+
236
+ return (
237
+ // @ts-expect-error - cleanup typings
238
+ <Pressable onPress={onPress} style={style} {...props}>
239
+ {renderChildren}
240
+ </Pressable>
241
+ );
242
+ });
243
+
244
+
245
+ let TabNavigator = React.memo(function TabNavigator({
246
+ screens,
247
+ tabbarPosition = "bottom",
248
+ tabbarStyle,
249
+ ...rootProps
250
+ }: TabNavigatorProps) {
251
+ return (
252
+ <Tabs.Root {...rootProps}>
253
+ {tabbarPosition === "top" && (
254
+ <Tabs.Tabbar style={tabbarStyle}>
255
+ {screens.map((screen) => {
256
+ return <Tabs.Tab key={screen.key}>{screen.tab}</Tabs.Tab>;
257
+ })}
258
+ </Tabs.Tabbar>
259
+ )}
260
+
261
+ <Tabs.Screens>
262
+ {screens.map((screen) => {
263
+ return <Tabs.Screen key={screen.key}>{screen.screen}</Tabs.Screen>;
264
+ })}
265
+ </Tabs.Screens>
266
+
267
+ {tabbarPosition === "bottom" && (
268
+ <Tabs.Tabbar style={tabbarStyle}>
269
+ {screens.map((screen) => {
270
+ return <Tabs.Tab key={screen.key}>{screen.tab}</Tabs.Tab>;
271
+ })}
272
+ </Tabs.Tabbar>
273
+ )}
274
+ </Tabs.Root>
275
+ );
276
+ });
277
+
278
+ export let Tabs = {
279
+ Root: TabsRoot,
280
+ Screens: TabsScreens,
281
+ Screen: TabsScreen,
282
+ Tabbar: TabsTabbar,
283
+ Tab: TabsTab,
284
+ Navigator: TabNavigator,
285
+ };
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/types.ts ADDED
@@ -0,0 +1,48 @@
1
+ export type PushScreenOptions = {
2
+ stackId?: string;
3
+ slotName?: string;
4
+ screenId?: string;
5
+ };
6
+
7
+ export type StackItem = {
8
+ id: string;
9
+ defaultSlotName?: string;
10
+ screens: string[];
11
+ };
12
+
13
+ export type ScreenItem = {
14
+ id: string;
15
+ stackId: string;
16
+ element: React.ReactElement<unknown>;
17
+ slotName?: string;
18
+ };
19
+
20
+ export type TabItem = {
21
+ id: string;
22
+ activeIndex: number;
23
+ history: number[];
24
+ };
25
+
26
+ export type NavigationState = {
27
+ stacks: {
28
+ lookup: Record<string, StackItem>;
29
+ ids: string[];
30
+ };
31
+ tabs: {
32
+ lookup: Record<string, TabItem>;
33
+ ids: string[];
34
+ };
35
+ screens: {
36
+ lookup: Record<string, ScreenItem>;
37
+ ids: string[];
38
+ };
39
+ debugModeEnabled: boolean;
40
+ };
41
+
42
+ export type RenderCharts = {
43
+ stacksByDepth: Record<string, string[]>;
44
+ tabsByDepth: Record<string, string[]>;
45
+ tabParentsById: Record<string, string>;
46
+ stackParentsById: Record<string, string>;
47
+ stacksByTabIndex: Record<string, string[]>;
48
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,14 @@
1
+ export let generateStackId = createIdGenerator("stack");
2
+ export let generateScreenId = createIdGenerator("screen");
3
+ export let generateTabId = createIdGenerator("tab");
4
+
5
+ function createIdGenerator(name: string) {
6
+ let counter = 0;
7
+
8
+ return function generateId() {
9
+ return name + "-" + counter++;
10
+ };
11
+ }
12
+
13
+ export let serializeTabIndexKey = (tabId: string, index: number) =>
14
+ `${tabId}-${index}`;