@rn-tools/navigation 2.3.0 → 3.0.2

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