@rn-tools/navigation 2.2.6 → 3.0.1

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/tabs.tsx CHANGED
@@ -1,318 +1,169 @@
1
1
  import * as React from "react";
2
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";
3
+ RenderTreeNode,
4
+ useRenderNode,
5
+ useSafeAreaInsets,
6
+ } from "@rn-tools/core";
7
+ import { useTabActiveIndex, useNavigationStore, useNavigation } from "./navigation-client";
8
+
9
+ import * as RNScreens from "react-native-screens";
10
+ import { StyleSheet, View, type ViewStyle } from "react-native";
11
+
12
+ export type TabScreenOptions = {
13
+ id: string;
14
+ screen: React.ReactElement;
15
+ tab: (props: {
16
+ id: string;
17
+ isActive: boolean;
18
+ onPress: () => void;
19
+ }) => React.ReactElement;
20
+ };
17
21
 
18
- import {
19
- ActiveContext,
20
- DepthContext,
21
- TabIdContext,
22
- TabScreenIndexContext,
23
- } from "./contexts";
24
- import {
25
- useGetNavigationStore,
26
- useNavigationDispatch,
27
- useNavigationState,
28
- } from "./navigation-store";
29
- import { generateTabId, useSafeAreaInsetsSafe } from "./utils";
22
+ export type TabsHandle = {
23
+ setActiveIndex: (index: number) => void;
24
+ };
30
25
 
