@sigmela/router 0.2.2 → 0.2.3
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/Router.js +8 -0
- package/lib/module/TabBar/RenderTabBar.native.js +19 -1
- package/lib/module/TabBar/RenderTabBar.web.js +39 -0
- package/lib/module/TabBar/TabBar.js +21 -9
- package/lib/module/styles.css +4 -2
- package/lib/typescript/src/TabBar/TabBar.d.ts +23 -0
- package/package.json +1 -1
package/lib/module/Router.js
CHANGED
|
@@ -367,6 +367,14 @@ export class Router {
|
|
|
367
367
|
}
|
|
368
368
|
return;
|
|
369
369
|
}
|
|
370
|
+
|
|
371
|
+
// Ensure the root-level container (e.g. TabBar) is activated for the matched route.
|
|
372
|
+
// This is important when the matched route belongs to a nested stack inside a composite node
|
|
373
|
+
// like SplitView: stackActivators won't fire for the tab itself in that case.
|
|
374
|
+
const rootStackId = this.root?.getId();
|
|
375
|
+
if (rootStackId) {
|
|
376
|
+
this.activateContainerForRoute(base.routeId, rootStackId);
|
|
377
|
+
}
|
|
370
378
|
const activator = base.stackId ? this.stackActivators.get(base.stackId) : undefined;
|
|
371
379
|
if (activator) {
|
|
372
380
|
activator();
|
|
@@ -5,7 +5,7 @@ import { TabBarContext } from "./TabBarContext.js";
|
|
|
5
5
|
import { useRouter } from "../RouterContext.js";
|
|
6
6
|
import { BottomTabsScreen, BottomTabs, ScreenStackItem } from 'react-native-screens';
|
|
7
7
|
import { Platform, StyleSheet, View } from 'react-native';
|
|
8
|
-
import { useCallback, useSyncExternalStore, memo, useEffect, useState } from 'react';
|
|
8
|
+
import { useCallback, useSyncExternalStore, memo, useEffect, useState, useMemo } from 'react';
|
|
9
9
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
10
|
const isImageSource = value => {
|
|
11
11
|
if (value == null) return false;
|
|
@@ -146,6 +146,16 @@ const TabStackRenderer = /*#__PURE__*/memo(({
|
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
148
|
TabStackRenderer.displayName = 'TabStackRenderer';
|
|
149
|
+
const TabNodeRenderer = /*#__PURE__*/memo(({
|
|
150
|
+
node,
|
|
151
|
+
appearance
|
|
152
|
+
}) => {
|
|
153
|
+
const Renderer = useMemo(() => node.getRenderer(), [node]);
|
|
154
|
+
return /*#__PURE__*/_jsx(Renderer, {
|
|
155
|
+
appearance: appearance
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
TabNodeRenderer.displayName = 'TabNodeRenderer';
|
|
149
159
|
export const RenderTabBar = /*#__PURE__*/memo(({
|
|
150
160
|
tabBar,
|
|
151
161
|
appearance = {}
|
|
@@ -245,12 +255,16 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
245
255
|
children: tabs.filter(t => visited[t.tabKey]).map(tab => {
|
|
246
256
|
const isActive = tab.tabKey === tabs[index]?.tabKey;
|
|
247
257
|
const stackForTab = tabBar.stacks[tab.tabKey];
|
|
258
|
+
const nodeForTab = tabBar.nodes[tab.tabKey];
|
|
248
259
|
const ScreenForTab = tabBar.screens[tab.tabKey];
|
|
249
260
|
return /*#__PURE__*/_jsx(View, {
|
|
250
261
|
style: [styles.flex, !isActive && styles.hidden],
|
|
251
262
|
children: stackForTab ? /*#__PURE__*/_jsx(TabStackRenderer, {
|
|
252
263
|
appearance: appearance,
|
|
253
264
|
stack: stackForTab
|
|
265
|
+
}) : nodeForTab ? /*#__PURE__*/_jsx(TabNodeRenderer, {
|
|
266
|
+
appearance: appearance,
|
|
267
|
+
node: nodeForTab
|
|
254
268
|
}) : ScreenForTab ? /*#__PURE__*/_jsx(ScreenForTab, {}) : null
|
|
255
269
|
}, `tab-content-${tab.tabKey}`);
|
|
256
270
|
})
|
|
@@ -277,6 +291,7 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
277
291
|
children: tabs.map(tab => {
|
|
278
292
|
const isFocused = tab.tabKey === tabs[index]?.tabKey;
|
|
279
293
|
const stack = tabBar.stacks[tab.tabKey];
|
|
294
|
+
const node = tabBar.nodes[tab.tabKey];
|
|
280
295
|
const Screen = tabBar.screens[tab.tabKey];
|
|
281
296
|
const icon = getTabIcon(tab);
|
|
282
297
|
return /*#__PURE__*/_jsx(BottomTabsScreen, {
|
|
@@ -292,6 +307,9 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
292
307
|
children: stack ? /*#__PURE__*/_jsx(TabStackRenderer, {
|
|
293
308
|
appearance: appearance,
|
|
294
309
|
stack: stack
|
|
310
|
+
}) : node ? /*#__PURE__*/_jsx(TabNodeRenderer, {
|
|
311
|
+
appearance: appearance,
|
|
312
|
+
node: node
|
|
295
313
|
}) : Screen ? /*#__PURE__*/_jsx(Screen, {}) : null
|
|
296
314
|
}, tab.tabKey);
|
|
297
315
|
})
|
|
@@ -34,6 +34,29 @@ const TabStackRenderer = /*#__PURE__*/memo(({
|
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
36
|
TabStackRenderer.displayName = 'TabStackRenderer';
|
|
37
|
+
const TabNodeRenderer = /*#__PURE__*/memo(({
|
|
38
|
+
node,
|
|
39
|
+
appearance
|
|
40
|
+
}) => {
|
|
41
|
+
const Renderer = useMemo(() => node.getRenderer(), [node]);
|
|
42
|
+
return /*#__PURE__*/_jsx(Renderer, {
|
|
43
|
+
appearance: appearance
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
TabNodeRenderer.displayName = 'TabNodeRenderer';
|
|
47
|
+
const joinPrefixAndPath = (prefix, path) => {
|
|
48
|
+
const base = (prefix ?? '').trim();
|
|
49
|
+
const child = (path || '/').trim();
|
|
50
|
+
if (!base) return child || '/';
|
|
51
|
+
const baseNorm = base === '/' ? '' : base.endsWith('/') ? base.slice(0, -1) : base;
|
|
52
|
+
if (!child || child === '/') {
|
|
53
|
+
return baseNorm || '/';
|
|
54
|
+
}
|
|
55
|
+
if (child.startsWith('/')) {
|
|
56
|
+
return `${baseNorm}${child}`;
|
|
57
|
+
}
|
|
58
|
+
return `${baseNorm}/${child}`;
|
|
59
|
+
};
|
|
37
60
|
export const RenderTabBar = /*#__PURE__*/memo(({
|
|
38
61
|
tabBar,
|
|
39
62
|
appearance
|
|
@@ -48,11 +71,13 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
48
71
|
} = snapshot;
|
|
49
72
|
const focusedTab = tabs[index];
|
|
50
73
|
const stack = focusedTab ? tabBar.stacks[focusedTab.tabKey] : undefined;
|
|
74
|
+
const node = focusedTab ? tabBar.nodes[focusedTab.tabKey] : undefined;
|
|
51
75
|
const Screen = focusedTab ? tabBar.screens[focusedTab.tabKey] : undefined;
|
|
52
76
|
const onTabClick = useCallback(nextIndex => {
|
|
53
77
|
const targetTab = tabs[nextIndex];
|
|
54
78
|
if (!targetTab) return;
|
|
55
79
|
const targetStack = tabBar.stacks[targetTab.tabKey];
|
|
80
|
+
const targetNode = tabBar.nodes[targetTab.tabKey];
|
|
56
81
|
if (targetStack) {
|
|
57
82
|
// Keep TabBar UI in sync immediately.
|
|
58
83
|
if (nextIndex !== index) {
|
|
@@ -65,6 +90,17 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
65
90
|
router.reset(firstRoutePath);
|
|
66
91
|
return;
|
|
67
92
|
}
|
|
93
|
+
} else if (targetNode) {
|
|
94
|
+
// Keep TabBar UI in sync immediately.
|
|
95
|
+
if (nextIndex !== index) {
|
|
96
|
+
tabBar.onIndexChange(nextIndex);
|
|
97
|
+
}
|
|
98
|
+
const seedPath = targetNode.seed?.()?.path;
|
|
99
|
+
const fallbackFirstPath = targetNode.getNodeRoutes()?.[0]?.path;
|
|
100
|
+
const path = seedPath ?? fallbackFirstPath ?? '/';
|
|
101
|
+
const fullPath = joinPrefixAndPath(targetTab.tabPrefix, path);
|
|
102
|
+
router.reset(fullPath);
|
|
103
|
+
return;
|
|
68
104
|
}
|
|
69
105
|
if (nextIndex !== index) {
|
|
70
106
|
tabBar.onIndexChange(nextIndex);
|
|
@@ -97,6 +133,9 @@ export const RenderTabBar = /*#__PURE__*/memo(({
|
|
|
97
133
|
children: [stack ? /*#__PURE__*/_jsx(TabStackRenderer, {
|
|
98
134
|
appearance: appearance,
|
|
99
135
|
stack: stack
|
|
136
|
+
}) : node ? /*#__PURE__*/_jsx(TabNodeRenderer, {
|
|
137
|
+
appearance: appearance,
|
|
138
|
+
node: node
|
|
100
139
|
}) : Screen ? /*#__PURE__*/_jsx(Screen, {}) : null, CustomTabBar ? /*#__PURE__*/_jsx(CustomTabBar, {
|
|
101
140
|
onTabPress: onTabClick,
|
|
102
141
|
activeIndex: index,
|
|
@@ -8,6 +8,7 @@ import { RenderTabBar } from './RenderTabBar';
|
|
|
8
8
|
export class TabBar {
|
|
9
9
|
screens = {};
|
|
10
10
|
stacks = {};
|
|
11
|
+
nodes = {};
|
|
11
12
|
listeners = new Set();
|
|
12
13
|
constructor(options = {}) {
|
|
13
14
|
this.tabBarId = `tabbar-${Math.random().toString(36).slice(2)}`;
|
|
@@ -23,6 +24,10 @@ export class TabBar {
|
|
|
23
24
|
return this.tabBarId;
|
|
24
25
|
}
|
|
25
26
|
addTab(tab) {
|
|
27
|
+
const sourcesCount = (tab.stack ? 1 : 0) + (tab.node ? 1 : 0) + (tab.screen ? 1 : 0);
|
|
28
|
+
if (sourcesCount !== 1) {
|
|
29
|
+
throw new Error(`TabBar.addTab: exactly one of { stack, node, screen } must be provided (got ${sourcesCount})`);
|
|
30
|
+
}
|
|
26
31
|
const {
|
|
27
32
|
key,
|
|
28
33
|
...rest
|
|
@@ -31,6 +36,7 @@ export class TabBar {
|
|
|
31
36
|
const tabKey = key ?? `tab-${nextIndex}`;
|
|
32
37
|
const nextTabs = [...this.state.tabs, {
|
|
33
38
|
tabKey,
|
|
39
|
+
tabPrefix: tab.prefix,
|
|
34
40
|
...rest
|
|
35
41
|
}];
|
|
36
42
|
this.setState({
|
|
@@ -38,6 +44,8 @@ export class TabBar {
|
|
|
38
44
|
});
|
|
39
45
|
if (tab.stack) {
|
|
40
46
|
this.stacks[tabKey] = tab.stack;
|
|
47
|
+
} else if (tab.node) {
|
|
48
|
+
this.nodes[tabKey] = tab.node;
|
|
41
49
|
} else if (tab.screen) {
|
|
42
50
|
this.screens[tabKey] = tab.screen;
|
|
43
51
|
}
|
|
@@ -102,8 +110,8 @@ export class TabBar {
|
|
|
102
110
|
getActiveChildId() {
|
|
103
111
|
const activeTab = this.state.tabs[this.state.index];
|
|
104
112
|
if (!activeTab) return undefined;
|
|
105
|
-
const
|
|
106
|
-
return
|
|
113
|
+
const node = this.nodes[activeTab.tabKey] ?? this.stacks[activeTab.tabKey];
|
|
114
|
+
return node?.getId();
|
|
107
115
|
}
|
|
108
116
|
switchToRoute(routeId) {
|
|
109
117
|
const idx = this.findTabIndexByRoute(routeId);
|
|
@@ -126,11 +134,11 @@ export class TabBar {
|
|
|
126
134
|
const children = [];
|
|
127
135
|
for (let idx = 0; idx < this.state.tabs.length; idx++) {
|
|
128
136
|
const tab = this.state.tabs[idx];
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
137
|
+
const node = tab ? this.nodes[tab.tabKey] ?? this.stacks[tab.tabKey] : undefined;
|
|
138
|
+
if (node) {
|
|
131
139
|
children.push({
|
|
132
|
-
prefix: '',
|
|
133
|
-
node
|
|
140
|
+
prefix: tab?.tabPrefix ?? '',
|
|
141
|
+
node,
|
|
134
142
|
onMatch: () => this.onIndexChange(idx)
|
|
135
143
|
});
|
|
136
144
|
}
|
|
@@ -150,6 +158,10 @@ export class TabBar {
|
|
|
150
158
|
seed() {
|
|
151
159
|
const activeTab = this.state.tabs[this.state.index];
|
|
152
160
|
if (!activeTab) return null;
|
|
161
|
+
const node = this.nodes[activeTab.tabKey];
|
|
162
|
+
if (node) {
|
|
163
|
+
return node.seed?.() ?? null;
|
|
164
|
+
}
|
|
153
165
|
const stack = this.stacks[activeTab.tabKey];
|
|
154
166
|
if (!stack) return null;
|
|
155
167
|
const firstRoute = stack.getFirstRoute();
|
|
@@ -163,9 +175,9 @@ export class TabBar {
|
|
|
163
175
|
findTabIndexByRoute(routeId) {
|
|
164
176
|
for (let i = 0; i < this.state.tabs.length; i++) {
|
|
165
177
|
const tab = this.state.tabs[i];
|
|
166
|
-
const
|
|
167
|
-
if (!
|
|
168
|
-
const hasRoute = this.nodeHasRoute(
|
|
178
|
+
const node = tab ? this.nodes[tab.tabKey] ?? this.stacks[tab.tabKey] : undefined;
|
|
179
|
+
if (!node) continue;
|
|
180
|
+
const hasRoute = this.nodeHasRoute(node, routeId);
|
|
169
181
|
if (hasRoute) {
|
|
170
182
|
return i;
|
|
171
183
|
}
|
package/lib/module/styles.css
CHANGED
|
@@ -489,7 +489,8 @@
|
|
|
489
489
|
position: relative;
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
.tab-stacks-container > .screen-stack
|
|
492
|
+
.tab-stacks-container > .screen-stack,
|
|
493
|
+
.tab-stacks-container > .split-view-container {
|
|
493
494
|
padding-bottom: calc(73px + env(safe-area-inset-bottom));
|
|
494
495
|
}
|
|
495
496
|
|
|
@@ -725,7 +726,8 @@
|
|
|
725
726
|
flex-direction: row;
|
|
726
727
|
}
|
|
727
728
|
|
|
728
|
-
.tab-stacks-container > .screen-stack
|
|
729
|
+
.tab-stacks-container > .screen-stack,
|
|
730
|
+
.tab-stacks-container > .split-view-container {
|
|
729
731
|
padding-bottom: 0;
|
|
730
732
|
margin-left: 260px;
|
|
731
733
|
min-width: 0;
|
|
@@ -17,6 +17,11 @@ export type InternalTabItem = Omit<TabItem, 'icon' | 'selectedIcon'> & {
|
|
|
17
17
|
icon?: ExtendedIcon;
|
|
18
18
|
selectedIcon?: ExtendedIcon;
|
|
19
19
|
badgeValue?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Optional base prefix for this tab's navigation subtree, e.g. '/mail'.
|
|
22
|
+
* Used by Router registry building via TabBar.getNodeChildren().
|
|
23
|
+
*/
|
|
24
|
+
tabPrefix?: string;
|
|
20
25
|
};
|
|
21
26
|
export type TabBarProps = {
|
|
22
27
|
onTabPress: (index: number) => void;
|
|
@@ -27,8 +32,25 @@ export type TabBarDescriptor = {
|
|
|
27
32
|
renderer?: ComponentType<TabBarProps>;
|
|
28
33
|
};
|
|
29
34
|
type TabBarConfig = Omit<InternalTabItem, 'tabKey' | 'key'> & {
|
|
35
|
+
/**
|
|
36
|
+
* Legacy content type: a stack rendered in the tab.
|
|
37
|
+
*/
|
|
30
38
|
stack?: NavigationStack;
|
|
39
|
+
/**
|
|
40
|
+
* New content type: any NavigationNode (e.g. SplitView).
|
|
41
|
+
*/
|
|
42
|
+
node?: NavigationNode;
|
|
43
|
+
/**
|
|
44
|
+
* Screen component rendered in the tab (no routing).
|
|
45
|
+
*/
|
|
31
46
|
screen?: React.ComponentType<any>;
|
|
47
|
+
/**
|
|
48
|
+
* Optional base prefix for node/stack routes, e.g. '/mail'.
|
|
49
|
+
*/
|
|
50
|
+
prefix?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Custom tab bar component (UI). Kept for compatibility.
|
|
53
|
+
*/
|
|
32
54
|
component?: ComponentType<TabBarProps>;
|
|
33
55
|
};
|
|
34
56
|
type TabBarOptions = {
|
|
@@ -40,6 +62,7 @@ export declare class TabBar implements NavigationNode {
|
|
|
40
62
|
private readonly tabBarId;
|
|
41
63
|
screens: Record<string, React.ComponentType<any>>;
|
|
42
64
|
stacks: Record<string, NavigationStack>;
|
|
65
|
+
nodes: Record<string, NavigationNode>;
|
|
43
66
|
private listeners;
|
|
44
67
|
private state;
|
|
45
68
|
constructor(options?: TabBarOptions);
|