@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/package.json +6 -15
- package/readme.md +68 -0
- package/src/index.ts +2 -2
- package/src/navigation-client.test.tsx +206 -0
- package/src/navigation-client.tsx +216 -0
- package/src/navigation.tsx +38 -147
- package/src/stack.test.tsx +341 -0
- package/src/stack.tsx +78 -307
- package/src/tabs.test.tsx +288 -0
- package/src/tabs.tsx +142 -291
- package/README.md +0 -788
- package/src/__tests__/navigation-reducer.test.tsx +0 -346
- package/src/__tests__/stack.test.tsx +0 -48
- package/src/contexts.tsx +0 -8
- package/src/deep-links.tsx +0 -37
- package/src/navigation-reducer.ts +0 -487
- package/src/navigation-store.ts +0 -58
- package/src/types.ts +0 -48
- package/src/utils.ts +0 -41
package/src/tabs.tsx
CHANGED
|
@@ -1,318 +1,169 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "react-native";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
37
|
+
<RenderTreeNode type="tabs" id={props.id} active={props.active}>
|
|
38
|
+
{props.children}
|
|
39
|
+
</RenderTreeNode>
|
|
89
40
|
);
|
|
90
41
|
});
|
|
91
42
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
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.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
};
|