@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/package.json +6 -13
- package/readme.md +50 -757
- package/src/index.ts +2 -2
- package/src/navigation-client.test.tsx +237 -0
- package/src/navigation-client.tsx +245 -0
- package/src/navigation.tsx +41 -147
- package/src/stack.test.tsx +410 -0
- package/src/stack.tsx +87 -300
- package/src/tabs.test.tsx +359 -0
- package/src/tabs.tsx +156 -289
- 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 -40
package/src/tabs.tsx
CHANGED
|
@@ -1,313 +1,180 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
</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
|
-
};
|