31
- export type TabsRootProps = {
32
- children: React.ReactNode;
26
+ export type TabsProps = {
33
27
  id?: string;
28
+ active?: boolean;
29
+ screens: TabScreenOptions[];
30
+ tabbarPosition?: "top" | "bottom";
31
+ tabbarStyle?: ViewStyle;
32
+ children?: React.ReactNode;
34
33
  };
35
34
 
36
- let useTabsInternal = (tabId = "") =>
37
- useNavigationState((state) => {
38
- let tab = state.tabs.lookup[tabId];
39
-
40
- if (!tab) {
41
- return null;
42
- }
43
-
44
- return tab;
45
- });
46
-
47
- let TabsRoot = React.memo(function TabsRoot({ children, id }: TabsRootProps) {
48
- let tabIdRef = React.useRef(id || generateTabId());
49
- let tabId = tabIdRef.current;
50
- let tabs = useTabsInternal(tabId);
51
- let dispatch = useNavigationDispatch();
52
-
53
- React.useEffect(() => {
54
- if (!tabs) {
55
- dispatch({ type: "CREATE_TAB_INSTANCE", tabId: tabId });
56
- }
57
- }, [tabs, tabId, dispatch]);
58
-
59
- let depth = React.useContext(DepthContext);
60
- let isActive = React.useContext(ActiveContext);
61
- let parentTabId = React.useContext(TabIdContext);
62
-
63
- React.useEffect(() => {
64
- if (tabs != null) {
65
- dispatch({
66
- type: "REGISTER_TAB",
67
- depth,
68
- isActive,
69
- tabId: tabs.id,
70
- parentTabId,
71
- });
72
- }
73
- }, [tabs, depth, isActive, parentTabId, dispatch]);
74
-
75
- React.useEffect(() => {
76
- return () => {
77
- if (tabId != null) {
78
- dispatch({ type: "UNREGISTER_TAB", tabId });
79
- }
80
- };
81
- }, [tabId, dispatch]);
82
-
83
- if (!tabs) {
84
- return null;
85
- }
86
-
35
+ const TabsRoot = React.memo(function TabsRoot(props: TabsProps) {
87
36
  return (
88
- <TabIdContext.Provider value={tabs.id}>{children}</TabIdContext.Provider>
37
+ <RenderTreeNode type="tabs" id={props.id} active={props.active}>
38
+ {props.children}
39
+ </RenderTreeNode>
89
40
  );
90
41
  });
91
42
 
92
- let defaultScreenContainerStyle = {
93
- flex: 1,
94
- };
95
-
96
- export type TabsScreensProps = RNScreenContainerProps;
97
-
98
- function TabsScreens({ children, ...props }: TabsScreensProps) {
99
- return (
100
- <RNScreenContainer style={defaultScreenContainerStyle} {...props}>
101
- {React.Children.map(children, (child, index) => {
102
- return (
103
- <TabScreenIndexContext.Provider value={index}>
104
- {child}
105
- </TabScreenIndexContext.Provider>
106
- );
107
- })}
108
- </RNScreenContainer>
43
+ const TabBar = React.memo(function TabBar(props: {
44
+ screens: TabScreenOptions[];
45
+ style?: ViewStyle;
46
+ position: "top" | "bottom";
47
+ }) {
48
+ const node = useRenderNode();
49
+ const tabsId = node?.id ?? null;
50
+ const activeIndex = useTabActiveIndex(tabsId);
51
+ const navStore = useNavigationStore();
52
+ const insets = useSafeAreaInsets();
53
+
54
+ const tabbarStyle = React.useMemo(
55
+ () => [
56
+ styles.tabbar,
57
+ props.position === "top" && { paddingTop: insets.top },
58
+ props.position === "bottom" && { paddingBottom: insets.bottom },
59
+ props.style,
60
+ ],
61
+ [props.position, props.style, insets.top, insets.bottom],
109
62
  );
110
- }
111
-
112
- export type TabsScreenProps = RNScreenProps;
113
-
114
- let TabsScreen = React.memo(function TabsScreen({
115
- children,
116
- style: styleProp,
117
- ...props
118
- }: TabsScreenProps) {
119
- let dispatch = useNavigationDispatch();
120
-
121
- let tabId = React.useContext(TabIdContext);
122
- let tabs = useTabsInternal(tabId);
123
- let getNavigationStore = useGetNavigationStore();
124
- let index = React.useContext(TabScreenIndexContext);
125
-
126
- let parentIsActive = React.useContext(ActiveContext);
127
- let activeIndex = tabs?.activeIndex;
128
- let isActive = index === activeIndex;
129
63
 
130
- React.useEffect(() => {
131
- function backHandler() {
132
- // Use getter to register the handler once on mount
133
- // Prevents it from overriding child screen handlers
134
- let tabs = getNavigationStore().tabs.lookup[tabId];
135
- if (tabs && tabs.history.length > 0) {
136
- dispatch({ type: "TAB_BACK", tabId });
137
- return true;
64
+ const handlePress = React.useCallback(
65
+ (index: number) => {
66
+ if (tabsId) {
67
+ navStore.setState((prev) => {
68
+ const tabs = new Map(prev.tabs);
69
+ tabs.set(tabsId, { activeIndex: index });
70
+ return { ...prev, tabs };
71
+ });
138
72
  }
139
-
140
- return false;
141
- }
142
-
143
- BackHandler.addEventListener("hardwareBackPress", backHandler);
144
-
145
- return () => {
146
- BackHandler.removeEventListener("hardwareBackPress", backHandler);
147
- };
148
- }, [tabId, dispatch, getNavigationStore]);
149
-
150
- let style = React.useMemo(
151
- () => styleProp || StyleSheet.absoluteFill,
152
- [styleProp]
73
+ },
74
+ [tabsId, navStore],
153
75
  );
154
76
 
155
77
  return (
156
- // @ts-expect-error - Ref typings in RNScreens
157
- <RNScreen
158
- active={isActive ? 1 : 0}
159
- activityState={isActive ? 2 : 0}
160
- style={style}
161
- {...props}
162
- >
163
- <ActiveContext.Provider value={parentIsActive && isActive}>
164
- {children}
165
- </ActiveContext.Provider>
166
- </RNScreen>
167
- );
168
- });
169
-
170
- export let defaultTabbarStyle: ViewStyle = {
171
- flexDirection: "row",
172
- backgroundColor: "white",
173
- };
174
-
175
-
176
- let TabsTabbar = React.memo(function TabsTabbar({
177
- children,
178
- style: styleProp,
179
- ...props
180
- }: { children: React.ReactNode } & ViewProps) {
181
- let style = React.useMemo(() => styleProp || defaultTabbarStyle, [styleProp]);
182
-
183
- return (
184
- <View style={style} {...props}>
185
- {React.Children.map(children, (child, index) => {
186
- return (
187
- <TabScreenIndexContext.Provider value={index}>
188
- {child}
189
- </TabScreenIndexContext.Provider>
190
- );
191
- })}
78
+ <View style={tabbarStyle}>
79
+ {props.screens.map((entry, index) => (
80
+ <React.Fragment key={entry.id}>
81
+ {entry.tab({
82
+ id: entry.id,
83
+ isActive: index === activeIndex,
84
+ onPress: () => handlePress(index),
85
+ })}
86
+ </React.Fragment>
87
+ ))}
192
88
  </View>
193
89
  );
194
90
  });
195
91
 
196
- type TabbarTabProps = {
197
- activeStyle?: PressableProps["style"];
198
- inactiveStyle?: PressableProps["style"];
199
- style?: PressableProps["style"];
200
- children:
201
- | React.ReactNode
202
- | ((props: { isActive: boolean; onPress: () => void }) => React.ReactNode);
203
- } & Omit<PressableProps, "children">;
204
-
205
- let defaultTabStyle: ViewStyle = {
206
- flex: 1,
207
- };
208
-
209
- let TabsTab = React.memo(function TabsTab({
210
- children,
211
- ...props
212
- }: TabbarTabProps) {
213
- let dispatch = useNavigationDispatch();
214
-
215
- let tabId = React.useContext(TabIdContext);
216
- let index = React.useContext(TabScreenIndexContext);
217
- let tabs = useTabsInternal(tabId);
218
-
219
- let activeIndex = tabs?.activeIndex;
220
- let isActive = index === activeIndex;
221
-
222
- let onPress: () => void = React.useCallback(() => {
223
- dispatch({ type: "SET_TAB_INDEX", tabId, index });
224
-
225
- if (isActive) {
226
- dispatch({ type: "POP_ACTIVE_TAB", tabId, index });
227
- }
228
- }, [tabId, index, dispatch, isActive]);
229
-
230
- let style = React.useMemo(() => {
231
- let baseStyle = props.style || defaultTabStyle;
232
- let activeStyle = isActive ? props.activeStyle : props.inactiveStyle;
233
- return [baseStyle, activeStyle];
234
- }, [isActive, props.activeStyle, props.inactiveStyle, props.style]);
235
-
236
- let renderChildren = React.useMemo(() => {
237
- if (typeof children === "function") {
238
- return children({ isActive, onPress });
239
- }
240
-
241
- return children;
242
- }, [isActive, onPress, children]);
92
+ const TabsSlot = React.memo(function TabsSlot(props: {
93
+ screens: TabScreenOptions[];
94
+ }) {
95
+ const node = useRenderNode();
96
+ const tabsId = node?.id ?? null;
97
+ const activeIndex = useTabActiveIndex(tabsId);
243
98
 
244
99
  return (
245
- // @ts-expect-error - cleanup typings
246
- <Pressable onPress={onPress} style={style} {...props}>
247
- {renderChildren}
248
- </Pressable>
100
+ <RNScreens.ScreenContainer style={StyleSheet.absoluteFill}>
101
+ {props.screens.map((entry, index) => (
102
+ <RNScreens.Screen
103
+ key={entry.id}
104
+ activityState={index === activeIndex ? 2 : 0}
105
+ style={StyleSheet.absoluteFill}
106
+ >
107
+ <RenderTreeNode
108
+ type="tab-screen"
109
+ id={`${tabsId}/${entry.id}`}
110
+ active={index === activeIndex}
111
+ >
112
+ {entry.screen}
113
+ </RenderTreeNode>
114
+ </RNScreens.Screen>
115
+ ))}
116
+ </RNScreens.ScreenContainer>
249
117
  );
250
118
  });
251
119
 
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
-
265
- let TabNavigator = React.memo(function TabNavigator({
266
- screens,
267
- tabbarPosition = "bottom",
268
- tabbarStyle: tabbarStyleProp,
269
- ...rootProps
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
-
284
- return (
285
- <Tabs.Root {...rootProps}>
286
- {tabbarPosition === "top" && (
287
- <Tabs.Tabbar style={tabbarStyle}>
288
- {screens.map((screen) => {
289
- return <Tabs.Tab key={screen.key}>{screen.tab}</Tabs.Tab>;
290
- })}
291
- </Tabs.Tabbar>
292
- )}
293
-
294
- <Tabs.Screens>
295
- {screens.map((screen) => {
296
- return <Tabs.Screen key={screen.key}>{screen.screen}</Tabs.Screen>;
297
- })}
298
- </Tabs.Screens>
299
-
300
- {tabbarPosition === "bottom" && (
301
- <Tabs.Tabbar style={tabbarStyle}>
302
- {screens.map((screen) => {
303
- return <Tabs.Tab key={screen.key}>{screen.tab}</Tabs.Tab>;
304
- })}
305
- </Tabs.Tabbar>
306
- )}
307
- </Tabs.Root>
308
- );
120
+ export const Tabs = React.memo(
121
+ React.forwardRef<TabsHandle, Omit<TabsProps, "children">>(
122
+ function Tabs(props, ref) {
123
+ const position = props.tabbarPosition ?? "bottom";
124
+ const navigation = useNavigation();
125
+ const node = useRenderNode();
126
+ const tabsId = props.id ?? node?.id ?? null;
127
+
128
+ React.useImperativeHandle(ref, () => ({
129
+ setActiveIndex(index: number) {
130
+ if (tabsId) {
131
+ navigation.setActiveTab(index, { tabsId });
132
+ }
133
+ },
134
+ }), [tabsId, navigation]);
135
+
136
+ const tabbar = React.useMemo(
137
+ () => (
138
+ <TabBar
139
+ screens={props.screens}
140
+ style={props.tabbarStyle}
141
+ position={position}
142
+ />
143
+ ),
144
+ [props.screens, props.tabbarStyle, position],
145
+ );
146
+
147
+ return (
148
+ <TabsRoot {...props}>
149
+ {position === "top" && tabbar}
150
+ <View style={styles.slotContainer}>
151
+ <TabsSlot screens={props.screens} />
152
+ </View>
153
+ {position === "bottom" && tabbar}
154
+ </TabsRoot>
155
+ );
156
+ },
157
+ ),
158
+ );
159
+
160
+ const styles = StyleSheet.create({
161
+ tabbar: {
162
+ flexDirection: "row",
163
+ justifyContent: "center",
164
+ alignItems: "center",
165
+ },
166
+ slotContainer: {
167
+ flex: 1,
168
+ },
309
169
  });
310
-
311
- export let Tabs = {
312
- Root: TabsRoot,
313
- Screens: TabsScreens,
314
- Screen: TabsScreen,
315
- Tabbar: TabsTabbar,
316
- Tab: TabsTab,
317
- Navigator: TabNavigator,
318
- };