@sigmela/router 0.3.1 → 0.3.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.
|
@@ -4,33 +4,67 @@ 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,
|
|
7
|
+
import { memo, useMemo, useState, useEffect, startTransition } from 'react';
|
|
8
8
|
import { StyleSheet } from 'react-native';
|
|
9
9
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
10
|
/**
|
|
11
|
-
* On native
|
|
12
|
-
*
|
|
11
|
+
* On native, SplitView renders primary and secondary screens in a SINGLE
|
|
12
|
+
* ScreenStack to get native push/pop animations.
|
|
13
13
|
*
|
|
14
14
|
* The combined history is: [...primaryHistory, ...secondaryHistory]
|
|
15
15
|
* This way, navigating from primary to secondary is a native push.
|
|
16
|
+
*
|
|
17
|
+
* On Android (Fabric) react-native-screens manages each ScreenStackItem via
|
|
18
|
+
* an Android Fragment. Fragment lifecycle (onCreateView) is asynchronous,
|
|
19
|
+
* but Fabric's layout pass is synchronous. If a new commit is dispatched
|
|
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
|
|
16
32
|
*/
|
|
17
33
|
export const RenderSplitView = /*#__PURE__*/memo(({
|
|
18
34
|
splitView,
|
|
19
35
|
appearance
|
|
20
36
|
}) => {
|
|
21
37
|
const router = useRouter();
|
|
22
|
-
|
|
23
|
-
// Subscribe to primary stack
|
|
24
38
|
const primaryId = splitView.primary.getId();
|
|
25
|
-
const subscribePrimary = useCallback(cb => router.subscribeStack(primaryId, cb), [router, primaryId]);
|
|
26
|
-
const getPrimary = useCallback(() => router.getStackHistory(primaryId), [router, primaryId]);
|
|
27
|
-
const primaryHistory = useSyncExternalStore(subscribePrimary, getPrimary, getPrimary);
|
|
28
|
-
|
|
29
|
-
// Subscribe to secondary stack
|
|
30
39
|
const secondaryId = splitView.secondary.getId();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
|
|
41
|
+
// Subscribe to both stacks via useEffect + startTransition.
|
|
42
|
+
// startTransition defers the Fabric commit so the FragmentManager has
|
|
43
|
+
// time to finish pending transactions between commits.
|
|
44
|
+
const [primaryHistory, setPrimaryHistory] = useState(() => router.getStackHistory(primaryId));
|
|
45
|
+
const [secondaryHistory, setSecondaryHistory] = useState(() => router.getStackHistory(secondaryId));
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const updatePrimary = () => {
|
|
48
|
+
startTransition(() => {
|
|
49
|
+
setPrimaryHistory(router.getStackHistory(primaryId));
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
const updateSecondary = () => {
|
|
53
|
+
startTransition(() => {
|
|
54
|
+
setSecondaryHistory(router.getStackHistory(secondaryId));
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const unsub1 = router.subscribeStack(primaryId, updatePrimary);
|
|
58
|
+
const unsub2 = router.subscribeStack(secondaryId, updateSecondary);
|
|
59
|
+
|
|
60
|
+
// Sync in case the store changed between useState init and useEffect
|
|
61
|
+
updatePrimary();
|
|
62
|
+
updateSecondary();
|
|
63
|
+
return () => {
|
|
64
|
+
unsub1();
|
|
65
|
+
unsub2();
|
|
66
|
+
};
|
|
67
|
+
}, [router, primaryId, secondaryId]);
|
|
34
68
|
|
|
35
69
|
// Fallback: if primary is empty, seed with first route
|
|
36
70
|
const primaryHistoryToRender = useMemo(() => {
|
|
@@ -56,9 +90,6 @@ export const RenderSplitView = /*#__PURE__*/memo(({
|
|
|
56
90
|
const combinedHistory = useMemo(() => {
|
|
57
91
|
return [...primaryHistoryToRender, ...secondaryHistory];
|
|
58
92
|
}, [primaryHistoryToRender, secondaryHistory]);
|
|
59
|
-
|
|
60
|
-
// Use primary stack ID for the combined ScreenStack
|
|
61
|
-
// (secondary items will animate as if pushed onto this stack)
|
|
62
93
|
return /*#__PURE__*/_jsx(SplitViewContext.Provider, {
|
|
63
94
|
value: splitView,
|
|
64
95
|
children: /*#__PURE__*/_jsx(ScreenStack, {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { StackRenderer } from "../StackRenderer.js";
|
|
4
4
|
import { TabBarContext } from "./TabBarContext.js";
|
|
5
5
|
import { useRouter } from "../RouterContext.js";
|
|
6
|
-
import { Tabs
|
|
6
|
+
import { Tabs } from 'react-native-screens';
|
|
7
7
|
import { Platform, StyleSheet, View } from 'react-native';
|
|
8
8
|
import { useCallback, useSyncExternalStore, memo, useEffect, useState, useMemo } from 'react';
|
|
9
9
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -190,81 +190,54 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
190
190
|
const tabKey = event.nativeEvent.tabKey;
|
|
191
191
|
const tabIndex = tabs.findIndex(route => route.tabKey === tabKey);
|
|
192
192
|
if (tabIndex === -1) return;
|
|
193
|
-
const targetTab = tabs[tabIndex];
|
|
194
|
-
if (!targetTab) return;
|
|
195
|
-
const targetStack = tabBar.stacks[targetTab.tabKey];
|
|
196
|
-
const targetNode = tabBar.nodes[targetTab.tabKey];
|
|
197
193
|
|
|
198
|
-
//
|
|
194
|
+
// Keep native Tabs.Host focus state as the source of truth.
|
|
199
195
|
if (tabIndex !== index) {
|
|
200
196
|
tabBar.onIndexChange(tabIndex);
|
|
201
197
|
}
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
}, [tabs, tabBar, index]);
|
|
199
|
+
const seedTabIfNeeded = useCallback(targetTab => {
|
|
200
|
+
if (!targetTab) return;
|
|
201
|
+
const targetStack = tabBar.stacks[targetTab.tabKey];
|
|
202
|
+
const targetNode = tabBar.nodes[targetTab.tabKey];
|
|
204
203
|
if (targetStack) {
|
|
205
204
|
const stackId = targetStack.getId();
|
|
206
205
|
const stackHistory = router.getStackHistory(stackId);
|
|
207
|
-
// Only navigate if stack is empty (first visit)
|
|
208
206
|
if (stackHistory.length === 0) {
|
|
209
207
|
const firstRoute = targetStack.getFirstRoute();
|
|
210
208
|
if (firstRoute?.path) {
|
|
211
209
|
router.navigate(firstRoute.path);
|
|
212
210
|
}
|
|
213
211
|
}
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (targetNode) {
|
|
216
215
|
const nodeId = targetNode.getId?.();
|
|
217
|
-
if (nodeId)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
216
|
+
if (!nodeId) return;
|
|
217
|
+
const nodeHistory = router.getStackHistory(nodeId);
|
|
218
|
+
if (nodeHistory.length === 0) {
|
|
219
|
+
const seed = targetNode.seed?.();
|
|
220
|
+
if (seed?.path) {
|
|
221
|
+
const prefix = targetTab.tabPrefix ?? '';
|
|
222
|
+
const fullPath = prefix && !seed.path.startsWith(prefix) ? `${prefix}${seed.path.startsWith('/') ? '' : '/'}${seed.path}` : seed.path;
|
|
223
|
+
router.navigate(fullPath);
|
|
226
224
|
}
|
|
227
225
|
}
|
|
228
226
|
}
|
|
229
|
-
}, [
|
|
227
|
+
}, [tabBar.nodes, tabBar.stacks, router]);
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
seedTabIfNeeded(tabs[index]);
|
|
230
|
+
}, [tabs, index, seedTabIfNeeded]);
|
|
230
231
|
const onTabPress = useCallback(nextIndex => {
|
|
231
232
|
const targetTab = tabs[nextIndex];
|
|
232
233
|
if (!targetTab) return;
|
|
233
|
-
const targetStack = tabBar.stacks[targetTab.tabKey];
|
|
234
|
-
const targetNode = tabBar.nodes[targetTab.tabKey];
|
|
235
234
|
|
|
236
235
|
// Update TabBar UI state
|
|
237
236
|
if (nextIndex !== index) {
|
|
238
237
|
tabBar.onIndexChange(nextIndex);
|
|
239
238
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (targetStack) {
|
|
243
|
-
const stackId = targetStack.getId();
|
|
244
|
-
const stackHistory = router.getStackHistory(stackId);
|
|
245
|
-
// Only navigate if stack is empty (first visit)
|
|
246
|
-
if (stackHistory.length === 0) {
|
|
247
|
-
const firstRoute = targetStack.getFirstRoute();
|
|
248
|
-
if (firstRoute?.path) {
|
|
249
|
-
router.navigate(firstRoute.path);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
} else if (targetNode) {
|
|
253
|
-
// For nodes like SplitView, check if we need to seed it
|
|
254
|
-
const nodeId = targetNode.getId?.();
|
|
255
|
-
if (nodeId) {
|
|
256
|
-
const nodeHistory = router.getStackHistory(nodeId);
|
|
257
|
-
if (nodeHistory.length === 0) {
|
|
258
|
-
const seed = targetNode.seed?.();
|
|
259
|
-
if (seed?.path) {
|
|
260
|
-
const prefix = targetTab.tabPrefix ?? '';
|
|
261
|
-
const fullPath = prefix && !seed.path.startsWith(prefix) ? `${prefix}${seed.path.startsWith('/') ? '' : '/'}${seed.path}` : seed.path;
|
|
262
|
-
router.navigate(fullPath);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}, [tabs, tabBar, index, router]);
|
|
239
|
+
seedTabIfNeeded(targetTab);
|
|
240
|
+
}, [tabs, tabBar, index, seedTabIfNeeded]);
|
|
268
241
|
const containerProps = useMemo(() => ({
|
|
269
242
|
tabBarBackgroundColor: backgroundColor,
|
|
270
243
|
tabBarItemTitleFontFamily: title?.fontFamily,
|
|
@@ -339,83 +312,67 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
339
312
|
}
|
|
340
313
|
}, [tabs, index]);
|
|
341
314
|
if (CustomTabBar) {
|
|
342
|
-
return /*#__PURE__*/
|
|
343
|
-
screenId: "root-tabbar",
|
|
344
|
-
headerConfig: {
|
|
345
|
-
hidden: true
|
|
346
|
-
},
|
|
347
|
-
style: StyleSheet.absoluteFill,
|
|
348
|
-
stackAnimation: "slide_from_right",
|
|
349
|
-
children: /*#__PURE__*/_jsxs(TabBarContext.Provider, {
|
|
350
|
-
value: tabBar,
|
|
351
|
-
children: [/*#__PURE__*/_jsx(View, {
|
|
352
|
-
style: styles.flex,
|
|
353
|
-
children: tabs.filter(t => visited[t.tabKey]).map(tab => {
|
|
354
|
-
const isActive = tab.tabKey === tabs[index]?.tabKey;
|
|
355
|
-
const stackForTab = tabBar.stacks[tab.tabKey];
|
|
356
|
-
const nodeForTab = tabBar.nodes[tab.tabKey];
|
|
357
|
-
const ScreenForTab = tabBar.screens[tab.tabKey];
|
|
358
|
-
return /*#__PURE__*/_jsx(View, {
|
|
359
|
-
style: [styles.flex, !isActive && styles.hidden],
|
|
360
|
-
children: stackForTab ? /*#__PURE__*/_jsx(TabStackRenderer, {
|
|
361
|
-
appearance: appearance,
|
|
362
|
-
stack: stackForTab
|
|
363
|
-
}) : nodeForTab ? /*#__PURE__*/_jsx(TabNodeRenderer, {
|
|
364
|
-
appearance: appearance,
|
|
365
|
-
node: nodeForTab
|
|
366
|
-
}) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
|
|
367
|
-
}, `tab-content-${tab.tabKey}`);
|
|
368
|
-
})
|
|
369
|
-
}), /*#__PURE__*/_jsx(CustomTabBar, {
|
|
370
|
-
onTabPress: onTabPress,
|
|
371
|
-
activeIndex: index,
|
|
372
|
-
tabs: tabs
|
|
373
|
-
})]
|
|
374
|
-
})
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
return /*#__PURE__*/_jsx(ScreenStackItem, {
|
|
378
|
-
screenId: "root-tabbar",
|
|
379
|
-
headerConfig: {
|
|
380
|
-
hidden: true
|
|
381
|
-
},
|
|
382
|
-
style: StyleSheet.absoluteFill,
|
|
383
|
-
stackAnimation: "slide_from_right",
|
|
384
|
-
children: /*#__PURE__*/_jsx(TabBarContext.Provider, {
|
|
315
|
+
return /*#__PURE__*/_jsxs(TabBarContext.Provider, {
|
|
385
316
|
value: tabBar,
|
|
386
|
-
children: /*#__PURE__*/_jsx(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const convertedIcon = tabIcons[i];
|
|
397
|
-
const {
|
|
398
|
-
icon: _icon,
|
|
399
|
-
selectedIcon: _selectedIcon,
|
|
400
|
-
tabPrefix: _prefix,
|
|
401
|
-
...tabScreenProps
|
|
402
|
-
} = tab;
|
|
403
|
-
return /*#__PURE__*/_jsx(Tabs.Screen, {
|
|
404
|
-
...tabScreenProps,
|
|
405
|
-
scrollEdgeAppearance: iosAppearance,
|
|
406
|
-
standardAppearance: iosAppearance,
|
|
407
|
-
isFocused: isFocused,
|
|
408
|
-
icon: convertedIcon?.icon,
|
|
409
|
-
selectedIcon: convertedIcon?.selectedIcon,
|
|
410
|
-
children: stack ? /*#__PURE__*/_jsx(TabStackRenderer, {
|
|
317
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
318
|
+
style: styles.flex,
|
|
319
|
+
children: tabs.filter(t => visited[t.tabKey]).map(tab => {
|
|
320
|
+
const isActive = tab.tabKey === tabs[index]?.tabKey;
|
|
321
|
+
const stackForTab = tabBar.stacks[tab.tabKey];
|
|
322
|
+
const nodeForTab = tabBar.nodes[tab.tabKey];
|
|
323
|
+
const ScreenForTab = tabBar.screens[tab.tabKey];
|
|
324
|
+
return /*#__PURE__*/_jsx(View, {
|
|
325
|
+
style: [styles.flex, !isActive && styles.hidden],
|
|
326
|
+
children: stackForTab ? /*#__PURE__*/_jsx(TabStackRenderer, {
|
|
411
327
|
appearance: appearance,
|
|
412
|
-
stack:
|
|
413
|
-
}) :
|
|
328
|
+
stack: stackForTab
|
|
329
|
+
}) : nodeForTab ? /*#__PURE__*/_jsx(TabNodeRenderer, {
|
|
414
330
|
appearance: appearance,
|
|
415
|
-
node:
|
|
416
|
-
}) :
|
|
417
|
-
}, tab.tabKey);
|
|
331
|
+
node: nodeForTab
|
|
332
|
+
}) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
|
|
333
|
+
}, `tab-content-${tab.tabKey}`);
|
|
418
334
|
})
|
|
335
|
+
}), /*#__PURE__*/_jsx(CustomTabBar, {
|
|
336
|
+
onTabPress: onTabPress,
|
|
337
|
+
activeIndex: index,
|
|
338
|
+
tabs: tabs
|
|
339
|
+
})]
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return /*#__PURE__*/_jsx(TabBarContext.Provider, {
|
|
343
|
+
value: tabBar,
|
|
344
|
+
children: /*#__PURE__*/_jsx(Tabs.Host, {
|
|
345
|
+
onNativeFocusChange: onNativeFocusChange,
|
|
346
|
+
bottomAccessory: config.bottomAccessory,
|
|
347
|
+
experimentalControlNavigationStateInJS: config.experimentalControlNavigationStateInJS,
|
|
348
|
+
...containerProps,
|
|
349
|
+
children: tabs.map((tab, i) => {
|
|
350
|
+
const isFocused = tab.tabKey === tabs[index]?.tabKey;
|
|
351
|
+
const stack = tabBar.stacks[tab.tabKey];
|
|
352
|
+
const node = tabBar.nodes[tab.tabKey];
|
|
353
|
+
const Screen = tabBar.screens[tab.tabKey];
|
|
354
|
+
const convertedIcon = tabIcons[i];
|
|
355
|
+
const {
|
|
356
|
+
icon: _icon,
|
|
357
|
+
selectedIcon: _selectedIcon,
|
|
358
|
+
tabPrefix: _prefix,
|
|
359
|
+
...tabScreenProps
|
|
360
|
+
} = tab;
|
|
361
|
+
return /*#__PURE__*/_jsx(Tabs.Screen, {
|
|
362
|
+
...tabScreenProps,
|
|
363
|
+
scrollEdgeAppearance: iosAppearance,
|
|
364
|
+
standardAppearance: iosAppearance,
|
|
365
|
+
isFocused: isFocused,
|
|
366
|
+
icon: convertedIcon?.icon,
|
|
367
|
+
selectedIcon: convertedIcon?.selectedIcon,
|
|
368
|
+
children: stack ? /*#__PURE__*/_jsx(TabStackRenderer, {
|
|
369
|
+
appearance: appearance,
|
|
370
|
+
stack: stack
|
|
371
|
+
}) : node ? /*#__PURE__*/_jsx(TabNodeRenderer, {
|
|
372
|
+
appearance: appearance,
|
|
373
|
+
node: node
|
|
374
|
+
}) : Screen ? /*#__PURE__*/_jsx(Screen, {}) : null
|
|
375
|
+
}, tab.tabKey);
|
|
419
376
|
})
|
|
420
377
|
})
|
|
421
378
|
});
|
|
@@ -5,11 +5,27 @@ export interface RenderSplitViewProps {
|
|
|
5
5
|
appearance?: NavigationAppearance;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* On native
|
|
9
|
-
*
|
|
8
|
+
* On native, SplitView renders primary and secondary screens in a SINGLE
|
|
9
|
+
* ScreenStack to get native push/pop animations.
|
|
10
10
|
*
|
|
11
11
|
* The combined history is: [...primaryHistory, ...secondaryHistory]
|
|
12
12
|
* This way, navigating from primary to secondary is a native push.
|
|
13
|
+
*
|
|
14
|
+
* On Android (Fabric) react-native-screens manages each ScreenStackItem via
|
|
15
|
+
* an Android Fragment. Fragment lifecycle (onCreateView) is asynchronous,
|
|
16
|
+
* but Fabric's layout pass is synchronous. If a new commit is dispatched
|
|
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
|
|
13
29
|
*/
|
|
14
30
|
export declare const RenderSplitView: import("react").NamedExoticComponent<RenderSplitViewProps>;
|
|
15
31
|
//# sourceMappingURL=RenderSplitView.native.d.ts.map
|