@sigmela/router 0.3.4 → 0.3.6
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/lib/module/Drawer/RenderDrawer.native.js +58 -64
- package/lib/module/Navigation.js +1 -1
- package/lib/module/SplitView/RenderSplitView.native.js +14 -22
- package/lib/module/TabBar/RenderTabBar.native.js +11 -5
- package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +3 -15
- package/package.json +1 -1
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import { StackRenderer } from "../StackRenderer.js";
|
|
4
4
|
import { DrawerContext } from "./DrawerContext.js";
|
|
5
5
|
import { useRouter } from "../RouterContext.js";
|
|
6
|
-
import { ScreenStackItem } from 'react-native-screens';
|
|
7
6
|
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
|
8
7
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
9
8
|
import { useCallback, useSyncExternalStore, memo, useEffect, useState, useMemo, startTransition } from 'react';
|
|
9
|
+
const EMPTY_HISTORY = [];
|
|
10
10
|
import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
|
|
11
11
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
12
12
|
const TIMING_CONFIG = {
|
|
@@ -19,7 +19,9 @@ const DrawerStackRenderer = /*#__PURE__*/memo(({
|
|
|
19
19
|
}) => {
|
|
20
20
|
const router = useRouter();
|
|
21
21
|
const stackId = stack.getId();
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
// Start with empty history — useEffect populates via startTransition.
|
|
24
|
+
const [history, setHistory] = useState(EMPTY_HISTORY);
|
|
23
25
|
useEffect(() => {
|
|
24
26
|
const update = () => {
|
|
25
27
|
startTransition(() => {
|
|
@@ -133,69 +135,61 @@ export const RenderDrawer = /*#__PURE__*/memo(({
|
|
|
133
135
|
}
|
|
134
136
|
}, [tabs, index]);
|
|
135
137
|
const CustomDrawer = config.component;
|
|
136
|
-
return /*#__PURE__*/_jsx(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
onPress: () => onItemPress(i),
|
|
165
|
-
style: [styles.item, isActive && styles.itemActive],
|
|
166
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
167
|
-
style: [styles.itemText, isActive && styles.itemTextActive],
|
|
168
|
-
children: tab.title
|
|
169
|
-
})
|
|
170
|
-
}, tab.tabKey);
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
174
|
-
style: [styles.overlay, overlayStyle],
|
|
175
|
-
children: /*#__PURE__*/_jsx(Pressable, {
|
|
176
|
-
style: StyleSheet.absoluteFill,
|
|
177
|
-
onPress: handleOverlayPress
|
|
178
|
-
})
|
|
179
|
-
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
180
|
-
style: [styles.main, mainStyle],
|
|
181
|
-
children: tabs.filter(t => visited[t.tabKey]).map(tab => {
|
|
182
|
-
const isActive = tab.tabKey === tabs[index]?.tabKey;
|
|
183
|
-
const stackForTab = drawer.stacks[tab.tabKey];
|
|
184
|
-
const nodeForTab = drawer.nodes[tab.tabKey];
|
|
185
|
-
const ScreenForTab = drawer.screens[tab.tabKey];
|
|
186
|
-
return /*#__PURE__*/_jsx(View, {
|
|
187
|
-
style: [styles.flex, !isActive && styles.hidden],
|
|
188
|
-
children: stackForTab ? /*#__PURE__*/_jsx(DrawerStackRenderer, {
|
|
189
|
-
appearance: appearance,
|
|
190
|
-
stack: stackForTab
|
|
191
|
-
}) : nodeForTab ? /*#__PURE__*/_jsx(DrawerNodeRenderer, {
|
|
192
|
-
appearance: appearance,
|
|
193
|
-
node: nodeForTab
|
|
194
|
-
}) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
|
|
195
|
-
}, `drawer-content-${tab.tabKey}`);
|
|
138
|
+
return /*#__PURE__*/_jsx(DrawerContext.Provider, {
|
|
139
|
+
value: drawer,
|
|
140
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
141
|
+
style: styles.container,
|
|
142
|
+
children: [/*#__PURE__*/_jsx(Animated.View, {
|
|
143
|
+
style: [styles.sidebar, {
|
|
144
|
+
width: drawerWidth
|
|
145
|
+
}, drawerStyle],
|
|
146
|
+
children: CustomDrawer ? /*#__PURE__*/_jsx(CustomDrawer, {
|
|
147
|
+
onItemPress: onItemPress,
|
|
148
|
+
activeIndex: index,
|
|
149
|
+
items: tabs,
|
|
150
|
+
isOpen: isOpen,
|
|
151
|
+
onClose: handleOverlayPress
|
|
152
|
+
}) : /*#__PURE__*/_jsx(View, {
|
|
153
|
+
style: [styles.sidebarContent, {
|
|
154
|
+
paddingTop: insets.top + 12
|
|
155
|
+
}],
|
|
156
|
+
children: tabs.map((tab, i) => {
|
|
157
|
+
const isActive = i === index;
|
|
158
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
159
|
+
onPress: () => onItemPress(i),
|
|
160
|
+
style: [styles.item, isActive && styles.itemActive],
|
|
161
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
162
|
+
style: [styles.itemText, isActive && styles.itemTextActive],
|
|
163
|
+
children: tab.title
|
|
164
|
+
})
|
|
165
|
+
}, tab.tabKey);
|
|
196
166
|
})
|
|
197
|
-
})
|
|
198
|
-
})
|
|
167
|
+
})
|
|
168
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
169
|
+
style: [styles.overlay, overlayStyle],
|
|
170
|
+
children: /*#__PURE__*/_jsx(Pressable, {
|
|
171
|
+
style: StyleSheet.absoluteFill,
|
|
172
|
+
onPress: handleOverlayPress
|
|
173
|
+
})
|
|
174
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
175
|
+
style: [styles.main, mainStyle],
|
|
176
|
+
children: tabs.filter(t => visited[t.tabKey]).map(tab => {
|
|
177
|
+
const isActive = tab.tabKey === tabs[index]?.tabKey;
|
|
178
|
+
const stackForTab = drawer.stacks[tab.tabKey];
|
|
179
|
+
const nodeForTab = drawer.nodes[tab.tabKey];
|
|
180
|
+
const ScreenForTab = drawer.screens[tab.tabKey];
|
|
181
|
+
return /*#__PURE__*/_jsx(View, {
|
|
182
|
+
style: [styles.flex, !isActive && styles.hidden],
|
|
183
|
+
children: stackForTab ? /*#__PURE__*/_jsx(DrawerStackRenderer, {
|
|
184
|
+
appearance: appearance,
|
|
185
|
+
stack: stackForTab
|
|
186
|
+
}) : nodeForTab ? /*#__PURE__*/_jsx(DrawerNodeRenderer, {
|
|
187
|
+
appearance: appearance,
|
|
188
|
+
node: nodeForTab
|
|
189
|
+
}) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
|
|
190
|
+
}, `drawer-content-${tab.tabKey}`);
|
|
191
|
+
})
|
|
192
|
+
})]
|
|
199
193
|
})
|
|
200
194
|
});
|
|
201
195
|
});
|
package/lib/module/Navigation.js
CHANGED
|
@@ -8,7 +8,7 @@ import { memo, useEffect, useRef, useState, startTransition } from 'react';
|
|
|
8
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
9
|
const EMPTY_HISTORY = [];
|
|
10
10
|
function useStackHistory(router, stackId) {
|
|
11
|
-
const [history, setHistory] = useState(
|
|
11
|
+
const [history, setHistory] = useState(EMPTY_HISTORY);
|
|
12
12
|
useEffect(() => {
|
|
13
13
|
if (!stackId) {
|
|
14
14
|
startTransition(() => {
|
|
@@ -4,9 +4,10 @@ import { ScreenStackItem } from "../ScreenStackItem/index.js";
|
|
|
4
4
|
import { ScreenStack } from "../ScreenStack/index.js";
|
|
5
5
|
import { SplitViewContext } from "./SplitViewContext.js";
|
|
6
6
|
import { useRouter } from "../RouterContext.js";
|
|
7
|
-
import { memo, useMemo, useState, useEffect, startTransition } from 'react';
|
|
7
|
+
import { memo, useMemo, useRef, useState, useEffect, startTransition } from 'react';
|
|
8
8
|
import { StyleSheet } from 'react-native';
|
|
9
9
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
const EMPTY_HISTORY = [];
|
|
10
11
|
/**
|
|
11
12
|
* On native, SplitView renders primary and secondary screens in a SINGLE
|
|
12
13
|
* ScreenStack to get native push/pop animations.
|
|
@@ -14,21 +15,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
14
15
|
* The combined history is: [...primaryHistory, ...secondaryHistory]
|
|
15
16
|
* This way, navigating from primary to secondary is a native push.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* while the previous Fragment transaction is still in flight, Fabric tries
|
|
21
|
-
* to insert child views into a Screen whose Fragment isn't attached yet:
|
|
22
|
-
*
|
|
23
|
-
* "addViewAt: Parent Screen does not have its Fragment attached"
|
|
24
|
-
*
|
|
25
|
-
* The fix follows the same pattern used in react-native-screens' own
|
|
26
|
-
* BottomTabsContainer example: subscribe via useEffect and apply updates
|
|
27
|
-
* inside `startTransition`. This marks the update as non-urgent, letting
|
|
28
|
-
* React defer the Fabric commit until pending Fragment transactions complete.
|
|
29
|
-
*
|
|
30
|
-
* See: https://github.com/software-mansion/react-native-screens/blob/ddd1a9e/
|
|
31
|
-
* apps/src/shared/gamma/containers/bottom-tabs/BottomTabsContainer.tsx
|
|
18
|
+
* History starts empty and is populated via useEffect + startTransition,
|
|
19
|
+
* following the pattern from react-native-screens' BottomTabsContainer.
|
|
20
|
+
* This lets the parent Screen attach before child ScreenStackItems mount.
|
|
32
21
|
*/
|
|
33
22
|
export const RenderSplitView = /*#__PURE__*/memo(({
|
|
34
23
|
splitView,
|
|
@@ -38,12 +27,12 @@ export const RenderSplitView = /*#__PURE__*/memo(({
|
|
|
38
27
|
const primaryId = splitView.primary.getId();
|
|
39
28
|
const secondaryId = splitView.secondary.getId();
|
|
40
29
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const [
|
|
45
|
-
const [secondaryHistory, setSecondaryHistory] = useState(() => router.getStackHistory(secondaryId));
|
|
30
|
+
// Start with empty history — useEffect populates via startTransition.
|
|
31
|
+
const hydratedRef = useRef(false);
|
|
32
|
+
const [primaryHistory, setPrimaryHistory] = useState(EMPTY_HISTORY);
|
|
33
|
+
const [secondaryHistory, setSecondaryHistory] = useState(EMPTY_HISTORY);
|
|
46
34
|
useEffect(() => {
|
|
35
|
+
hydratedRef.current = true;
|
|
47
36
|
const updatePrimary = () => {
|
|
48
37
|
startTransition(() => {
|
|
49
38
|
setPrimaryHistory(router.getStackHistory(primaryId));
|
|
@@ -66,11 +55,14 @@ export const RenderSplitView = /*#__PURE__*/memo(({
|
|
|
66
55
|
};
|
|
67
56
|
}, [router, primaryId, secondaryId]);
|
|
68
57
|
|
|
69
|
-
// Fallback: if primary is empty, seed with first route
|
|
58
|
+
// Fallback: if primary is empty, seed with first route.
|
|
59
|
+
// Skip the seed during the initial empty mount (before useEffect
|
|
60
|
+
// hydrates history) so no ScreenStackItems mount before the parent Fragment attaches.
|
|
70
61
|
const primaryHistoryToRender = useMemo(() => {
|
|
71
62
|
if (primaryHistory.length > 0) {
|
|
72
63
|
return primaryHistory;
|
|
73
64
|
}
|
|
65
|
+
if (!hydratedRef.current) return [];
|
|
74
66
|
const first = splitView.primary.getFirstRoute();
|
|
75
67
|
if (!first) return [];
|
|
76
68
|
const activePath = router.getActiveRoute()?.path;
|
|
@@ -7,6 +7,7 @@ import { Tabs } from 'react-native-screens';
|
|
|
7
7
|
import { Platform, StyleSheet, View } from 'react-native';
|
|
8
8
|
import { useCallback, useSyncExternalStore, memo, useEffect, useState, useMemo, startTransition } from 'react';
|
|
9
9
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
|
+
const EMPTY_HISTORY = [];
|
|
10
11
|
const isImageSource = value => {
|
|
11
12
|
if (value == null) return false;
|
|
12
13
|
const valueType = typeof value;
|
|
@@ -136,7 +137,10 @@ const TabStackRenderer = /*#__PURE__*/memo(({
|
|
|
136
137
|
}) => {
|
|
137
138
|
const router = useRouter();
|
|
138
139
|
const stackId = stack.getId();
|
|
139
|
-
|
|
140
|
+
|
|
141
|
+
// Start with empty history — useEffect populates via startTransition.
|
|
142
|
+
// This avoids mounting ScreenStackItems before parent Fragments attach.
|
|
143
|
+
const [history, setHistory] = useState(EMPTY_HISTORY);
|
|
140
144
|
useEffect(() => {
|
|
141
145
|
const update = () => {
|
|
142
146
|
startTransition(() => {
|
|
@@ -145,7 +149,7 @@ const TabStackRenderer = /*#__PURE__*/memo(({
|
|
|
145
149
|
};
|
|
146
150
|
const unsub = router.subscribeStack(stackId, update);
|
|
147
151
|
update();
|
|
148
|
-
return
|
|
152
|
+
return unsub;
|
|
149
153
|
}, [router, stackId]);
|
|
150
154
|
return /*#__PURE__*/_jsx(StackRenderer, {
|
|
151
155
|
appearance: appearance,
|
|
@@ -330,9 +334,11 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
330
334
|
useEffect(() => {
|
|
331
335
|
const key = tabs[index]?.tabKey;
|
|
332
336
|
if (key) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
337
|
+
startTransition(() => {
|
|
338
|
+
setVisited(prev => prev[key] ? prev : {
|
|
339
|
+
...prev,
|
|
340
|
+
[key]: true
|
|
341
|
+
});
|
|
336
342
|
});
|
|
337
343
|
}
|
|
338
344
|
}, [tabs, index]);
|
|
@@ -11,21 +11,9 @@ export interface RenderSplitViewProps {
|
|
|
11
11
|
* The combined history is: [...primaryHistory, ...secondaryHistory]
|
|
12
12
|
* This way, navigating from primary to secondary is a native push.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* while the previous Fragment transaction is still in flight, Fabric tries
|
|
18
|
-
* to insert child views into a Screen whose Fragment isn't attached yet:
|
|
19
|
-
*
|
|
20
|
-
* "addViewAt: Parent Screen does not have its Fragment attached"
|
|
21
|
-
*
|
|
22
|
-
* The fix follows the same pattern used in react-native-screens' own
|
|
23
|
-
* BottomTabsContainer example: subscribe via useEffect and apply updates
|
|
24
|
-
* inside `startTransition`. This marks the update as non-urgent, letting
|
|
25
|
-
* React defer the Fabric commit until pending Fragment transactions complete.
|
|
26
|
-
*
|
|
27
|
-
* See: https://github.com/software-mansion/react-native-screens/blob/ddd1a9e/
|
|
28
|
-
* apps/src/shared/gamma/containers/bottom-tabs/BottomTabsContainer.tsx
|
|
14
|
+
* History starts empty and is populated via useEffect + startTransition,
|
|
15
|
+
* following the pattern from react-native-screens' BottomTabsContainer.
|
|
16
|
+
* This lets the parent Screen attach before child ScreenStackItems mount.
|
|
29
17
|
*/
|
|
30
18
|
export declare const RenderSplitView: import("react").NamedExoticComponent<RenderSplitViewProps>;
|
|
31
19
|
//# sourceMappingURL=RenderSplitView.native.d.ts.map
|