@symbiote-native/navigation 2.1.0 → 3.0.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 +89 -14
- package/build/angular/drawer/index.js +23 -9
- package/build/angular/index.d.ts +4 -4
- package/build/angular/index.js +1 -1
- package/build/angular/injectors/inject-navigation.js +1 -1
- package/build/angular/navigation-scope.directive.js +1 -1
- package/build/angular/search-bar-ref.directive.js +1 -1
- package/build/angular/stack/index.js +20 -6
- package/build/angular/tabs/index.js +21 -7
- package/build/core/drawer-options/index.js +19 -5
- package/build/core/drawer-router-state/index.js +8 -2
- package/build/core/guards.js +3 -1
- package/build/core/header-bar-buttons.js +14 -4
- package/build/core/index.d.ts +4 -4
- package/build/core/index.js +2 -2
- package/build/core/navigator-state/index.js +12 -4
- package/build/core/render-drawer.js +4 -1
- package/build/core/render-stack.js +13 -4
- package/build/core/screen-options.js +1 -1
- package/build/core/search-bar-commands.js +1 -1
- package/build/core/state-persistence/index.js +6 -2
- package/build/core/tab-router-state/index.js +8 -2
- package/build/react/drawer/index.js +36 -13
- package/build/react/index.d.ts +7 -7
- package/build/react/index.js +3 -3
- package/build/react/stack/index.js +34 -8
- package/build/react/tabs/index.js +14 -4
- package/build/solid/drawer/index.d.ts +22 -0
- package/build/solid/drawer/index.js +394 -0
- package/build/solid/drawer-screen-props.d.ts +13 -0
- package/build/solid/drawer-screen-props.js +2 -0
- package/build/solid/host.d.ts +2 -0
- package/build/solid/host.js +23 -0
- package/build/solid/index.d.ts +19 -0
- package/build/solid/index.js +24 -0
- package/build/solid/linking.d.ts +3 -0
- package/build/solid/linking.js +55 -0
- package/build/solid/navigation-context.d.ts +13 -0
- package/build/solid/navigation-context.js +37 -0
- package/build/solid/primitives/create-focus-effect.d.ts +1 -0
- package/build/solid/primitives/create-focus-effect.js +30 -0
- package/build/solid/primitives/create-is-focused.d.ts +2 -0
- package/build/solid/primitives/create-is-focused.js +31 -0
- package/build/solid/primitives/create-navigation-state.d.ts +3 -0
- package/build/solid/primitives/create-navigation-state.js +30 -0
- package/build/solid/primitives/index.d.ts +12 -0
- package/build/solid/primitives/index.js +8 -0
- package/build/solid/primitives/use-drawer-navigation.d.ts +5 -0
- package/build/solid/primitives/use-drawer-navigation.js +15 -0
- package/build/solid/primitives/use-navigation.d.ts +8 -0
- package/build/solid/primitives/use-navigation.js +23 -0
- package/build/solid/primitives/use-route.d.ts +3 -0
- package/build/solid/primitives/use-route.js +14 -0
- package/build/solid/primitives/use-stack-navigation.d.ts +5 -0
- package/build/solid/primitives/use-stack-navigation.js +19 -0
- package/build/solid/primitives/use-tab-navigation.d.ts +5 -0
- package/build/solid/primitives/use-tab-navigation.js +15 -0
- package/build/solid/screen-props.d.ts +19 -0
- package/build/solid/screen-props.js +9 -0
- package/build/solid/screen-registry.d.ts +31 -0
- package/build/solid/screen-registry.js +86 -0
- package/build/solid/screen.d.ts +6 -0
- package/build/solid/screen.js +69 -0
- package/build/solid/stack/index.d.ts +15 -0
- package/build/solid/stack/index.js +348 -0
- package/build/solid/tab-screen-props.d.ts +13 -0
- package/build/solid/tab-screen-props.js +3 -0
- package/build/solid/tabs/index.d.ts +14 -0
- package/build/solid/tabs/index.js +228 -0
- package/build/svelte/descriptor-subtree.js +3 -1
- package/build/svelte/drawer/index.svelte +163 -25
- package/build/svelte/drawer-screen.svelte +2 -1
- package/build/svelte/index.d.ts +3 -3
- package/build/svelte/index.js +1 -1
- package/build/svelte/navigation-scope.svelte +4 -1
- package/build/svelte/runes/use-navigation.svelte.js +5 -1
- package/build/svelte/stack/index.svelte +43 -6
- package/build/svelte/stack/stack-screen.svelte +66 -9
- package/build/svelte/tabs/index.svelte +42 -20
- package/build/vue/composables/use-navigation.js +5 -1
- package/build/vue/drawer/index.js +30 -8
- package/build/vue/index.d.ts +3 -3
- package/build/vue/index.js +1 -1
- package/build/vue/navigation-context.js +1 -1
- package/build/vue/stack/index.js +17 -6
- package/build/vue/tabs/index.js +18 -5
- package/build-ngc/angular/drawer/index.js +31 -11
- package/build-ngc/angular/drawer/index.js.map +1 -1
- package/build-ngc/angular/index.d.ts +4 -4
- package/build-ngc/angular/index.js.map +1 -1
- package/build-ngc/angular/injectors/inject-focus-effect.js.map +1 -1
- package/build-ngc/angular/injectors/inject-is-focused.js.map +1 -1
- package/build-ngc/angular/injectors/inject-navigation-state.js.map +1 -1
- package/build-ngc/angular/injectors/inject-navigation.js +1 -1
- package/build-ngc/angular/injectors/inject-navigation.js.map +1 -1
- package/build-ngc/angular/linking.js.map +1 -1
- package/build-ngc/angular/navigation-context.service.js.map +1 -1
- package/build-ngc/angular/navigation-scope.directive.js +1 -1
- package/build-ngc/angular/navigation-scope.directive.js.map +1 -1
- package/build-ngc/angular/screen.directive.js.map +1 -1
- package/build-ngc/angular/search-bar-ref.directive.js +1 -1
- package/build-ngc/angular/search-bar-ref.directive.js.map +1 -1
- package/build-ngc/angular/stack/index.js +26 -8
- package/build-ngc/angular/stack/index.js.map +1 -1
- package/build-ngc/angular/tab-screen.directive.js.map +1 -1
- package/build-ngc/angular/tabs/index.js +21 -7
- package/build-ngc/angular/tabs/index.js.map +1 -1
- package/build-ngc/core/constants.js.map +1 -1
- package/build-ngc/core/drawer-options/index.js +19 -5
- package/build-ngc/core/drawer-options/index.js.map +1 -1
- package/build-ngc/core/drawer-router-state/index.js +8 -2
- package/build-ngc/core/drawer-router-state/index.js.map +1 -1
- package/build-ngc/core/guards.js +3 -1
- package/build-ngc/core/guards.js.map +1 -1
- package/build-ngc/core/header-bar-buttons.js +14 -4
- package/build-ngc/core/header-bar-buttons.js.map +1 -1
- package/build-ngc/core/index.d.ts +4 -4
- package/build-ngc/core/index.js +2 -2
- package/build-ngc/core/index.js.map +1 -1
- package/build-ngc/core/linking-config/index.js.map +1 -1
- package/build-ngc/core/navigation-events/index.js.map +1 -1
- package/build-ngc/core/navigator-handles/index.js.map +1 -1
- package/build-ngc/core/navigator-state/index.js +12 -4
- package/build-ngc/core/navigator-state/index.js.map +1 -1
- package/build-ngc/core/render-drawer.js +4 -1
- package/build-ngc/core/render-drawer.js.map +1 -1
- package/build-ngc/core/render-stack.js +13 -4
- package/build-ngc/core/render-stack.js.map +1 -1
- package/build-ngc/core/render-tabs.js.map +1 -1
- package/build-ngc/core/screen-options.js +1 -1
- package/build-ngc/core/screen-options.js.map +1 -1
- package/build-ngc/core/search-bar-commands.js +1 -1
- package/build-ngc/core/search-bar-commands.js.map +1 -1
- package/build-ngc/core/stack-router-state/index.js.map +1 -1
- package/build-ngc/core/state-persistence/index.js +6 -2
- package/build-ngc/core/state-persistence/index.js.map +1 -1
- package/build-ngc/core/tab-router-state/index.js +8 -2
- package/build-ngc/core/tab-router-state/index.js.map +1 -1
- package/package.json +40 -15
- package/src/angular/drawer/index.ts +69 -25
- package/src/angular/index.ts +20 -4
- package/src/angular/injectors/inject-focus-effect.ts +8 -2
- package/src/angular/injectors/inject-is-focused.ts +6 -4
- package/src/angular/injectors/inject-navigation-state.ts +7 -4
- package/src/angular/injectors/inject-navigation.ts +12 -3
- package/src/angular/linking.ts +4 -1
- package/src/angular/navigation-context.service.ts +5 -2
- package/src/angular/navigation-scope.directive.ts +6 -2
- package/src/angular/screen.directive.ts +13 -3
- package/src/angular/search-bar-ref.directive.ts +10 -2
- package/src/angular/stack/index.ts +68 -22
- package/src/angular/tab-screen.directive.ts +3 -1
- package/src/angular/tabs/index.ts +36 -12
- package/src/core/constants.ts +6 -3
- package/src/core/drawer-options/index.ts +40 -13
- package/src/core/drawer-router-state/index.ts +11 -3
- package/src/core/guards.ts +3 -1
- package/src/core/header-bar-buttons.ts +41 -12
- package/src/core/index.ts +18 -4
- package/src/core/linking-config/index.ts +31 -10
- package/src/core/navigation-events/index.ts +8 -2
- package/src/core/navigator-handles/index.ts +8 -3
- package/src/core/navigator-props.ts +13 -4
- package/src/core/navigator-state/index.ts +24 -7
- package/src/core/render-drawer.ts +24 -9
- package/src/core/render-stack.ts +52 -13
- package/src/core/render-tabs.ts +24 -6
- package/src/core/screen-options.ts +15 -5
- package/src/core/search-bar-commands.ts +12 -3
- package/src/core/stack-router-state/index.ts +5 -2
- package/src/core/state-persistence/index.ts +13 -3
- package/src/core/tab-router-state/index.ts +10 -3
- package/src/react/drawer/index.ts +408 -344
- package/src/react/drawer-screen.ts +8 -2
- package/src/react/hooks/use-focus-effect.ts +8 -2
- package/src/react/hooks/use-is-focused.ts +6 -2
- package/src/react/hooks/use-navigation.ts +8 -2
- package/src/react/index.ts +37 -7
- package/src/react/linking.ts +4 -1
- package/src/react/navigation-context.ts +6 -2
- package/src/react/screen.ts +12 -3
- package/src/react/stack/index.ts +294 -231
- package/src/react/tab-screen.ts +3 -1
- package/src/react/tabs/index.ts +188 -158
- package/src/solid/drawer/index.ts +570 -0
- package/src/solid/drawer-screen-props.ts +24 -0
- package/src/solid/host.ts +29 -0
- package/src/solid/index.ts +87 -0
- package/src/solid/linking.ts +74 -0
- package/src/solid/navigation-context.ts +56 -0
- package/src/solid/primitives/create-focus-effect.ts +41 -0
- package/src/solid/primitives/create-is-focused.ts +41 -0
- package/src/solid/primitives/create-navigation-state.ts +44 -0
- package/src/solid/primitives/index.ts +12 -0
- package/src/solid/primitives/use-drawer-navigation.ts +25 -0
- package/src/solid/primitives/use-navigation.ts +44 -0
- package/src/solid/primitives/use-route.ts +18 -0
- package/src/solid/primitives/use-stack-navigation.ts +29 -0
- package/src/solid/primitives/use-tab-navigation.ts +25 -0
- package/src/solid/screen-props.ts +51 -0
- package/src/solid/screen-registry.ts +159 -0
- package/src/solid/screen.ts +80 -0
- package/src/solid/stack/index.ts +528 -0
- package/src/solid/tab-screen-props.ts +21 -0
- package/src/solid/tabs/index.ts +327 -0
- package/src/svelte/attachments.ts +3 -1
- package/src/svelte/descriptor-subtree.ts +3 -1
- package/src/svelte/drawer/index.svelte +163 -25
- package/src/svelte/drawer-screen-props.ts +8 -2
- package/src/svelte/drawer-screen.svelte +2 -1
- package/src/svelte/fabric-tree.test-helper.ts +37 -3
- package/src/svelte/index.ts +15 -3
- package/src/svelte/linking.svelte.ts +11 -5
- package/src/svelte/navigation-scope.svelte +4 -1
- package/src/svelte/runes/use-drawer-navigation.svelte.ts +3 -1
- package/src/svelte/runes/use-focus-effect.svelte.ts +8 -2
- package/src/svelte/runes/use-navigation-state.svelte.ts +3 -1
- package/src/svelte/runes/use-navigation.svelte.ts +13 -3
- package/src/svelte/runes/use-stack-navigation.svelte.ts +3 -1
- package/src/svelte/screen-props.ts +7 -2
- package/src/svelte/screen-registry.ts +9 -3
- package/src/svelte/stack/index.svelte +43 -6
- package/src/svelte/stack/stack-screen.svelte +66 -9
- package/src/svelte/svelte-compile.test-helper.ts +16 -12
- package/src/svelte/tab-screen-props.ts +3 -1
- package/src/svelte/tabs/index.svelte +42 -20
- package/src/vue/composables/use-navigation-state.ts +7 -4
- package/src/vue/composables/use-navigation.ts +13 -3
- package/src/vue/drawer/index.ts +122 -36
- package/src/vue/drawer-screen.ts +12 -5
- package/src/vue/index.ts +15 -3
- package/src/vue/linking.ts +7 -2
- package/src/vue/navigation-context.ts +17 -5
- package/src/vue/screen.ts +7 -2
- package/src/vue/stack/index.ts +82 -34
- package/src/vue/tab-screen.ts +3 -1
- package/src/vue/tabs/index.ts +51 -13
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
// Stack, the Solid lifecycle half. The route-stack transitions (navigator-state) and the
|
|
2
|
+
// options/props folds (screen-options, render-stack) live in @symbiote-native/navigation core,
|
|
3
|
+
// shared verbatim with the React/Vue/Svelte/Angular adapters; here Solid supplies the lifecycle -
|
|
4
|
+
// a signal for the pushed-route stack, a route-key counter, a callback `ref` for the push/pop/
|
|
5
|
+
// replace handle - plus the descriptor bridge for the header config leaf. Pushing/popping a route
|
|
6
|
+
// is an ordinary child insert/remove: RNSScreenStack diffs its RNSScreen children natively, so no
|
|
7
|
+
// imperative native command is needed here. Neither this nor the Screen marker imports
|
|
8
|
+
// react-native-screens' own React components (they are React internally and crash a non-React
|
|
9
|
+
// adapter); the native views are driven through the ViewConfig ../../register registers. See
|
|
10
|
+
// CLAUDE.md <third_party_rn_packages_are_react_only>.
|
|
11
|
+
//
|
|
12
|
+
// FOUR THINGS DIFFER FROM EVERY OTHER ADAPTER, and each is a place a naive port freezes:
|
|
13
|
+
//
|
|
14
|
+
// 1. THE REGISTRY IS EMPTY WHEN THIS BODY RUNS. Solid cannot inspect children (screen-registry.ts),
|
|
15
|
+
// and the markers only register once the collector Provider evaluates them - after the body has
|
|
16
|
+
// returned. So nothing is computed eagerly from the registry: the initial state is seeded lazily
|
|
17
|
+
// by `seed()` the first time a non-empty registry is observed.
|
|
18
|
+
//
|
|
19
|
+
// 2. ROUTES ARE KEYED BY `route.key`, NOT BY THE ROUTE OBJECT. `setParams` produces a NEW route
|
|
20
|
+
// object under the same key. A `<For each={routes()}>` keys on object identity and would tear a
|
|
21
|
+
// screen's whole subtree down and rebuild it on every setParams - losing native-owned state and
|
|
22
|
+
// every ref. Iterating the KEYS (a memo with an element-wise `equals`) makes a params change
|
|
23
|
+
// invisible to the list and visible only to the leaf accessors that read it.
|
|
24
|
+
//
|
|
25
|
+
// 3. THE SCREEN IS BUILT BEHIND AN EXPLICIT REBUILD BOUNDARY. Three things legitimately change a
|
|
26
|
+
// route's native SHAPE rather than its props: `screenViewName` (RN treats RNSModalScreen as a
|
|
27
|
+
// different view from RNSScreen), `inModal` (a modal nests an inner stack purely to host the
|
|
28
|
+
// header - isHeaderInModal in core/render-stack.ts), and whether the header config carries a
|
|
29
|
+
// search-bar child. Each is read in a `createMemo` over those discriminators ALONE with the
|
|
30
|
+
// build `untrack`ed - the shape .claude/rules/solid-descriptor-bridge.md §5 prescribes. Without
|
|
31
|
+
// the untrack the memo subscribes to every prop the build reads and re-creates the native screen
|
|
32
|
+
// on each header-title change. The screen COMPONENT is its own, narrower boundary inside that.
|
|
33
|
+
//
|
|
34
|
+
// 4. FOCUS/STATE EMISSION IS DEFERRED BY A MICROTASK. A screen's subtree - and the subscriptions
|
|
35
|
+
// createIsFocused/createNavigationState set up inside it - is built by the render effects of the
|
|
36
|
+
// very update that changed the state, so an inline emit would reach zero subscribers.
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
For,
|
|
40
|
+
createEffect,
|
|
41
|
+
createMemo,
|
|
42
|
+
createSignal,
|
|
43
|
+
onCleanup,
|
|
44
|
+
untrack,
|
|
45
|
+
} from 'solid-js';
|
|
46
|
+
import type { Accessor } from 'solid-js';
|
|
47
|
+
import { descriptorToSolid } from '@symbiote-native/solid';
|
|
48
|
+
import type { JSX } from '@symbiote-native/solid/jsx-runtime';
|
|
49
|
+
// `createComponent` comes from solid-js, not from the renderer: they are the SAME runtime call
|
|
50
|
+
// (untrack + invoke), but the renderer's is typed to return a host node, which a Solid component
|
|
51
|
+
// legitimately does not have to (a screen may render nothing). `insert`/`insertNode` are the
|
|
52
|
+
// renderer's own, since they mutate the engine tree.
|
|
53
|
+
import { createComponent } from 'solid-js';
|
|
54
|
+
import { insert, insertNode } from '@symbiote-native/solid/renderer';
|
|
55
|
+
import {
|
|
56
|
+
Platform,
|
|
57
|
+
debugNodeId,
|
|
58
|
+
dlog,
|
|
59
|
+
isSymbioteNode,
|
|
60
|
+
} from '@symbiote-native/engine';
|
|
61
|
+
import type { ISymbioteNode } from '@symbiote-native/engine';
|
|
62
|
+
import {
|
|
63
|
+
NAVIGATION_EVENT_BLUR,
|
|
64
|
+
NAVIGATION_EVENT_FOCUS,
|
|
65
|
+
NAVIGATION_EVENT_STATE,
|
|
66
|
+
RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME,
|
|
67
|
+
RNS_SCREEN_STACK_VIEW_NAME,
|
|
68
|
+
RNS_SCREEN_VIEW_NAME,
|
|
69
|
+
SCREEN_ON_APPEAR,
|
|
70
|
+
SCREEN_ON_DISAPPEAR,
|
|
71
|
+
SCREEN_ON_DISMISSED,
|
|
72
|
+
SCREEN_ON_HEADER_BACK_BUTTON_CLICKED,
|
|
73
|
+
SCREEN_ON_WILL_APPEAR,
|
|
74
|
+
SCREEN_ON_WILL_DISAPPEAR,
|
|
75
|
+
STACK_ON_FINISH_TRANSITIONING,
|
|
76
|
+
buildSearchBarHandle,
|
|
77
|
+
buildSearchBarPassthrough,
|
|
78
|
+
createInitialNavigatorState,
|
|
79
|
+
createNavigationEmitter,
|
|
80
|
+
navigatorReducer,
|
|
81
|
+
reconcileStackRoutes,
|
|
82
|
+
resolveScreenRenderPlan,
|
|
83
|
+
resolveStackProps,
|
|
84
|
+
} from '../../core';
|
|
85
|
+
import type {
|
|
86
|
+
INavigationEmitter,
|
|
87
|
+
INavigatorHandle,
|
|
88
|
+
INavigatorPlatform,
|
|
89
|
+
INavigatorState,
|
|
90
|
+
IRoute,
|
|
91
|
+
IScreenRenderPlan,
|
|
92
|
+
} from '../../core';
|
|
93
|
+
import { hostElement } from '../host';
|
|
94
|
+
import {
|
|
95
|
+
NavigationScopeProvider,
|
|
96
|
+
useNavigationScope,
|
|
97
|
+
} from '../navigation-context';
|
|
98
|
+
import type { INavigationScope } from '../navigation-context';
|
|
99
|
+
import {
|
|
100
|
+
ScreenCollectorProvider,
|
|
101
|
+
createScreenSignal,
|
|
102
|
+
toRegistry,
|
|
103
|
+
} from '../screen-registry';
|
|
104
|
+
import type { IRegisteredScreen } from '../screen-registry';
|
|
105
|
+
import { Screen } from '../screen';
|
|
106
|
+
import type {
|
|
107
|
+
IScreenOptionsArgs,
|
|
108
|
+
IScreenProps,
|
|
109
|
+
ISolidScreenOptions,
|
|
110
|
+
} from '../screen-props';
|
|
111
|
+
|
|
112
|
+
export type { INavigatorHandle } from '../../core';
|
|
113
|
+
|
|
114
|
+
// React's `children?: ReactNode` and Vue's default slot become Solid's `children` - the markers,
|
|
115
|
+
// which register themselves rather than being scanned. `ref` is Solid's own spelling of Vue's
|
|
116
|
+
// `expose()`: a callback the compiler hands the navigator handle.
|
|
117
|
+
export type IStackProps = {
|
|
118
|
+
initialRouteName?: string;
|
|
119
|
+
screenOptions?: ISolidScreenOptions;
|
|
120
|
+
ref?: (handle: INavigatorHandle) => void;
|
|
121
|
+
children?: JSX.Element;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// backTitleVisible defaults to `true` on both platforms per the codegen spec's own default
|
|
125
|
+
// (CT.WithDefault<boolean, 'true'>) - no ios/android divergence in v1 scope.
|
|
126
|
+
const NAVIGATOR_PLATFORM: INavigatorPlatform = {
|
|
127
|
+
defaultHeaderBackTitleVisible: true,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const EMPTY_STATE: INavigatorState = { routes: [] };
|
|
131
|
+
|
|
132
|
+
// Route keys must be unique per navigator INSTANCE. React/Vue take that from useId(); Solid has no
|
|
133
|
+
// equivalent, so a module counter stands in - read once, at construction.
|
|
134
|
+
let navigatorSequence = 0;
|
|
135
|
+
|
|
136
|
+
type IStackScreenEntry = IRegisteredScreen<IScreenProps['options']>;
|
|
137
|
+
|
|
138
|
+
// What makes a route's NATIVE shape different rather than just its props (header note 3).
|
|
139
|
+
type IScreenShape = {
|
|
140
|
+
viewName: string;
|
|
141
|
+
inModal: boolean;
|
|
142
|
+
hasSearchBar: boolean;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
function sameShape(a: IScreenShape, b: IScreenShape): boolean {
|
|
146
|
+
return (
|
|
147
|
+
a.viewName === b.viewName &&
|
|
148
|
+
a.inModal === b.inModal &&
|
|
149
|
+
a.hasSearchBar === b.hasSearchBar
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sameKeys(a: readonly string[], b: readonly string[]): boolean {
|
|
154
|
+
return a.length === b.length && a.every((key, index) => key === b[index]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveScreenOptions(
|
|
158
|
+
entry: IStackScreenEntry,
|
|
159
|
+
args: IScreenOptionsArgs,
|
|
160
|
+
screenOptions: ISolidScreenOptions | undefined,
|
|
161
|
+
): ISolidScreenOptions {
|
|
162
|
+
const own =
|
|
163
|
+
typeof entry.options === 'function' ? entry.options(args) : entry.options;
|
|
164
|
+
return { ...screenOptions, ...own };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function StackImpl(props: IStackProps): JSX.Element {
|
|
168
|
+
// Read BEFORE this Stack provides its own per-screen scope below - becomes the `parent` link a
|
|
169
|
+
// nested screen's useNavigation().getParent() walks. undefined when this Stack is the root.
|
|
170
|
+
const parentScope = useNavigationScope();
|
|
171
|
+
|
|
172
|
+
const { screens, collector } = createScreenSignal<
|
|
173
|
+
'stack',
|
|
174
|
+
IScreenProps['options']
|
|
175
|
+
>('stack');
|
|
176
|
+
const registry = createMemo(() => toRegistry(screens()));
|
|
177
|
+
|
|
178
|
+
navigatorSequence += 1;
|
|
179
|
+
const routeIdPrefix = `stack-${navigatorSequence}`;
|
|
180
|
+
let routeSequence = 0;
|
|
181
|
+
|
|
182
|
+
function createRoute(name: string, params: unknown): IRoute<unknown> {
|
|
183
|
+
routeSequence += 1;
|
|
184
|
+
return { key: `${routeIdPrefix}-${name}-${routeSequence}`, name, params };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// One emitter per route.key, created lazily and pruned once the route is popped (below).
|
|
188
|
+
const emitters = new Map<string, INavigationEmitter>();
|
|
189
|
+
function emitterFor(routeKey: string): INavigationEmitter {
|
|
190
|
+
let emitter = emitters.get(routeKey);
|
|
191
|
+
if (emitter === undefined) {
|
|
192
|
+
emitter = createNavigationEmitter();
|
|
193
|
+
emitters.set(routeKey, emitter);
|
|
194
|
+
}
|
|
195
|
+
return emitter;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const [dispatched, setDispatched] = createSignal<INavigatorState | null>(
|
|
199
|
+
null,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// The initial push, deferred until the markers have registered (header note 1). Cached in a plain
|
|
203
|
+
// local rather than a memo: it must run EXACTLY once - a memo re-running would mint a second
|
|
204
|
+
// route key for the same screen and silently reset the stack.
|
|
205
|
+
let seeded: INavigatorState | undefined;
|
|
206
|
+
function seed(entries: Map<string, IStackScreenEntry>): INavigatorState {
|
|
207
|
+
if (seeded !== undefined) return seeded;
|
|
208
|
+
// Deliberately NOT seeded from an empty registry, even when `initialRouteName` names the route:
|
|
209
|
+
// <For> maps a key exactly once, so a route minted before its marker exists would be handed a
|
|
210
|
+
// build with no component and stay blank forever. Waiting one update costs nothing - the
|
|
211
|
+
// markers register during the same flush this navigator is created in.
|
|
212
|
+
if (entries.size === 0) {
|
|
213
|
+
dlog('Stack: no <Stack.Screen> children registered');
|
|
214
|
+
return EMPTY_STATE;
|
|
215
|
+
}
|
|
216
|
+
const initialRouteName =
|
|
217
|
+
props.initialRouteName ?? entries.keys().next().value;
|
|
218
|
+
if (initialRouteName === undefined) return EMPTY_STATE;
|
|
219
|
+
seeded = createInitialNavigatorState(
|
|
220
|
+
createRoute(
|
|
221
|
+
initialRouteName,
|
|
222
|
+
entries.get(initialRouteName)?.initialParams,
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
return seeded;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// A <Stack.Screen> marker can vanish while its route is still in the pushed history, which would
|
|
229
|
+
// leave that entry with nothing to render (reconcileStackRoutes' header). Reconciling on READ
|
|
230
|
+
// keeps the repair out of any write path; the next dispatch then persists it.
|
|
231
|
+
const currentState = createMemo<INavigatorState>(() => {
|
|
232
|
+
const entries = registry();
|
|
233
|
+
const state = dispatched();
|
|
234
|
+
if (state === null) return seed(entries);
|
|
235
|
+
return reconcileStackRoutes(state, [...entries.keys()]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
function dispatch(action: Parameters<typeof navigatorReducer>[1]): void {
|
|
239
|
+
setDispatched(navigatorReducer(currentState(), action));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const handle: INavigatorHandle = {
|
|
243
|
+
push: (name, params) =>
|
|
244
|
+
dispatch({ type: 'push', route: createRoute(name, params) }),
|
|
245
|
+
pop: count => dispatch({ type: 'pop', count }),
|
|
246
|
+
popToTop: () => dispatch({ type: 'popToTop' }),
|
|
247
|
+
popTo: key => dispatch({ type: 'popTo', key }),
|
|
248
|
+
replace: (name, params) =>
|
|
249
|
+
dispatch({ type: 'replace', route: createRoute(name, params) }),
|
|
250
|
+
setParams: (params, key) => dispatch({ type: 'setParams', key, params }),
|
|
251
|
+
reset: nextState => dispatch({ type: 'reset', state: nextState }),
|
|
252
|
+
canGoBack: () => currentState().routes.length > 1,
|
|
253
|
+
};
|
|
254
|
+
props.ref?.(handle);
|
|
255
|
+
|
|
256
|
+
// Broadcasts the router state to every still-live route's emitter (createNavigationState's
|
|
257
|
+
// source) and prunes emitters for popped routes. Deferred by a microtask - header note 4.
|
|
258
|
+
createEffect(() => {
|
|
259
|
+
const current = currentState();
|
|
260
|
+
queueMicrotask(() => {
|
|
261
|
+
for (const route of current.routes) {
|
|
262
|
+
emitterFor(route.key).emit(NAVIGATION_EVENT_STATE, current);
|
|
263
|
+
}
|
|
264
|
+
for (const routeKey of [...emitters.keys()]) {
|
|
265
|
+
if (!current.routes.some(route => route.key === routeKey)) {
|
|
266
|
+
emitters.delete(routeKey);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Header note 2: keyed on route KEYS, so a setParams (new route object, same key) never reaches
|
|
273
|
+
// <For> and never rebuilds a screen subtree.
|
|
274
|
+
const routeKeys = createMemo<readonly string[]>(
|
|
275
|
+
() => currentState().routes.map(route => route.key),
|
|
276
|
+
[],
|
|
277
|
+
{ equals: sameKeys },
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
function renderRoute(routeKey: string, index: Accessor<number>): JSX.Element {
|
|
281
|
+
const route = createMemo(() =>
|
|
282
|
+
currentState().routes.find(candidate => candidate.key === routeKey),
|
|
283
|
+
);
|
|
284
|
+
const entry = createMemo(() => {
|
|
285
|
+
const current = route();
|
|
286
|
+
return current === undefined ? undefined : registry().get(current.name);
|
|
287
|
+
});
|
|
288
|
+
const emitter = emitterFor(routeKey);
|
|
289
|
+
// The host node the app's search-bar ref last saw. `spread` re-runs a bag's `ref` on EVERY prop
|
|
290
|
+
// change, and an app expects one attach per node the way React's ref contract gives it.
|
|
291
|
+
let lastSearchBarNode: ISymbioteNode | null = null;
|
|
292
|
+
|
|
293
|
+
// Keeping the PREVIOUS plan when the route or its marker vanishes: <For> disposes this item in
|
|
294
|
+
// the same update, but memo evaluation order inside one batch is not ours to choose, so the
|
|
295
|
+
// live prop accessors below must never observe an undefined plan mid-teardown.
|
|
296
|
+
const plan = createMemo<IScreenRenderPlan | undefined>(previous => {
|
|
297
|
+
const current = route();
|
|
298
|
+
const registered = entry();
|
|
299
|
+
if (current === undefined || registered === undefined) return previous;
|
|
300
|
+
|
|
301
|
+
const merged = resolveScreenOptions(
|
|
302
|
+
registered,
|
|
303
|
+
{ route: current, navigation: handle },
|
|
304
|
+
props.screenOptions,
|
|
305
|
+
);
|
|
306
|
+
const searchBarOptions = merged.headerSearchBarOptions;
|
|
307
|
+
|
|
308
|
+
return resolveScreenRenderPlan({
|
|
309
|
+
screenId: current.key,
|
|
310
|
+
index: index(),
|
|
311
|
+
routeCount: currentState().routes.length,
|
|
312
|
+
options: merged,
|
|
313
|
+
platform: NAVIGATOR_PLATFORM,
|
|
314
|
+
isAndroid: Platform.OS === 'android',
|
|
315
|
+
screenPassthrough: {
|
|
316
|
+
[SCREEN_ON_DISMISSED]: () => dispatch({ type: 'pop', count: 1 }),
|
|
317
|
+
[SCREEN_ON_HEADER_BACK_BUTTON_CLICKED]: () =>
|
|
318
|
+
dispatch({ type: 'pop', count: 1 }),
|
|
319
|
+
// onAppear/onDisappear are the definitive visibility boundary (post-transition), so
|
|
320
|
+
// 'focus'/'blur' fire exactly once per transition; onWillAppear/onWillDisappear fire
|
|
321
|
+
// BEFORE the animation and would double-invoke createFocusEffect, so they only log.
|
|
322
|
+
[SCREEN_ON_WILL_APPEAR]: () =>
|
|
323
|
+
dlog(
|
|
324
|
+
`Stack: route "${current.name}" will appear at t=${Date.now()}`,
|
|
325
|
+
),
|
|
326
|
+
[SCREEN_ON_APPEAR]: () => {
|
|
327
|
+
dlog(
|
|
328
|
+
`Stack: route "${current.name}" appeared (focus) at t=${Date.now()}`,
|
|
329
|
+
);
|
|
330
|
+
emitter.emit(NAVIGATION_EVENT_FOCUS);
|
|
331
|
+
},
|
|
332
|
+
[SCREEN_ON_WILL_DISAPPEAR]: () =>
|
|
333
|
+
dlog(
|
|
334
|
+
`Stack: route "${current.name}" will disappear at t=${Date.now()}`,
|
|
335
|
+
),
|
|
336
|
+
[SCREEN_ON_DISAPPEAR]: () => {
|
|
337
|
+
dlog(
|
|
338
|
+
`Stack: route "${current.name}" disappeared (blur) at t=${Date.now()}`,
|
|
339
|
+
);
|
|
340
|
+
emitter.emit(NAVIGATION_EVENT_BLUR);
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
searchBarPassthrough:
|
|
344
|
+
searchBarOptions === undefined
|
|
345
|
+
? undefined
|
|
346
|
+
: {
|
|
347
|
+
...buildSearchBarPassthrough(searchBarOptions, message =>
|
|
348
|
+
dlog(`Stack: route "${current.name}" ${message}`),
|
|
349
|
+
),
|
|
350
|
+
// The imperative ref (ISearchBarCommands): `spread` calls a `ref` found in the bag
|
|
351
|
+
// with the host node, so it rides down inside passthrough untouched.
|
|
352
|
+
ref: (element: unknown): void => {
|
|
353
|
+
const node = isSymbioteNode(element) ? element : null;
|
|
354
|
+
if (node === lastSearchBarNode) return;
|
|
355
|
+
lastSearchBarNode = node;
|
|
356
|
+
dlog(
|
|
357
|
+
`Stack: search bar ref, node=${node === null ? 'null' : debugNodeId(node)}`,
|
|
358
|
+
);
|
|
359
|
+
searchBarOptions.ref?.(
|
|
360
|
+
node === null ? null : buildSearchBarHandle(() => node),
|
|
361
|
+
);
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
}, undefined);
|
|
366
|
+
|
|
367
|
+
// NOT an early `return undefined` when the plan is still unresolved. <For> maps a key exactly
|
|
368
|
+
// ONCE, so bailing here would strand the route with nothing to paint for the rest of its life -
|
|
369
|
+
// and an unresolved plan is the NORMAL first state, since the markers register after the
|
|
370
|
+
// navigator's body has run (header note 1).
|
|
371
|
+
const shape = createMemo<IScreenShape | undefined>(
|
|
372
|
+
() => {
|
|
373
|
+
const current = plan();
|
|
374
|
+
if (current === undefined) return undefined;
|
|
375
|
+
return {
|
|
376
|
+
viewName: current.screenViewName,
|
|
377
|
+
inModal: current.inModal,
|
|
378
|
+
hasSearchBar: current.searchBarProps !== undefined,
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
undefined,
|
|
382
|
+
{
|
|
383
|
+
equals: (a, b) =>
|
|
384
|
+
a === b || (a !== undefined && b !== undefined && sameShape(a, b)),
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
function buildScreen(current: IScreenShape): ISymbioteNode | undefined {
|
|
389
|
+
// Re-read rather than closed over: this runs on every rebuild boundary crossing, and the
|
|
390
|
+
// fallbacks below keep the live accessors from ever seeing `undefined` mid-teardown.
|
|
391
|
+
const builtPlan = plan();
|
|
392
|
+
const builtRoute = route();
|
|
393
|
+
const builtEntry = entry();
|
|
394
|
+
if (
|
|
395
|
+
builtPlan === undefined ||
|
|
396
|
+
builtRoute === undefined ||
|
|
397
|
+
builtEntry === undefined
|
|
398
|
+
) {
|
|
399
|
+
dlog(`Stack: no screen registered for route key "${routeKey}"`);
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
const livePlan = (): IScreenRenderPlan => plan() ?? builtPlan;
|
|
403
|
+
const liveRoute = (): IRoute<unknown> => route() ?? builtRoute;
|
|
404
|
+
const initialComponent = builtEntry.component;
|
|
405
|
+
|
|
406
|
+
const scope: INavigationScope = () => ({
|
|
407
|
+
route: liveRoute(),
|
|
408
|
+
navigation: handle,
|
|
409
|
+
emitter,
|
|
410
|
+
parent: parentScope?.(),
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// The narrower boundary inside the shape one: a screen swapping its `component` rebuilds only
|
|
414
|
+
// the body, never the native screen.
|
|
415
|
+
const component = createMemo(
|
|
416
|
+
() => entry()?.component ?? initialComponent,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// Must not be flattened away (collapsable: false, supplied by contentWrapperProps) -
|
|
420
|
+
// react-native-screens' native side finds THIS view type by class check to register a
|
|
421
|
+
// formSheet's content for sizing (RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME in core/constants.ts).
|
|
422
|
+
const contentWrapper = hostElement(
|
|
423
|
+
RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME,
|
|
424
|
+
() => livePlan().contentWrapperProps,
|
|
425
|
+
);
|
|
426
|
+
insert(
|
|
427
|
+
contentWrapper,
|
|
428
|
+
createComponent(NavigationScopeProvider, {
|
|
429
|
+
value: scope,
|
|
430
|
+
// The screen component is created HERE, inside the Provider's own children resolution,
|
|
431
|
+
// not in a memo declared above it - a Solid computation captures its owner at CREATION,
|
|
432
|
+
// so a component built outside this getter would run with the enclosing owner and see no
|
|
433
|
+
// navigation scope at all (useRoute would throw). Reading `component()` tracked makes the
|
|
434
|
+
// getter the rebuild boundary for a component swap; `untrack` around the build keeps the
|
|
435
|
+
// screen's own prop reads out of that boundary. No route/navigation props: the screen
|
|
436
|
+
// reads both through the primitives off the scope provided here.
|
|
437
|
+
get children(): JSX.Element {
|
|
438
|
+
const Component = component();
|
|
439
|
+
return untrack(() => createComponent(Component, {}));
|
|
440
|
+
},
|
|
441
|
+
}),
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// The header config (and its RNSSearchBar child, when the screen has one) is a pure
|
|
445
|
+
// Descriptor leaf, so it goes through the shared bridge verbatim, exactly like React's and
|
|
446
|
+
// Vue's do. Its child COUNT is part of `shape` above, which is what keeps the bridge's
|
|
447
|
+
// shape-stability contract satisfied when an app adds or removes headerSearchBarOptions.
|
|
448
|
+
const headerConfig = descriptorToSolid(() => livePlan().headerConfig);
|
|
449
|
+
|
|
450
|
+
const screen = hostElement(
|
|
451
|
+
current.viewName,
|
|
452
|
+
() => livePlan().screenProps,
|
|
453
|
+
);
|
|
454
|
+
if (!current.inModal) {
|
|
455
|
+
insertNode(screen, headerConfig);
|
|
456
|
+
insertNode(screen, contentWrapper);
|
|
457
|
+
return screen;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// A modal/formSheet screen has no UINavigationController of its own on iOS - nest an inner
|
|
461
|
+
// RNSScreenStack/RNSScreen purely to host the native header bar. Skipping this leaves
|
|
462
|
+
// RNSScreenStackHeaderConfig with no navigation controller to attach to, so the header
|
|
463
|
+
// silently never renders.
|
|
464
|
+
const innerStack = hostElement(RNS_SCREEN_STACK_VIEW_NAME, () => ({
|
|
465
|
+
style: livePlan().innerStackStyle,
|
|
466
|
+
}));
|
|
467
|
+
// activityState mirrors the outer Screen's - RNSScreen.mm treats an unset/inactive nested
|
|
468
|
+
// screen as not yet pushed, leaving it parked at its pre-push transition position (off past
|
|
469
|
+
// the bottom edge) instead of its real, presented frame.
|
|
470
|
+
const innerScreen = hostElement(RNS_SCREEN_VIEW_NAME, () => ({
|
|
471
|
+
style: livePlan().innerScreenStyle,
|
|
472
|
+
activityState: livePlan().activityState,
|
|
473
|
+
}));
|
|
474
|
+
insertNode(innerScreen, headerConfig);
|
|
475
|
+
insertNode(innerScreen, contentWrapper);
|
|
476
|
+
insertNode(innerStack, innerScreen);
|
|
477
|
+
insertNode(screen, innerStack);
|
|
478
|
+
return screen;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return createMemo(() => {
|
|
482
|
+
const current = shape();
|
|
483
|
+
if (current === undefined) return undefined;
|
|
484
|
+
return untrack(() => buildScreen(current));
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Investigation instrumentation (flicker-on-focus bug): STACK_ON_FINISH_TRANSITIONING is the
|
|
489
|
+
// native signal that the WHOLE push/pop animation finished, as opposed to the per-screen
|
|
490
|
+
// onAppear/onDisappear. Kept behind DEBUG, never removed.
|
|
491
|
+
const stackProps = resolveStackProps({
|
|
492
|
+
passthrough: {
|
|
493
|
+
[STACK_ON_FINISH_TRANSITIONING]: () =>
|
|
494
|
+
dlog(`Stack: onFinishTransitioning at t=${Date.now()}`),
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const root = hostElement(RNS_SCREEN_STACK_VIEW_NAME, () => stackProps);
|
|
499
|
+
// The <For> component is created EAGERLY and handed to `insert` as a value, never as
|
|
500
|
+
// `() => createComponent(For, …)`. The latter would read `each` inside insert's own render
|
|
501
|
+
// effect, so every route change would re-create the whole list instead of letting For's keyed
|
|
502
|
+
// mapping reuse the screens it already built.
|
|
503
|
+
insert(
|
|
504
|
+
root,
|
|
505
|
+
createComponent(For, {
|
|
506
|
+
get each(): readonly string[] {
|
|
507
|
+
return routeKeys();
|
|
508
|
+
},
|
|
509
|
+
children: renderRoute,
|
|
510
|
+
}),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
onCleanup(() => emitters.clear());
|
|
514
|
+
|
|
515
|
+
// `props.children` is READ here and nowhere else: reading it is what CREATES the markers, and
|
|
516
|
+
// they must be created inside this Provider so their collectStackScreen() finds the collector on
|
|
517
|
+
// the owner chain. They paint nothing, so the array is effectively just `root` - and `root` is a
|
|
518
|
+
// pre-built node reference, so a marker list that changes re-runs this getter without disturbing
|
|
519
|
+
// the navigator tree.
|
|
520
|
+
return createComponent(ScreenCollectorProvider, {
|
|
521
|
+
value: collector,
|
|
522
|
+
get children(): JSX.Element {
|
|
523
|
+
return [props.children, root];
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export const Stack = Object.assign(StackImpl, { Screen });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Tab.Screen's public prop types. Same split rationale as screen-props.ts: `component` is a Solid
|
|
2
|
+
// value, everything else rides on the agnostic base from ../core.
|
|
3
|
+
|
|
4
|
+
import type { Component } from 'solid-js';
|
|
5
|
+
import type { IRoute, ITabNavigatorHandle, ITabOptions } from '../core';
|
|
6
|
+
|
|
7
|
+
export type ITabScreenOptionsArgs = {
|
|
8
|
+
route: IRoute<unknown>;
|
|
9
|
+
navigation: ITabNavigatorHandle;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ITabScreenOptionsResolver = (
|
|
13
|
+
args: ITabScreenOptionsArgs,
|
|
14
|
+
) => ITabOptions;
|
|
15
|
+
|
|
16
|
+
export type ITabScreenProps = {
|
|
17
|
+
name: string;
|
|
18
|
+
component: Component;
|
|
19
|
+
options?: ITabOptions | ITabScreenOptionsResolver;
|
|
20
|
+
initialParams?: unknown;
|
|
21
|
+
};
|