@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rn-tools/navigation",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"main": "./src/index.ts",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
],
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "NODE_OPTIONS='--no-experimental-detect-module' vitest",
|
|
12
12
|
"lint": "eslint -c ./.eslintrc.js ."
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@rn-tools/eslint-config": "*",
|
|
16
|
-
"@testing-library/react
|
|
17
|
-
"@types/jest": "^27.0.3",
|
|
16
|
+
"@testing-library/react": "^14.2.1",
|
|
18
17
|
"@types/react": "~18.2.79",
|
|
19
18
|
"@types/react-native": "^0.72.8",
|
|
20
19
|
"@types/react-test-renderer": "^18",
|
|
@@ -22,28 +21,20 @@
|
|
|
22
21
|
"@typescript-eslint/parser": "^6.8.0",
|
|
23
22
|
"babel-preset-expo": "~11.0.0",
|
|
24
23
|
"eslint": "^8.56.0",
|
|
25
|
-
"expo": "
|
|
26
|
-
"jest": "^29.7.0",
|
|
27
|
-
"jest-expo": "^51.0.2",
|
|
24
|
+
"expo": "~52.0.0",
|
|
28
25
|
"lint-staged": "^15.2.0",
|
|
29
26
|
"prettier": "^3.2.1",
|
|
30
27
|
"react-test-renderer": "18.2.0",
|
|
31
|
-
"typescript": "~5.3.3"
|
|
28
|
+
"typescript": "~5.3.3",
|
|
29
|
+
"vitest": "^1.6.0"
|
|
32
30
|
},
|
|
33
31
|
"peerDependencies": {
|
|
34
32
|
"react": "*",
|
|
35
33
|
"react-native": "*",
|
|
36
|
-
"react-native-safe-area-context": "*",
|
|
37
34
|
"react-native-screens": "*"
|
|
38
35
|
},
|
|
39
36
|
"dependencies": {
|
|
40
37
|
"path-to-regexp": "^6.2.2",
|
|
41
38
|
"zustand": "^4.5.2"
|
|
42
|
-
},
|
|
43
|
-
"jest": {
|
|
44
|
-
"preset": "jest-expo",
|
|
45
|
-
"transformIgnorePatterns": [
|
|
46
|
-
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)"
|
|
47
|
-
]
|
|
48
39
|
}
|
|
49
40
|
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @rn-tools/navigation
|
|
2
|
+
|
|
3
|
+
Navigation primitives for React Native. Built on `react-native-screens`.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn expo install @rn-tools/navigation react-native-screens
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import {
|
|
15
|
+
createNavigation,
|
|
16
|
+
Navigation,
|
|
17
|
+
Stack,
|
|
18
|
+
Tabs,
|
|
19
|
+
type TabScreenOptions,
|
|
20
|
+
} from "@rn-tools/navigation";
|
|
21
|
+
|
|
22
|
+
const navigation = createNavigation();
|
|
23
|
+
|
|
24
|
+
const tabScreens: TabScreenOptions[] = [
|
|
25
|
+
{
|
|
26
|
+
id: "home",
|
|
27
|
+
screen: <Stack id="home" rootScreen={<HomeScreen />} />,
|
|
28
|
+
tab: ({ isActive, onPress }) => (
|
|
29
|
+
<Pressable onPress={onPress}>
|
|
30
|
+
<Text style={{ fontWeight: isActive ? "bold" : "normal" }}>Home</Text>
|
|
31
|
+
</Pressable>
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "settings",
|
|
36
|
+
screen: <SettingsScreen />,
|
|
37
|
+
tab: ({ isActive, onPress }) => (
|
|
38
|
+
<Pressable onPress={onPress}>
|
|
39
|
+
<Text style={{ fontWeight: isActive ? "bold" : "normal" }}>Settings</Text>
|
|
40
|
+
</Pressable>
|
|
41
|
+
),
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export default function App() {
|
|
46
|
+
return (
|
|
47
|
+
<Navigation navigation={navigation}>
|
|
48
|
+
<Tabs id="main-tabs" screens={tabScreens} />
|
|
49
|
+
</Navigation>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Push and pop screens imperatively:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
navigation.pushScreen(<DetailScreen />, { id: "detail" });
|
|
58
|
+
navigation.popScreen();
|
|
59
|
+
navigation.setActiveTab(1);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
When no explicit target is provided, these methods automatically resolve the deepest active stack or tabs instance.
|
|
63
|
+
|
|
64
|
+
## Docs
|
|
65
|
+
|
|
66
|
+
- [Navigation](docs/navigation.md) — setup, `createNavigation`, `NavigationClient` API, hooks
|
|
67
|
+
- [Stack](docs/stack.md) — stack navigation, pushing/popping, refs, preloading, nesting
|
|
68
|
+
- [Tabs](docs/tabs.md) — tab navigation, tab bar, refs, preloading, nesting with stacks
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
createNavigation,
|
|
5
|
+
createNavigationState,
|
|
6
|
+
loadNavigationState,
|
|
7
|
+
} from "./navigation-client";
|
|
8
|
+
|
|
9
|
+
describe("createNavigationState", () => {
|
|
10
|
+
it("returns empty maps by default", () => {
|
|
11
|
+
const state = createNavigationState();
|
|
12
|
+
expect(state.stacks).toBeInstanceOf(Map);
|
|
13
|
+
expect(state.tabs).toBeInstanceOf(Map);
|
|
14
|
+
expect(state.stacks.size).toBe(0);
|
|
15
|
+
expect(state.tabs.size).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("normalizes Record inputs into Maps", () => {
|
|
19
|
+
const state = createNavigationState({
|
|
20
|
+
stacks: {
|
|
21
|
+
"stack-a": [{ element: <span>a</span> }],
|
|
22
|
+
},
|
|
23
|
+
tabs: {
|
|
24
|
+
"my-tabs": { activeIndex: 2 },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(state.stacks).toBeInstanceOf(Map);
|
|
29
|
+
expect(state.stacks.get("stack-a")).toHaveLength(1);
|
|
30
|
+
expect(state.tabs).toBeInstanceOf(Map);
|
|
31
|
+
expect(state.tabs.get("my-tabs")).toEqual({ activeIndex: 2 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("accepts Map inputs directly", () => {
|
|
35
|
+
const stacks = new Map([
|
|
36
|
+
["stack-a", [{ element: <span>a</span> }]],
|
|
37
|
+
]);
|
|
38
|
+
const tabs = new Map([["my-tabs", { activeIndex: 1 }]]);
|
|
39
|
+
const state = createNavigationState({ stacks, tabs });
|
|
40
|
+
|
|
41
|
+
expect(state.stacks.get("stack-a")).toHaveLength(1);
|
|
42
|
+
expect(state.tabs.get("my-tabs")).toEqual({ activeIndex: 1 });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("createNavigation", () => {
|
|
47
|
+
it("returns a client with the expected shape", () => {
|
|
48
|
+
const nav = createNavigation();
|
|
49
|
+
expect(nav.store).toBeDefined();
|
|
50
|
+
expect(nav.renderTreeStore).toBeDefined();
|
|
51
|
+
expect(typeof nav.pushScreen).toBe("function");
|
|
52
|
+
expect(typeof nav.popScreen).toBe("function");
|
|
53
|
+
expect(typeof nav.setActiveTab).toBe("function");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("initializes with empty state by default", () => {
|
|
57
|
+
const nav = createNavigation();
|
|
58
|
+
const state = nav.store.getState();
|
|
59
|
+
expect(state.stacks.size).toBe(0);
|
|
60
|
+
expect(state.tabs.size).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("initializes with the provided state", () => {
|
|
64
|
+
const nav = createNavigation({
|
|
65
|
+
stacks: { "stack-a": [{ element: <span>a</span> }] },
|
|
66
|
+
tabs: { "my-tabs": { activeIndex: 1 } },
|
|
67
|
+
});
|
|
68
|
+
const state = nav.store.getState();
|
|
69
|
+
expect(state.stacks.get("stack-a")).toHaveLength(1);
|
|
70
|
+
expect(state.tabs.get("my-tabs")).toEqual({ activeIndex: 1 });
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("loadNavigationState", () => {
|
|
75
|
+
it("replaces the store state with normalized input", () => {
|
|
76
|
+
const nav = createNavigation({
|
|
77
|
+
stacks: { "stack-a": [{ element: <span>old</span> }] },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
loadNavigationState(nav.store, {
|
|
81
|
+
stacks: { "stack-b": [{ element: <span>new</span> }] },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const state = nav.store.getState();
|
|
85
|
+
expect(state.stacks.has("stack-a")).toBe(false);
|
|
86
|
+
expect(state.stacks.get("stack-b")).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("pushScreen", () => {
|
|
91
|
+
it("pushes a screen to the specified stack", () => {
|
|
92
|
+
const nav = createNavigation({
|
|
93
|
+
stacks: { "stack-a": [] },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
nav.pushScreen(<span>pushed</span>, { stackId: "stack-a" });
|
|
97
|
+
|
|
98
|
+
const screens = nav.store.getState().stacks.get("stack-a");
|
|
99
|
+
expect(screens).toHaveLength(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("creates the stack entry if it did not exist", () => {
|
|
103
|
+
const nav = createNavigation();
|
|
104
|
+
|
|
105
|
+
nav.pushScreen(<span>pushed</span>, { stackId: "stack-new" });
|
|
106
|
+
|
|
107
|
+
const screens = nav.store.getState().stacks.get("stack-new");
|
|
108
|
+
expect(screens).toHaveLength(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("does not push a duplicate screen with the same id", () => {
|
|
112
|
+
const nav = createNavigation({
|
|
113
|
+
stacks: {
|
|
114
|
+
"stack-a": [{ element: <span>a</span>, options: { id: "screen-1" } }],
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
nav.pushScreen(<span>dup</span>, { id: "screen-1", stackId: "stack-a" });
|
|
119
|
+
|
|
120
|
+
expect(nav.store.getState().stacks.get("stack-a")).toHaveLength(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("allows pushing after a screen with the same id was popped", () => {
|
|
124
|
+
const nav = createNavigation({
|
|
125
|
+
stacks: {
|
|
126
|
+
"stack-a": [{ element: <span>a</span>, options: { id: "screen-1" } }],
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
nav.popScreen({ stackId: "stack-a" });
|
|
131
|
+
expect(nav.store.getState().stacks.get("stack-a")).toHaveLength(0);
|
|
132
|
+
|
|
133
|
+
nav.pushScreen(<span>new</span>, { id: "screen-1", stackId: "stack-a" });
|
|
134
|
+
expect(nav.store.getState().stacks.get("stack-a")).toHaveLength(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("throws when no stackId is provided and no stack is mounted", () => {
|
|
138
|
+
const nav = createNavigation();
|
|
139
|
+
expect(() => nav.pushScreen(<span>x</span>)).toThrow(
|
|
140
|
+
"could not resolve stackId",
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("popScreen", () => {
|
|
146
|
+
it("removes the top screen from the specified stack", () => {
|
|
147
|
+
const nav = createNavigation({
|
|
148
|
+
stacks: {
|
|
149
|
+
"stack-a": [
|
|
150
|
+
{ element: <span>a</span> },
|
|
151
|
+
{ element: <span>b</span> },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
nav.popScreen({ stackId: "stack-a" });
|
|
157
|
+
|
|
158
|
+
expect(nav.store.getState().stacks.get("stack-a")).toHaveLength(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("is a no-op when the stack is empty", () => {
|
|
162
|
+
const nav = createNavigation({
|
|
163
|
+
stacks: { "stack-a": [] },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const before = nav.store.getState();
|
|
167
|
+
nav.popScreen({ stackId: "stack-a" });
|
|
168
|
+
const after = nav.store.getState();
|
|
169
|
+
|
|
170
|
+
expect(before).toBe(after);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("throws when no stackId is provided and no stack is mounted", () => {
|
|
174
|
+
const nav = createNavigation();
|
|
175
|
+
expect(() => nav.popScreen()).toThrow("could not resolve stackId");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("setActiveTab", () => {
|
|
180
|
+
it("sets the active index for the specified tabs", () => {
|
|
181
|
+
const nav = createNavigation({
|
|
182
|
+
tabs: { "my-tabs": { activeIndex: 0 } },
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
nav.setActiveTab(2, { tabsId: "my-tabs" });
|
|
186
|
+
|
|
187
|
+
expect(nav.store.getState().tabs.get("my-tabs")).toEqual({
|
|
188
|
+
activeIndex: 2,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("creates the tabs entry if it did not exist", () => {
|
|
193
|
+
const nav = createNavigation();
|
|
194
|
+
|
|
195
|
+
nav.setActiveTab(1, { tabsId: "new-tabs" });
|
|
196
|
+
|
|
197
|
+
expect(nav.store.getState().tabs.get("new-tabs")).toEqual({
|
|
198
|
+
activeIndex: 1,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("throws when no tabsId is provided and no tabs are mounted", () => {
|
|
203
|
+
const nav = createNavigation();
|
|
204
|
+
expect(() => nav.setActiveTab(0)).toThrow("could not resolve tabsId");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
createStore,
|
|
4
|
+
useStore,
|
|
5
|
+
createRenderTreeStore,
|
|
6
|
+
getRenderNodeActive,
|
|
7
|
+
getRenderNodeDepth,
|
|
8
|
+
} from "@rn-tools/core";
|
|
9
|
+
import type { Store, RenderTreeStore } from "@rn-tools/core";
|
|
10
|
+
|
|
11
|
+
export type PushScreenOptions = {
|
|
12
|
+
id?: string;
|
|
13
|
+
stackId?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type NavigationScreenEntry = {
|
|
17
|
+
element: React.ReactElement;
|
|
18
|
+
id?: string;
|
|
19
|
+
options?: PushScreenOptions;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type TabState = {
|
|
23
|
+
activeIndex: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type NavigationState = {
|
|
27
|
+
stacks: Map<string, NavigationScreenEntry[]>;
|
|
28
|
+
tabs: Map<string, TabState>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type NavigationStore = Store<NavigationState>;
|
|
32
|
+
|
|
33
|
+
export const NavigationContext = React.createContext<NavigationClient | null>(null);
|
|
34
|
+
|
|
35
|
+
export const NavigationStoreContext = React.createContext<NavigationStore | null>(
|
|
36
|
+
null,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export type NavigationStateInput = {
|
|
40
|
+
stacks?:
|
|
41
|
+
| Map<string, NavigationScreenEntry[]>
|
|
42
|
+
| Record<string, NavigationScreenEntry[]>;
|
|
43
|
+
tabs?:
|
|
44
|
+
| Map<string, TabState>
|
|
45
|
+
| Record<string, TabState>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function createNavigationState(
|
|
49
|
+
input: NavigationStateInput = {},
|
|
50
|
+
): NavigationState {
|
|
51
|
+
return normalizeNavigationState(input);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function loadNavigationState(
|
|
55
|
+
store: NavigationStore,
|
|
56
|
+
input: NavigationState | NavigationStateInput,
|
|
57
|
+
) {
|
|
58
|
+
store.setState(normalizeNavigationState(input));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type NavigationClient = {
|
|
62
|
+
store: NavigationStore;
|
|
63
|
+
renderTreeStore: RenderTreeStore;
|
|
64
|
+
pushScreen: (element: React.ReactElement, options?: PushScreenOptions) => void;
|
|
65
|
+
popScreen: (options?: { stackId?: string }) => void;
|
|
66
|
+
setActiveTab: (index: number, options?: { tabsId?: string }) => void;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function createNavigation(
|
|
70
|
+
initialState?: NavigationStateInput,
|
|
71
|
+
): NavigationClient {
|
|
72
|
+
const navStore = createStore(
|
|
73
|
+
normalizeNavigationState(initialState ?? { stacks: new Map() }),
|
|
74
|
+
);
|
|
75
|
+
const renderTreeStore = createRenderTreeStore();
|
|
76
|
+
|
|
77
|
+
function getDeepestActiveNodeId(type: string): string | null {
|
|
78
|
+
const tree = renderTreeStore.getState();
|
|
79
|
+
let deepestId: string | null = null;
|
|
80
|
+
let deepestDepth = -1;
|
|
81
|
+
|
|
82
|
+
for (const [id, node] of tree.nodes) {
|
|
83
|
+
if (node.type !== type) continue;
|
|
84
|
+
if (!getRenderNodeActive(tree, id)) continue;
|
|
85
|
+
|
|
86
|
+
const depth = getRenderNodeDepth(tree, id);
|
|
87
|
+
if (depth > deepestDepth) {
|
|
88
|
+
deepestDepth = depth;
|
|
89
|
+
deepestId = id;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return deepestId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function pushScreen(
|
|
97
|
+
element: React.ReactElement,
|
|
98
|
+
options?: PushScreenOptions,
|
|
99
|
+
) {
|
|
100
|
+
const stackId = options?.stackId ?? getDeepestActiveNodeId("stack");
|
|
101
|
+
if (!stackId) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"pushScreen: could not resolve stackId. Pass { stackId } explicitly or ensure a Stack is mounted and active.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
navStore.setState((prev) => {
|
|
108
|
+
const stacks = new Map(prev.stacks);
|
|
109
|
+
const existing = stacks.get(stackId) ?? [];
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
options?.id &&
|
|
113
|
+
existing.some(
|
|
114
|
+
(s) => s.id === options.id || s.options?.id === options.id,
|
|
115
|
+
)
|
|
116
|
+
) {
|
|
117
|
+
return prev;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
stacks.set(stackId, [...existing, { element, id: options?.id, options }]);
|
|
121
|
+
return { ...prev, stacks };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function popScreen(options?: { stackId?: string }) {
|
|
126
|
+
const stackId = options?.stackId ?? getDeepestActiveNodeId("stack");
|
|
127
|
+
if (!stackId) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"popScreen: could not resolve stackId. Pass { stackId } explicitly or ensure a Stack is mounted and active.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
navStore.setState((prev) => {
|
|
134
|
+
const stacks = new Map(prev.stacks);
|
|
135
|
+
const screens = stacks.get(stackId);
|
|
136
|
+
if (!screens || screens.length === 0) return prev;
|
|
137
|
+
stacks.set(stackId, screens.slice(0, -1));
|
|
138
|
+
return { ...prev, stacks };
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function setActiveTab(index: number, options?: { tabsId?: string }) {
|
|
143
|
+
const tabsId = options?.tabsId ?? getDeepestActiveNodeId("tabs");
|
|
144
|
+
if (!tabsId) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
"setActiveTab: could not resolve tabsId. Pass { tabsId } explicitly or ensure a Tabs is mounted and active.",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
navStore.setState((prev) => {
|
|
151
|
+
const tabs = new Map(prev.tabs);
|
|
152
|
+
tabs.set(tabsId, { activeIndex: index });
|
|
153
|
+
return { ...prev, tabs };
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { store: navStore, renderTreeStore, pushScreen, popScreen, setActiveTab };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function useNavigation(): NavigationClient {
|
|
161
|
+
const navigation = React.useContext(NavigationContext);
|
|
162
|
+
if (!navigation) {
|
|
163
|
+
throw new Error("NavigationProvider is missing from the component tree.");
|
|
164
|
+
}
|
|
165
|
+
return navigation;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function useNavigationStore(): NavigationStore {
|
|
169
|
+
const store = React.useContext(NavigationStoreContext);
|
|
170
|
+
if (!store) {
|
|
171
|
+
throw new Error("NavigationProvider is missing from the component tree.");
|
|
172
|
+
}
|
|
173
|
+
return store;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function useTabActiveIndex(tabsId: string | null): number {
|
|
177
|
+
const store = useNavigationStore();
|
|
178
|
+
return useStore(store, (state) =>
|
|
179
|
+
tabsId ? state.tabs.get(tabsId)?.activeIndex ?? 0 : 0,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function useStackScreens(
|
|
184
|
+
stackKey: string | null,
|
|
185
|
+
): NavigationScreenEntry[] {
|
|
186
|
+
const store = useNavigationStore();
|
|
187
|
+
return useStore(store, (state) =>
|
|
188
|
+
stackKey ? state.stacks.get(stackKey) ?? [] : [],
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeNavigationState(
|
|
193
|
+
input: NavigationState | NavigationStateInput,
|
|
194
|
+
): NavigationState {
|
|
195
|
+
const stacksInput =
|
|
196
|
+
(input as NavigationState).stacks ??
|
|
197
|
+
(input as NavigationStateInput).stacks ??
|
|
198
|
+
new Map();
|
|
199
|
+
|
|
200
|
+
const stacks =
|
|
201
|
+
stacksInput instanceof Map
|
|
202
|
+
? new Map(stacksInput)
|
|
203
|
+
: new Map(Object.entries(stacksInput));
|
|
204
|
+
|
|
205
|
+
const tabsInput =
|
|
206
|
+
(input as NavigationState).tabs ??
|
|
207
|
+
(input as NavigationStateInput).tabs ??
|
|
208
|
+
new Map();
|
|
209
|
+
|
|
210
|
+
const tabs =
|
|
211
|
+
tabsInput instanceof Map
|
|
212
|
+
? new Map(tabsInput)
|
|
213
|
+
: new Map(Object.entries(tabsInput));
|
|
214
|
+
|
|
215
|
+
return { stacks, tabs };
|
|
216
|
+
}
|