@sigx/lynx-navigation 0.1.3 → 0.2.0
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/README.md +64 -2
- package/dist/components/Stack.d.ts +56 -11
- package/dist/components/Stack.d.ts.map +1 -1
- package/dist/components/Stack.js +160 -14
- package/dist/components/Stack.js.map +1 -1
- package/dist/components/Tabs.d.ts +30 -21
- package/dist/components/Tabs.d.ts.map +1 -1
- package/dist/components/Tabs.js +35 -21
- package/dist/components/Tabs.js.map +1 -1
- package/dist/hooks/use-focus.d.ts.map +1 -1
- package/dist/hooks/use-focus.js +7 -1
- package/dist/hooks/use-focus.js.map +1 -1
- package/dist/hooks/use-hardware-back.d.ts +9 -2
- package/dist/hooks/use-hardware-back.d.ts.map +1 -1
- package/dist/hooks/use-hardware-back.js +42 -8
- package/dist/hooks/use-hardware-back.js.map +1 -1
- package/dist/hooks/use-nav.d.ts +36 -1
- package/dist/hooks/use-nav.d.ts.map +1 -1
- package/dist/hooks/use-nav.js.map +1 -1
- package/dist/navigator/core.d.ts +26 -0
- package/dist/navigator/core.d.ts.map +1 -1
- package/dist/navigator/core.js +44 -5
- package/dist/navigator/core.js.map +1 -1
- package/package.json +6 -6
- package/src/components/Stack.tsx +212 -15
- package/src/components/Tabs.tsx +39 -21
- package/src/hooks/use-focus.ts +7 -1
- package/src/hooks/use-hardware-back.ts +43 -9
- package/src/hooks/use-nav.ts +38 -1
- package/src/navigator/core.ts +73 -4
package/README.md
CHANGED
|
@@ -98,7 +98,66 @@ and every other API gets typed.
|
|
|
98
98
|
### `<Stack>`
|
|
99
99
|
|
|
100
100
|
Renders the topmost stack entry plus the entry beneath it during
|
|
101
|
-
transitions.
|
|
101
|
+
transitions. By default it binds to the enclosing navigator (the root one
|
|
102
|
+
under `<NavigationRoot>`).
|
|
103
|
+
|
|
104
|
+
Pass `initialRoute` to make `<Stack>` create its **own** nested navigator —
|
|
105
|
+
this is how per-tab stacks work. See [Per-tab nested stacks](#per-tab-nested-stacks).
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
<Stack initialRoute="tripsHome" />
|
|
109
|
+
<Stack initialRoute="profile" initialParams={{ id: 'me' }} />
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Per-tab nested stacks
|
|
113
|
+
|
|
114
|
+
Drop a `<Stack initialRoute=…>` inside each `<Tabs.Screen>` to give every
|
|
115
|
+
tab its own back-stack. Card-presentation pushes stay inside the tab;
|
|
116
|
+
modal / fullScreen / transparent-modal pushes **escalate** to the root
|
|
117
|
+
navigator so they overlay the entire tabs UI (TabBar included).
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
<NavigationRoot routes={routes} initialRoute="root">
|
|
121
|
+
<Header />
|
|
122
|
+
<Stack /> {/* root navigator — renders the `root` entry below */}
|
|
123
|
+
</NavigationRoot>
|
|
124
|
+
|
|
125
|
+
// `root` route renders this:
|
|
126
|
+
<Tabs initialTab="trips">
|
|
127
|
+
<Tabs.Screen name="trips" label="Trips">
|
|
128
|
+
<Stack initialRoute="tripsHome" />
|
|
129
|
+
</Tabs.Screen>
|
|
130
|
+
<Tabs.Screen name="map" label="Map">
|
|
131
|
+
<Stack initialRoute="mapHome" />
|
|
132
|
+
</Tabs.Screen>
|
|
133
|
+
<TabBar />
|
|
134
|
+
</Tabs>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Inside a tab body, `useNav()` resolves to the **innermost** navigator:
|
|
138
|
+
|
|
139
|
+
- `nav.push('tripDetail', { tripId })` — `tripDetail` is a card route →
|
|
140
|
+
pushed onto the trips tab's stack, TabBar stays visible.
|
|
141
|
+
- `nav.push('newTrip')` — `newTrip` is `presentation: 'modal'` → walks up
|
|
142
|
+
`nav.parent` to root and pushes there, overlays the whole tabs UI.
|
|
143
|
+
- `nav.replace(...)` is **strictly local** and never escalates (asymmetric
|
|
144
|
+
with `push` by design — keeps the root stack stable).
|
|
145
|
+
- Hardware/edge back pops the focused inner nav first; only falls through
|
|
146
|
+
to root once the inner stack is empty.
|
|
147
|
+
- `useIsFocused()` is `true` only when the screen is the top of its own
|
|
148
|
+
nav **and** every ancestor is focused (parent's current entry matches +
|
|
149
|
+
enclosing tab is active).
|
|
150
|
+
|
|
151
|
+
**Limitations (current slice)**:
|
|
152
|
+
|
|
153
|
+
- Pushing a modal re-mounts the underlying root entry (the Tabs UI),
|
|
154
|
+
resetting inner-tab stack state. To preserve inner state across modals,
|
|
155
|
+
hoist app state out of screens.
|
|
156
|
+
- One global route registry — there's no per-tab whitelist yet. Deep-link
|
|
157
|
+
routing always pushes against the innermost nav of the caller site
|
|
158
|
+
(modal routes still escalate). A future slice may add `<Stack routes={…}>`.
|
|
159
|
+
- `useNavSerializer` snapshots one nav only — nested-tab stack state isn't
|
|
160
|
+
persisted across reload yet.
|
|
102
161
|
|
|
103
162
|
### `<Screen>`
|
|
104
163
|
|
|
@@ -143,7 +202,10 @@ screen's `useScreenOptions(...)` registration.
|
|
|
143
202
|
|
|
144
203
|
### `<Tabs>` + `<Tabs.Screen>` + `<TabBar>`
|
|
145
204
|
|
|
146
|
-
Persistent tab navigator. Each tab
|
|
205
|
+
Persistent tab navigator. Each tab body stays mounted (hidden via
|
|
206
|
+
`display: none`) so switching tabs preserves state. Drop a `<Stack
|
|
207
|
+
initialRoute=…>` inside a `<Tabs.Screen>` to give that tab its own
|
|
208
|
+
back-stack — see [Per-tab nested stacks](#per-tab-nested-stacks).
|
|
147
209
|
|
|
148
210
|
```tsx
|
|
149
211
|
<Tabs initialTab="home">
|
|
@@ -1,21 +1,66 @@
|
|
|
1
|
-
import { type ComponentFactory } from '@sigx/lynx';
|
|
1
|
+
import { type ComponentFactory, type Define } from '@sigx/lynx';
|
|
2
|
+
type StackProps =
|
|
3
|
+
/**
|
|
4
|
+
* Mint a nested navigator with this route at its base. When set, the
|
|
5
|
+
* `<Stack>` becomes the owner of a new `NavigatorState` and provides
|
|
6
|
+
* `useNav` / `useNavInternals` / `useNavRoutes` to its subtree, so
|
|
7
|
+
* `nav.push('card-route', …)` from inside the stack stays *inside* it
|
|
8
|
+
* (e.g. for per-tab stacks). Routes presented as `modal` / `fullScreen` /
|
|
9
|
+
* `transparent-modal` automatically escalate to the parent navigator
|
|
10
|
+
* via `nav.parent`, walking up until they reach the root — so modals
|
|
11
|
+
* still overlay the whole app.
|
|
12
|
+
*
|
|
13
|
+
* Omit to render the *enclosing* navigator's stack (the default — this
|
|
14
|
+
* is how `<NavigationRoot> → <Stack />` works).
|
|
15
|
+
*/
|
|
16
|
+
Define.Prop<'initialRoute', string>
|
|
17
|
+
/** Initial params for the nested-stack base entry. */
|
|
18
|
+
& Define.Prop<'initialParams', Record<string, unknown>>
|
|
19
|
+
/** Initial search for the nested-stack base entry. */
|
|
20
|
+
& Define.Prop<'initialSearch', Record<string, unknown>>;
|
|
2
21
|
/**
|
|
3
22
|
* Stack navigator — renders the topmost stack entry's component at rest, or
|
|
4
23
|
* the top + underneath entries during a transition.
|
|
5
24
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
25
|
+
* Two modes:
|
|
26
|
+
*
|
|
27
|
+
* **Bound** (no `initialRoute`): renders the enclosing navigator's stack.
|
|
28
|
+
* This is the shape used directly under `<NavigationRoot>` and is what
|
|
29
|
+
* single-stack apps want.
|
|
30
|
+
*
|
|
31
|
+
* **Nested-owner** (`initialRoute="…"`): mints a fresh `NavigatorState` with
|
|
32
|
+
* its own progress `SharedValue` and edge-back gesture, and provides
|
|
33
|
+
* `useNav` / `useNavInternals` / `useNavRoutes` to its subtree. `useNav()`
|
|
34
|
+
* inside this stack returns the nested nav; `nav.parent` points to the
|
|
35
|
+
* enclosing one. Per-tab stacks are the canonical use case:
|
|
36
|
+
*
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <Tabs initialTab="trips">
|
|
39
|
+
* <Tabs.Screen name="trips"><Stack initialRoute="tripsHome" /></Tabs.Screen>
|
|
40
|
+
* <Tabs.Screen name="map"><Stack initialRoute="mapHome" /></Tabs.Screen>
|
|
41
|
+
* </Tabs>
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Modal/fullScreen pushes escalate up the parent chain automatically — so
|
|
45
|
+
* `nav.push('newTrip')` from inside Trips (where `newTrip` is `modal`)
|
|
46
|
+
* walks to root and overlays the whole UI. `replace` stays strictly local
|
|
47
|
+
* (asymmetric with `push`) so a modal `replace` never wipes the root stack.
|
|
9
48
|
*
|
|
10
|
-
* **
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
49
|
+
* **Render strategy** (same in both modes):
|
|
50
|
+
* - **Idle**: just the top entry, full-bleed, no transform. The screen
|
|
51
|
+
* component mounts directly so it can use its own layout (no extra
|
|
52
|
+
* absolute positioning that would break percentage heights).
|
|
53
|
+
* - **Transitioning**: two `<ScreenContainer>` instances stacked
|
|
54
|
+
* absolutely, each with an MT-driven `translateX` that reads from the
|
|
55
|
+
* navigator's progress `SharedValue`. The host's BG thread doesn't tick
|
|
56
|
+
* per frame — `useAnimatedStyle` runs the interpolation entirely on MT.
|
|
14
57
|
*
|
|
15
58
|
* `key={top.key}` keeps the idle render's component instance stable across
|
|
16
59
|
* unrelated re-renders. During transitions, composite keys
|
|
17
|
-
* (`${entry.key}-${role}-${kind}`) ensure a fresh mount per role/kind pair
|
|
18
|
-
* the `useAnimatedStyle` binding is set with the right input/output
|
|
60
|
+
* (`${entry.key}-${role}-${kind}`) ensure a fresh mount per role/kind pair
|
|
61
|
+
* so the `useAnimatedStyle` binding is set with the right input/output
|
|
62
|
+
* ranges.
|
|
19
63
|
*/
|
|
20
|
-
export declare const Stack: ComponentFactory<
|
|
64
|
+
export declare const Stack: ComponentFactory<StackProps, void, {}>;
|
|
65
|
+
export {};
|
|
21
66
|
//# sourceMappingURL=Stack.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Stack.d.ts","sourceRoot":"","sources":["../../src/components/Stack.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"Stack.d.ts","sourceRoot":"","sources":["../../src/components/Stack.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOH,KAAK,gBAAgB,EACrB,KAAK,MAAM,EAEd,MAAM,YAAY,CAAC;AAgBpB,KAAK,UAAU;AACX;;;;;;;;;;;;GAYG;AACD,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC;AACrC,sDAAsD;GACpD,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACvD,sDAAsD;GACpD,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAI5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,eAAO,MAAM,KAAK,wCA+NhB,CAAC"}
|
package/dist/components/Stack.js
CHANGED
|
@@ -1,33 +1,179 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@sigx/lynx/jsx-runtime";
|
|
2
|
-
import { component } from '@sigx/lynx';
|
|
2
|
+
import { component, defineProvide, effect, onUnmounted, untrack, useSharedValue, } from '@sigx/lynx';
|
|
3
3
|
import { Suspense, isLazyComponent } from '@sigx/lynx';
|
|
4
|
+
import { createNavigatorState } from '../navigator/core.js';
|
|
4
5
|
import { useNav } from '../hooks/use-nav.js';
|
|
5
|
-
import { useNavInternals, useNavRoutes } from '../hooks/use-nav-internal.js';
|
|
6
|
+
import { useCurrentEntry, useNavInternals, useNavRoutes, } from '../hooks/use-nav-internal.js';
|
|
6
7
|
import { ScreenContainer } from './ScreenContainer.js';
|
|
7
8
|
import { EdgeBackHandle } from './EdgeBackHandle.js';
|
|
8
9
|
import { EntryScope } from './EntryScope.js';
|
|
10
|
+
import { useTabScreenName, useTabs } from './Tabs.js';
|
|
11
|
+
let _nestedKeyCounter = 0;
|
|
9
12
|
/**
|
|
10
13
|
* Stack navigator — renders the topmost stack entry's component at rest, or
|
|
11
14
|
* the top + underneath entries during a transition.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
* component mounts directly so it can use its own layout (no extra absolute
|
|
15
|
-
* positioning that would break percentage heights).
|
|
16
|
+
* Two modes:
|
|
16
17
|
*
|
|
17
|
-
* **
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* **Bound** (no `initialRoute`): renders the enclosing navigator's stack.
|
|
19
|
+
* This is the shape used directly under `<NavigationRoot>` and is what
|
|
20
|
+
* single-stack apps want.
|
|
21
|
+
*
|
|
22
|
+
* **Nested-owner** (`initialRoute="…"`): mints a fresh `NavigatorState` with
|
|
23
|
+
* its own progress `SharedValue` and edge-back gesture, and provides
|
|
24
|
+
* `useNav` / `useNavInternals` / `useNavRoutes` to its subtree. `useNav()`
|
|
25
|
+
* inside this stack returns the nested nav; `nav.parent` points to the
|
|
26
|
+
* enclosing one. Per-tab stacks are the canonical use case:
|
|
27
|
+
*
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <Tabs initialTab="trips">
|
|
30
|
+
* <Tabs.Screen name="trips"><Stack initialRoute="tripsHome" /></Tabs.Screen>
|
|
31
|
+
* <Tabs.Screen name="map"><Stack initialRoute="mapHome" /></Tabs.Screen>
|
|
32
|
+
* </Tabs>
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Modal/fullScreen pushes escalate up the parent chain automatically — so
|
|
36
|
+
* `nav.push('newTrip')` from inside Trips (where `newTrip` is `modal`)
|
|
37
|
+
* walks to root and overlays the whole UI. `replace` stays strictly local
|
|
38
|
+
* (asymmetric with `push`) so a modal `replace` never wipes the root stack.
|
|
39
|
+
*
|
|
40
|
+
* **Render strategy** (same in both modes):
|
|
41
|
+
* - **Idle**: just the top entry, full-bleed, no transform. The screen
|
|
42
|
+
* component mounts directly so it can use its own layout (no extra
|
|
43
|
+
* absolute positioning that would break percentage heights).
|
|
44
|
+
* - **Transitioning**: two `<ScreenContainer>` instances stacked
|
|
45
|
+
* absolutely, each with an MT-driven `translateX` that reads from the
|
|
46
|
+
* navigator's progress `SharedValue`. The host's BG thread doesn't tick
|
|
47
|
+
* per frame — `useAnimatedStyle` runs the interpolation entirely on MT.
|
|
21
48
|
*
|
|
22
49
|
* `key={top.key}` keeps the idle render's component instance stable across
|
|
23
50
|
* unrelated re-renders. During transitions, composite keys
|
|
24
|
-
* (`${entry.key}-${role}-${kind}`) ensure a fresh mount per role/kind pair
|
|
25
|
-
* the `useAnimatedStyle` binding is set with the right input/output
|
|
51
|
+
* (`${entry.key}-${role}-${kind}`) ensure a fresh mount per role/kind pair
|
|
52
|
+
* so the `useAnimatedStyle` binding is set with the right input/output
|
|
53
|
+
* ranges.
|
|
26
54
|
*/
|
|
27
|
-
export const Stack = component(() => {
|
|
28
|
-
|
|
55
|
+
export const Stack = component(({ props }) => {
|
|
56
|
+
// Capture enclosing scope's nav + routes + internals BEFORE any of the
|
|
57
|
+
// defineProvide calls below override them for descendants. These are
|
|
58
|
+
// always the "outer" values regardless of whether this Stack is bound
|
|
59
|
+
// or nested-owner.
|
|
60
|
+
const parentNav = useNav();
|
|
29
61
|
const routes = useNavRoutes();
|
|
30
|
-
const
|
|
62
|
+
const parentInternals = useNavInternals();
|
|
63
|
+
// Decide mode at setup. `props.initialRoute` is captured once — the
|
|
64
|
+
// alternative (reactive switch between bound and nested-owner) would
|
|
65
|
+
// need to dispose and recreate the inner nav, which would lose all
|
|
66
|
+
// pushed state. Reasonable to pin it.
|
|
67
|
+
const initialName = props.initialRoute;
|
|
68
|
+
const isNested = typeof initialName === 'string' && initialName.length > 0;
|
|
69
|
+
let nav;
|
|
70
|
+
let internals;
|
|
71
|
+
if (isNested) {
|
|
72
|
+
if (!routes[initialName]) {
|
|
73
|
+
throw new Error(`[lynx-navigation] <Stack initialRoute='${initialName}'>: ` +
|
|
74
|
+
`route is not registered. Known routes: ` +
|
|
75
|
+
`${Object.keys(routes).join(', ') || '(none)'}`);
|
|
76
|
+
}
|
|
77
|
+
// Host entry — the parent's current top *when this Stack mounts*.
|
|
78
|
+
// Used by the focus chain so the nested nav is only "locally
|
|
79
|
+
// focused" while its host entry is still the top of the parent.
|
|
80
|
+
// Wrapped in try/catch because `<Stack initialRoute>` *may* be
|
|
81
|
+
// placed outside an EntryScope (e.g. directly under
|
|
82
|
+
// `<NavigationRoot>`); in that case there's no host-entry gate to
|
|
83
|
+
// apply and we just rely on `parent.isLocallyFocused`.
|
|
84
|
+
let hostEntryKey = null;
|
|
85
|
+
try {
|
|
86
|
+
hostEntryKey = useCurrentEntry().key;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
hostEntryKey = null;
|
|
90
|
+
}
|
|
91
|
+
// Enclosing tab name (if any). Lets the focus chain gate on tab
|
|
92
|
+
// active state — Trips' inner stack reports `isLocallyFocused: false`
|
|
93
|
+
// while the user is on the Map tab, even though it's the top of
|
|
94
|
+
// its own stack.
|
|
95
|
+
let tabName = null;
|
|
96
|
+
let tabsHandle = null;
|
|
97
|
+
try {
|
|
98
|
+
tabName = useTabScreenName();
|
|
99
|
+
tabsHandle = useTabs();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
tabName = null;
|
|
103
|
+
tabsHandle = null;
|
|
104
|
+
}
|
|
105
|
+
// Inherit animation enablement from the parent — if the root was
|
|
106
|
+
// created with `animated={false}` (tests), nested stacks should
|
|
107
|
+
// also commit instantly so test assertions don't have to wait on
|
|
108
|
+
// a SharedValue that won't tick.
|
|
109
|
+
const animationsEnabled = parentInternals.progress !== null;
|
|
110
|
+
const progressSv = useSharedValue(0);
|
|
111
|
+
const presentation = (routes[initialName].presentation ?? 'card');
|
|
112
|
+
// Counter-derived suffix keeps base-entry keys unique across
|
|
113
|
+
// concurrent nested stacks in a tab app. Plain `Math.random` would
|
|
114
|
+
// do but a counter is deterministic for test snapshots.
|
|
115
|
+
_nestedKeyCounter += 1;
|
|
116
|
+
const initial = {
|
|
117
|
+
key: `nested-${initialName}-${_nestedKeyCounter}`,
|
|
118
|
+
route: initialName,
|
|
119
|
+
params: props.initialParams ?? {},
|
|
120
|
+
search: props.initialSearch ?? {},
|
|
121
|
+
state: undefined,
|
|
122
|
+
presentation,
|
|
123
|
+
};
|
|
124
|
+
const navState = createNavigatorState({
|
|
125
|
+
routes,
|
|
126
|
+
initial,
|
|
127
|
+
progress: animationsEnabled ? progressSv : undefined,
|
|
128
|
+
parent: parentNav,
|
|
129
|
+
// Start un-focused; the effect below flips this once we observe
|
|
130
|
+
// the parent's current entry / tab-active state.
|
|
131
|
+
initialLocallyFocused: false,
|
|
132
|
+
});
|
|
133
|
+
nav = navState.nav;
|
|
134
|
+
internals = {
|
|
135
|
+
progress: animationsEnabled ? progressSv : null,
|
|
136
|
+
beginBackGesture: navState._gesture.beginBackGesture,
|
|
137
|
+
commitBackGesture: navState._gesture.commitBackGesture,
|
|
138
|
+
cancelBackGesture: navState._gesture.cancelBackGesture,
|
|
139
|
+
edgeSwipeEnabled:
|
|
140
|
+
// Gate on animationsEnabled too — if there's no progress
|
|
141
|
+
// SharedValue (e.g. parent is `animated={false}`), the edge
|
|
142
|
+
// swipe gesture would call `beginBackGesture()` with a null
|
|
143
|
+
// progress and leave the stack in an inconsistent state.
|
|
144
|
+
animationsEnabled && parentInternals.edgeSwipeEnabled,
|
|
145
|
+
screens: navState._screens,
|
|
146
|
+
};
|
|
147
|
+
// Reactive focus chain: this nav is locally focused iff
|
|
148
|
+
// 1. (no host entry captured) OR parent.current.key === hostEntryKey
|
|
149
|
+
// 2. parent.isLocallyFocused
|
|
150
|
+
// 3. (no enclosing tab) OR tabs.active === tabName
|
|
151
|
+
// Effect re-runs on any of those changing — parent's stack
|
|
152
|
+
// mutating, parent's own focus flipping, or the tab switching.
|
|
153
|
+
const focusRunner = effect(() => {
|
|
154
|
+
const hostMatch = hostEntryKey === null || parentNav.current.key === hostEntryKey;
|
|
155
|
+
const parentFocused = parentNav.isLocallyFocused;
|
|
156
|
+
const tabActive = tabName === null || tabsHandle === null
|
|
157
|
+
? true
|
|
158
|
+
: tabsHandle.active === tabName;
|
|
159
|
+
const focused = hostMatch && parentFocused && tabActive;
|
|
160
|
+
// Write outside the read-tracking window — `_setLocallyFocused`
|
|
161
|
+
// bumps a signal that no consumer in *this* setup reads, but
|
|
162
|
+
// it's good hygiene anyway.
|
|
163
|
+
untrack(() => navState._setLocallyFocused(focused));
|
|
164
|
+
});
|
|
165
|
+
onUnmounted(() => {
|
|
166
|
+
focusRunner.stop();
|
|
167
|
+
parentNav._children.delete(nav);
|
|
168
|
+
});
|
|
169
|
+
defineProvide(useNav, () => nav);
|
|
170
|
+
defineProvide(useNavRoutes, () => routes);
|
|
171
|
+
defineProvide(useNavInternals, () => internals);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
nav = parentNav;
|
|
175
|
+
internals = parentInternals;
|
|
176
|
+
}
|
|
31
177
|
return () => {
|
|
32
178
|
const transition = nav.transition;
|
|
33
179
|
const top = nav.current;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Stack.js","sourceRoot":"","sources":["../../src/components/Stack.tsx"],"names":[],"mappings":";AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"Stack.js","sourceRoot":"","sources":["../../src/components/Stack.tsx"],"names":[],"mappings":"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"}
|
|
@@ -4,31 +4,32 @@
|
|
|
4
4
|
* Usage:
|
|
5
5
|
*
|
|
6
6
|
* ```tsx
|
|
7
|
-
* <NavigationRoot routes={routes}>
|
|
8
|
-
* <
|
|
9
|
-
* <Tabs.Screen name="feed" icon={<FeedIcon />} label="Feed">
|
|
10
|
-
* <FeedView />
|
|
11
|
-
* </Tabs.Screen>
|
|
12
|
-
* <Tabs.Screen name="me" icon={<MeIcon />} label="Profile">
|
|
13
|
-
* <ProfileView />
|
|
14
|
-
* </Tabs.Screen>
|
|
15
|
-
* </Tabs>
|
|
7
|
+
* <NavigationRoot routes={routes} initialRoute="root">
|
|
8
|
+
* <Stack />
|
|
16
9
|
* </NavigationRoot>
|
|
17
|
-
* ```
|
|
18
10
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
11
|
+
* // The route "root" component renders:
|
|
12
|
+
* <Tabs initialTab="feed">
|
|
13
|
+
* <Tabs.Screen name="feed" icon={<FeedIcon />} label="Feed">
|
|
14
|
+
* <Stack initialRoute="feedHome" />
|
|
15
|
+
* </Tabs.Screen>
|
|
16
|
+
* <Tabs.Screen name="me" icon={<MeIcon />} label="Profile">
|
|
17
|
+
* <Stack initialRoute="profileHome" />
|
|
18
|
+
* </Tabs.Screen>
|
|
19
|
+
* <TabBar />
|
|
20
|
+
* </Tabs>
|
|
21
|
+
* ```
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* - Named navigators (`useNav('root')`)
|
|
23
|
+
* Tab bodies stay mounted across switches (the inactive ones render with
|
|
24
|
+
* `display: 'none'`), so each tab's nested `<Stack>` keeps its history when
|
|
25
|
+
* the user flips back to it. The active tab is reactive via `useTabs()`.
|
|
27
26
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* Per-tab stacks: each `<Tabs.Screen>` can host a `<Stack initialRoute="…">`
|
|
28
|
+
* which mints its own navigator. `useNav()` inside that subtree resolves to
|
|
29
|
+
* the tab's stack, so `nav.push('card-route', …)` stays inside the tab.
|
|
30
|
+
* Routes presented as `modal` / `fullScreen` / `transparent-modal` escalate
|
|
31
|
+
* up `nav.parent` to the root navigator automatically — they overlay the
|
|
32
|
+
* tabs UI (TabBar included) and dismiss back into the originating tab.
|
|
32
33
|
*/
|
|
33
34
|
import { type Define, type JSXElement } from '@sigx/lynx';
|
|
34
35
|
/** Metadata about a registered `<Tabs.Screen>`. */
|
|
@@ -59,6 +60,14 @@ export interface TabsNav {
|
|
|
59
60
|
* Access the enclosing Tabs navigator. Throws when called outside `<Tabs>`.
|
|
60
61
|
*/
|
|
61
62
|
export declare const useTabs: import("@sigx/runtime-core").InjectableFunction<TabsNav>;
|
|
63
|
+
/**
|
|
64
|
+
* @internal
|
|
65
|
+
* Provided by each `<Tabs.Screen>` so a nested `<Stack initialRoute>` can
|
|
66
|
+
* discover *which* tab it's hosted by, and gate its focus state on that
|
|
67
|
+
* tab being active. Throws when called outside a `<Tabs.Screen>` body so
|
|
68
|
+
* the gate degrades to "always active" via the caller's try/catch.
|
|
69
|
+
*/
|
|
70
|
+
export declare const useTabScreenName: import("@sigx/runtime-core").InjectableFunction<string>;
|
|
62
71
|
type TabsProps = Define.Prop<'initialTab', string> & Define.Slot<'default'>;
|
|
63
72
|
type TabsScreenProps = Define.Prop<'name', string, true> & Define.Prop<'icon', JSXElement> & Define.Prop<'label', string> & Define.Prop<'accessibilityLabel', string> & Define.Slot<'default'>;
|
|
64
73
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../src/components/Tabs.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../src/components/Tabs.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAQH,KAAK,MAAM,EACX,KAAK,UAAU,EAElB,MAAM,YAAY,CAAC;AAEpB,mDAAmD;AACnD,MAAM,WAAW,OAAO;IACpB,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED,kDAAkD;AAClD,MAAM,WAAW,OAAO;IACpB,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACzC;AAED;;GAEG;AACH,eAAO,MAAM,OAAO,0DAIlB,CAAC;AAsBH;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,yDAI3B,CAAC;AAEH,KAAK,SAAS,GACR,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,GACjC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAgE7B,KAAK,eAAe,GACd,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,GACjC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,GAC/B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAC5B,MAAM,CAAC,IAAI,CAAC,oBAAoB,EAAE,MAAM,CAAC,GACzC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAwC7B;;;;;GAKG;AACH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAEf,CAAC"}
|
package/dist/components/Tabs.js
CHANGED
|
@@ -5,31 +5,32 @@ import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
|
|
|
5
5
|
* Usage:
|
|
6
6
|
*
|
|
7
7
|
* ```tsx
|
|
8
|
-
* <NavigationRoot routes={routes}>
|
|
9
|
-
* <
|
|
10
|
-
* <Tabs.Screen name="feed" icon={<FeedIcon />} label="Feed">
|
|
11
|
-
* <FeedView />
|
|
12
|
-
* </Tabs.Screen>
|
|
13
|
-
* <Tabs.Screen name="me" icon={<MeIcon />} label="Profile">
|
|
14
|
-
* <ProfileView />
|
|
15
|
-
* </Tabs.Screen>
|
|
16
|
-
* </Tabs>
|
|
8
|
+
* <NavigationRoot routes={routes} initialRoute="root">
|
|
9
|
+
* <Stack />
|
|
17
10
|
* </NavigationRoot>
|
|
18
|
-
* ```
|
|
19
11
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
12
|
+
* // The route "root" component renders:
|
|
13
|
+
* <Tabs initialTab="feed">
|
|
14
|
+
* <Tabs.Screen name="feed" icon={<FeedIcon />} label="Feed">
|
|
15
|
+
* <Stack initialRoute="feedHome" />
|
|
16
|
+
* </Tabs.Screen>
|
|
17
|
+
* <Tabs.Screen name="me" icon={<MeIcon />} label="Profile">
|
|
18
|
+
* <Stack initialRoute="profileHome" />
|
|
19
|
+
* </Tabs.Screen>
|
|
20
|
+
* <TabBar />
|
|
21
|
+
* </Tabs>
|
|
22
|
+
* ```
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* - Named navigators (`useNav('root')`)
|
|
24
|
+
* Tab bodies stay mounted across switches (the inactive ones render with
|
|
25
|
+
* `display: 'none'`), so each tab's nested `<Stack>` keeps its history when
|
|
26
|
+
* the user flips back to it. The active tab is reactive via `useTabs()`.
|
|
28
27
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* Per-tab stacks: each `<Tabs.Screen>` can host a `<Stack initialRoute="…">`
|
|
29
|
+
* which mints its own navigator. `useNav()` inside that subtree resolves to
|
|
30
|
+
* the tab's stack, so `nav.push('card-route', …)` stays inside the tab.
|
|
31
|
+
* Routes presented as `modal` / `fullScreen` / `transparent-modal` escalate
|
|
32
|
+
* up `nav.parent` to the root navigator automatically — they overlay the
|
|
33
|
+
* tabs UI (TabBar included) and dismiss back into the originating tab.
|
|
33
34
|
*/
|
|
34
35
|
import { component, compound, defineInjectable, defineProvide, onUnmounted, signal, untrack, } from '@sigx/lynx';
|
|
35
36
|
/**
|
|
@@ -41,6 +42,16 @@ export const useTabs = defineInjectable(() => {
|
|
|
41
42
|
const useTabsRegistrar = defineInjectable(() => {
|
|
42
43
|
throw new Error('[lynx-navigation] <Tabs.Screen> rendered outside a <Tabs> component.');
|
|
43
44
|
});
|
|
45
|
+
/**
|
|
46
|
+
* @internal
|
|
47
|
+
* Provided by each `<Tabs.Screen>` so a nested `<Stack initialRoute>` can
|
|
48
|
+
* discover *which* tab it's hosted by, and gate its focus state on that
|
|
49
|
+
* tab being active. Throws when called outside a `<Tabs.Screen>` body so
|
|
50
|
+
* the gate degrades to "always active" via the caller's try/catch.
|
|
51
|
+
*/
|
|
52
|
+
export const useTabScreenName = defineInjectable(() => {
|
|
53
|
+
throw new Error('[lynx-navigation] useTabScreenName() called outside a <Tabs.Screen> body.');
|
|
54
|
+
});
|
|
44
55
|
const _Tabs = component(({ props, slots }) => {
|
|
45
56
|
// Tabs are stored as a deeply-reactive proxy signal so `tabs` consumers
|
|
46
57
|
// re-render when registration changes. `activeSignal` uses the wrapped
|
|
@@ -116,6 +127,9 @@ const TabsScreen = component(({ props, slots }) => {
|
|
|
116
127
|
accessibilityLabel: props.accessibilityLabel,
|
|
117
128
|
});
|
|
118
129
|
onUnmounted(() => registrar.unregister(name));
|
|
130
|
+
// Expose this screen's tab name so a nested `<Stack initialRoute>` body
|
|
131
|
+
// can gate its locally-focused state on `tabs.active === name`.
|
|
132
|
+
defineProvide(useTabScreenName, () => name);
|
|
119
133
|
return () => {
|
|
120
134
|
// `display: none` keeps the body mounted so per-tab state survives
|
|
121
135
|
// tab switches. Read activeSignal here so re-activating triggers a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.js","sourceRoot":"","sources":["../../src/components/Tabs.tsx"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"Tabs.js","sourceRoot":"","sources":["../../src/components/Tabs.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-focus.d.ts","sourceRoot":"","sources":["../../src/hooks/use-focus.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,QAAQ,EAChB,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,IAAI,QAAQ,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"use-focus.d.ts","sourceRoot":"","sources":["../../src/hooks/use-focus.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,QAAQ,EAChB,MAAM,YAAY,CAAC;AAIpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,IAAI,QAAQ,CAAC,OAAO,CAAC,CAahD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CA0BlE"}
|
package/dist/hooks/use-focus.js
CHANGED
|
@@ -27,7 +27,13 @@ export function useIsFocused() {
|
|
|
27
27
|
// through `defineProvide` may carry reactive dependencies; we only care
|
|
28
28
|
// about the immutable key of the entry this screen was mounted for.
|
|
29
29
|
const myKey = useCurrentEntry().key;
|
|
30
|
-
|
|
30
|
+
// AND in `nav.isLocallyFocused` so a screen in a nested stack (e.g. a
|
|
31
|
+
// per-tab `<Stack>`) reports unfocused when its enclosing tab is
|
|
32
|
+
// inactive, or when a modal on the root nav covers everything — even
|
|
33
|
+
// though it's still the top of its own (paused) stack. Root nav's
|
|
34
|
+
// `isLocallyFocused` is permanently true, so this reduces to the
|
|
35
|
+
// previous behavior for un-nested apps.
|
|
36
|
+
return computed(() => nav.current.key === myKey && nav.isLocallyFocused);
|
|
31
37
|
}
|
|
32
38
|
/**
|
|
33
39
|
* Run `cb` whenever this screen gains focus; run the returned cleanup when it
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-focus.js","sourceRoot":"","sources":["../../src/hooks/use-focus.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,QAAQ,EACR,MAAM,EACN,WAAW,EACX,OAAO,GAEV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,YAAY;IACxB,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,mEAAmE;IACnE,wEAAwE;IACxE,oEAAoE;IACpE,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC,GAAG,CAAC;IACpC,OAAO,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"use-focus.js","sourceRoot":"","sources":["../../src/hooks/use-focus.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,QAAQ,EACR,MAAM,EACN,WAAW,EACX,OAAO,GAEV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,YAAY;IACxB,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,mEAAmE;IACnE,wEAAwE;IACxE,oEAAoE;IACpE,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC,GAAG,CAAC;IACpC,sEAAsE;IACtE,iEAAiE;IACjE,qEAAqE;IACrE,kEAAkE;IAClE,iEAAiE;IACjE,wCAAwC;IACxC,OAAO,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,KAAK,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,cAAc,CAAC,EAA6B;IACxD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,IAAI,OAA4B,CAAC;IACjC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,EAAE;QACvB,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC;QAChC,oEAAoE;QACpE,mEAAmE;QACnE,qEAAqE;QACrE,wCAAwC;QACxC,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;YAChC,MAAM,EAAE,GAAG,OAAO,CAAC;YACnB,OAAO,GAAG,SAAS,CAAC;YACpB,EAAE,EAAE,CAAC;QACT,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACV,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAClC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,WAAW,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;YAChC,MAAM,EAAE,GAAG,OAAO,CAAC;YACnB,OAAO,GAAG,SAAS,CAAC;YACpB,EAAE,EAAE,CAAC;QACT,CAAC;QACD,MAAM,CAAC,IAAI,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -3,13 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Listens for `hardwareBackPress` events from `@sigx/lynx-linking`'s
|
|
5
5
|
* `BackHandler` (which the native side dispatches from
|
|
6
|
-
* `MainActivity.onBackPressed`). On press
|
|
6
|
+
* `MainActivity.onBackPressed`). On press the handler walks to the
|
|
7
|
+
* deepest currently-focused navigator (per-tab `<Stack>`s register with
|
|
8
|
+
* their parent), then walks back up the `parent` chain looking for the
|
|
9
|
+
* first nav that `canGoBack`:
|
|
7
10
|
*
|
|
8
|
-
* - If
|
|
11
|
+
* - If any nav in the chain can go back → `nav.pop()` on that nav.
|
|
9
12
|
* - Otherwise → `BackHandler.exitApp()` (Android: `moveTaskToBack(true)`,
|
|
10
13
|
* keeps the bundle warm; iOS: rejects, since iOS doesn't permit
|
|
11
14
|
* programmatic termination).
|
|
12
15
|
*
|
|
16
|
+
* The traversal means you only need to call this once at the root — a
|
|
17
|
+
* back press from inside a tab pops that tab's nested stack first, only
|
|
18
|
+
* exiting the app once every level is at its base entry.
|
|
19
|
+
*
|
|
13
20
|
* Call this once in any component under `<NavigationRoot>` (typically a
|
|
14
21
|
* thin wrapper sibling to `<Stack />`). iOS doesn't fire the event so the
|
|
15
22
|
* hook is a no-op there.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-hardware-back.d.ts","sourceRoot":"","sources":["../../src/hooks/use-hardware-back.ts"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"use-hardware-back.d.ts","sourceRoot":"","sources":["../../src/hooks/use-hardware-back.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,eAAe,IAAI,IAAI,CA2CtC"}
|