@symbiote-native/navigation 2.1.0 → 2.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.
Files changed (234) hide show
  1. package/README.md +42 -12
  2. package/build/angular/drawer/index.js +23 -9
  3. package/build/angular/index.d.ts +4 -4
  4. package/build/angular/injectors/inject-navigation.js +1 -1
  5. package/build/angular/navigation-scope.directive.js +1 -1
  6. package/build/angular/search-bar-ref.directive.js +1 -1
  7. package/build/angular/stack/index.js +20 -6
  8. package/build/angular/tabs/index.js +21 -7
  9. package/build/core/drawer-options/index.js +19 -5
  10. package/build/core/drawer-router-state/index.js +8 -2
  11. package/build/core/guards.js +3 -1
  12. package/build/core/header-bar-buttons.js +14 -4
  13. package/build/core/index.d.ts +4 -4
  14. package/build/core/index.js +2 -2
  15. package/build/core/navigator-state/index.js +12 -4
  16. package/build/core/render-drawer.js +4 -1
  17. package/build/core/render-stack.js +13 -4
  18. package/build/core/screen-options.js +1 -1
  19. package/build/core/search-bar-commands.js +1 -1
  20. package/build/core/state-persistence/index.js +6 -2
  21. package/build/core/tab-router-state/index.js +8 -2
  22. package/build/react/drawer/index.js +36 -13
  23. package/build/react/index.d.ts +7 -7
  24. package/build/react/index.js +2 -2
  25. package/build/react/stack/index.js +34 -8
  26. package/build/react/tabs/index.js +14 -4
  27. package/build/solid/drawer/index.d.ts +22 -0
  28. package/build/solid/drawer/index.js +394 -0
  29. package/build/solid/drawer-screen-props.d.ts +13 -0
  30. package/build/solid/drawer-screen-props.js +2 -0
  31. package/build/solid/host.d.ts +2 -0
  32. package/build/solid/host.js +23 -0
  33. package/build/solid/index.d.ts +19 -0
  34. package/build/solid/index.js +24 -0
  35. package/build/solid/linking.d.ts +3 -0
  36. package/build/solid/linking.js +55 -0
  37. package/build/solid/navigation-context.d.ts +13 -0
  38. package/build/solid/navigation-context.js +37 -0
  39. package/build/solid/primitives/create-focus-effect.d.ts +1 -0
  40. package/build/solid/primitives/create-focus-effect.js +30 -0
  41. package/build/solid/primitives/create-is-focused.d.ts +2 -0
  42. package/build/solid/primitives/create-is-focused.js +31 -0
  43. package/build/solid/primitives/create-navigation-state.d.ts +3 -0
  44. package/build/solid/primitives/create-navigation-state.js +30 -0
  45. package/build/solid/primitives/index.d.ts +12 -0
  46. package/build/solid/primitives/index.js +8 -0
  47. package/build/solid/primitives/use-drawer-navigation.d.ts +5 -0
  48. package/build/solid/primitives/use-drawer-navigation.js +15 -0
  49. package/build/solid/primitives/use-navigation.d.ts +8 -0
  50. package/build/solid/primitives/use-navigation.js +23 -0
  51. package/build/solid/primitives/use-route.d.ts +3 -0
  52. package/build/solid/primitives/use-route.js +14 -0
  53. package/build/solid/primitives/use-stack-navigation.d.ts +5 -0
  54. package/build/solid/primitives/use-stack-navigation.js +19 -0
  55. package/build/solid/primitives/use-tab-navigation.d.ts +5 -0
  56. package/build/solid/primitives/use-tab-navigation.js +15 -0
  57. package/build/solid/screen-props.d.ts +19 -0
  58. package/build/solid/screen-props.js +9 -0
  59. package/build/solid/screen-registry.d.ts +31 -0
  60. package/build/solid/screen-registry.js +86 -0
  61. package/build/solid/screen.d.ts +6 -0
  62. package/build/solid/screen.js +69 -0
  63. package/build/solid/stack/index.d.ts +15 -0
  64. package/build/solid/stack/index.js +348 -0
  65. package/build/solid/tab-screen-props.d.ts +13 -0
  66. package/build/solid/tab-screen-props.js +3 -0
  67. package/build/solid/tabs/index.d.ts +14 -0
  68. package/build/solid/tabs/index.js +228 -0
  69. package/build/svelte/descriptor-subtree.js +3 -1
  70. package/build/svelte/drawer/index.svelte +163 -25
  71. package/build/svelte/drawer-screen.svelte +2 -1
  72. package/build/svelte/index.d.ts +3 -3
  73. package/build/svelte/navigation-scope.svelte +4 -1
  74. package/build/svelte/runes/use-navigation.svelte.js +5 -1
  75. package/build/svelte/stack/index.svelte +43 -6
  76. package/build/svelte/stack/stack-screen.svelte +66 -9
  77. package/build/svelte/tabs/index.svelte +42 -20
  78. package/build/vue/composables/use-navigation.js +5 -1
  79. package/build/vue/drawer/index.js +30 -8
  80. package/build/vue/index.d.ts +3 -3
  81. package/build/vue/navigation-context.js +1 -1
  82. package/build/vue/stack/index.js +17 -6
  83. package/build/vue/tabs/index.js +18 -5
  84. package/build-ngc/angular/drawer/index.js +31 -11
  85. package/build-ngc/angular/drawer/index.js.map +1 -1
  86. package/build-ngc/angular/index.d.ts +4 -4
  87. package/build-ngc/angular/index.js.map +1 -1
  88. package/build-ngc/angular/injectors/inject-focus-effect.js.map +1 -1
  89. package/build-ngc/angular/injectors/inject-is-focused.js.map +1 -1
  90. package/build-ngc/angular/injectors/inject-navigation-state.js.map +1 -1
  91. package/build-ngc/angular/injectors/inject-navigation.js +1 -1
  92. package/build-ngc/angular/injectors/inject-navigation.js.map +1 -1
  93. package/build-ngc/angular/linking.js.map +1 -1
  94. package/build-ngc/angular/navigation-context.service.js.map +1 -1
  95. package/build-ngc/angular/navigation-scope.directive.js +1 -1
  96. package/build-ngc/angular/navigation-scope.directive.js.map +1 -1
  97. package/build-ngc/angular/screen.directive.js.map +1 -1
  98. package/build-ngc/angular/search-bar-ref.directive.js +1 -1
  99. package/build-ngc/angular/search-bar-ref.directive.js.map +1 -1
  100. package/build-ngc/angular/stack/index.js +26 -8
  101. package/build-ngc/angular/stack/index.js.map +1 -1
  102. package/build-ngc/angular/tab-screen.directive.js.map +1 -1
  103. package/build-ngc/angular/tabs/index.js +21 -7
  104. package/build-ngc/angular/tabs/index.js.map +1 -1
  105. package/build-ngc/core/constants.js.map +1 -1
  106. package/build-ngc/core/drawer-options/index.js +19 -5
  107. package/build-ngc/core/drawer-options/index.js.map +1 -1
  108. package/build-ngc/core/drawer-router-state/index.js +8 -2
  109. package/build-ngc/core/drawer-router-state/index.js.map +1 -1
  110. package/build-ngc/core/guards.js +3 -1
  111. package/build-ngc/core/guards.js.map +1 -1
  112. package/build-ngc/core/header-bar-buttons.js +14 -4
  113. package/build-ngc/core/header-bar-buttons.js.map +1 -1
  114. package/build-ngc/core/index.d.ts +4 -4
  115. package/build-ngc/core/index.js +2 -2
  116. package/build-ngc/core/index.js.map +1 -1
  117. package/build-ngc/core/linking-config/index.js.map +1 -1
  118. package/build-ngc/core/navigation-events/index.js.map +1 -1
  119. package/build-ngc/core/navigator-handles/index.js.map +1 -1
  120. package/build-ngc/core/navigator-state/index.js +12 -4
  121. package/build-ngc/core/navigator-state/index.js.map +1 -1
  122. package/build-ngc/core/render-drawer.js +4 -1
  123. package/build-ngc/core/render-drawer.js.map +1 -1
  124. package/build-ngc/core/render-stack.js +13 -4
  125. package/build-ngc/core/render-stack.js.map +1 -1
  126. package/build-ngc/core/render-tabs.js.map +1 -1
  127. package/build-ngc/core/screen-options.js +1 -1
  128. package/build-ngc/core/screen-options.js.map +1 -1
  129. package/build-ngc/core/search-bar-commands.js +1 -1
  130. package/build-ngc/core/search-bar-commands.js.map +1 -1
  131. package/build-ngc/core/stack-router-state/index.js.map +1 -1
  132. package/build-ngc/core/state-persistence/index.js +6 -2
  133. package/build-ngc/core/state-persistence/index.js.map +1 -1
  134. package/build-ngc/core/tab-router-state/index.js +8 -2
  135. package/build-ngc/core/tab-router-state/index.js.map +1 -1
  136. package/package.json +23 -9
  137. package/src/angular/drawer/index.ts +69 -25
  138. package/src/angular/index.ts +20 -4
  139. package/src/angular/injectors/inject-focus-effect.ts +8 -2
  140. package/src/angular/injectors/inject-is-focused.ts +6 -4
  141. package/src/angular/injectors/inject-navigation-state.ts +7 -4
  142. package/src/angular/injectors/inject-navigation.ts +12 -3
  143. package/src/angular/linking.ts +4 -1
  144. package/src/angular/navigation-context.service.ts +5 -2
  145. package/src/angular/navigation-scope.directive.ts +6 -2
  146. package/src/angular/screen.directive.ts +13 -3
  147. package/src/angular/search-bar-ref.directive.ts +10 -2
  148. package/src/angular/stack/index.ts +68 -22
  149. package/src/angular/tab-screen.directive.ts +3 -1
  150. package/src/angular/tabs/index.ts +36 -12
  151. package/src/core/constants.ts +6 -3
  152. package/src/core/drawer-options/index.ts +40 -13
  153. package/src/core/drawer-router-state/index.ts +11 -3
  154. package/src/core/guards.ts +3 -1
  155. package/src/core/header-bar-buttons.ts +41 -12
  156. package/src/core/index.ts +18 -4
  157. package/src/core/linking-config/index.ts +31 -10
  158. package/src/core/navigation-events/index.ts +8 -2
  159. package/src/core/navigator-handles/index.ts +8 -3
  160. package/src/core/navigator-props.ts +13 -4
  161. package/src/core/navigator-state/index.ts +24 -7
  162. package/src/core/render-drawer.ts +24 -9
  163. package/src/core/render-stack.ts +52 -13
  164. package/src/core/render-tabs.ts +24 -6
  165. package/src/core/screen-options.ts +15 -5
  166. package/src/core/search-bar-commands.ts +12 -3
  167. package/src/core/stack-router-state/index.ts +5 -2
  168. package/src/core/state-persistence/index.ts +13 -3
  169. package/src/core/tab-router-state/index.ts +10 -3
  170. package/src/react/drawer/index.ts +408 -344
  171. package/src/react/drawer-screen.ts +8 -2
  172. package/src/react/hooks/use-focus-effect.ts +8 -2
  173. package/src/react/hooks/use-is-focused.ts +6 -2
  174. package/src/react/hooks/use-navigation.ts +8 -2
  175. package/src/react/index.ts +37 -7
  176. package/src/react/linking.ts +4 -1
  177. package/src/react/navigation-context.ts +6 -2
  178. package/src/react/screen.ts +12 -3
  179. package/src/react/stack/index.ts +294 -231
  180. package/src/react/tab-screen.ts +3 -1
  181. package/src/react/tabs/index.ts +188 -158
  182. package/src/solid/drawer/index.ts +570 -0
  183. package/src/solid/drawer-screen-props.ts +24 -0
  184. package/src/solid/host.ts +29 -0
  185. package/src/solid/index.ts +87 -0
  186. package/src/solid/linking.ts +74 -0
  187. package/src/solid/navigation-context.ts +56 -0
  188. package/src/solid/primitives/create-focus-effect.ts +41 -0
  189. package/src/solid/primitives/create-is-focused.ts +41 -0
  190. package/src/solid/primitives/create-navigation-state.ts +44 -0
  191. package/src/solid/primitives/index.ts +12 -0
  192. package/src/solid/primitives/use-drawer-navigation.ts +25 -0
  193. package/src/solid/primitives/use-navigation.ts +44 -0
  194. package/src/solid/primitives/use-route.ts +18 -0
  195. package/src/solid/primitives/use-stack-navigation.ts +29 -0
  196. package/src/solid/primitives/use-tab-navigation.ts +25 -0
  197. package/src/solid/screen-props.ts +51 -0
  198. package/src/solid/screen-registry.ts +159 -0
  199. package/src/solid/screen.ts +80 -0
  200. package/src/solid/stack/index.ts +528 -0
  201. package/src/solid/tab-screen-props.ts +21 -0
  202. package/src/solid/tabs/index.ts +327 -0
  203. package/src/svelte/attachments.ts +3 -1
  204. package/src/svelte/descriptor-subtree.ts +3 -1
  205. package/src/svelte/drawer/index.svelte +163 -25
  206. package/src/svelte/drawer-screen-props.ts +8 -2
  207. package/src/svelte/drawer-screen.svelte +2 -1
  208. package/src/svelte/fabric-tree.test-helper.ts +37 -3
  209. package/src/svelte/index.ts +15 -3
  210. package/src/svelte/linking.svelte.ts +11 -5
  211. package/src/svelte/navigation-scope.svelte +4 -1
  212. package/src/svelte/runes/use-drawer-navigation.svelte.ts +3 -1
  213. package/src/svelte/runes/use-focus-effect.svelte.ts +8 -2
  214. package/src/svelte/runes/use-navigation-state.svelte.ts +3 -1
  215. package/src/svelte/runes/use-navigation.svelte.ts +13 -3
  216. package/src/svelte/runes/use-stack-navigation.svelte.ts +3 -1
  217. package/src/svelte/screen-props.ts +7 -2
  218. package/src/svelte/screen-registry.ts +9 -3
  219. package/src/svelte/stack/index.svelte +43 -6
  220. package/src/svelte/stack/stack-screen.svelte +66 -9
  221. package/src/svelte/svelte-compile.test-helper.ts +16 -12
  222. package/src/svelte/tab-screen-props.ts +3 -1
  223. package/src/svelte/tabs/index.svelte +42 -20
  224. package/src/vue/composables/use-navigation-state.ts +7 -4
  225. package/src/vue/composables/use-navigation.ts +13 -3
  226. package/src/vue/drawer/index.ts +122 -36
  227. package/src/vue/drawer-screen.ts +12 -5
  228. package/src/vue/index.ts +15 -3
  229. package/src/vue/linking.ts +7 -2
  230. package/src/vue/navigation-context.ts +17 -5
  231. package/src/vue/screen.ts +7 -2
  232. package/src/vue/stack/index.ts +82 -34
  233. package/src/vue/tab-screen.ts +3 -1
  234. package/src/vue/tabs/index.ts +51 -13
@@ -50,11 +50,20 @@ import {
50
50
  resolveScreenRenderPlan,
51
51
  resolveStackProps,
52
52
  } from '../../core';
53
- import type { INavigationEmitter, INavigatorHandle, INavigatorPlatform, IRoute } from '../../core';
53
+ import type {
54
+ INavigationEmitter,
55
+ INavigatorHandle,
56
+ INavigatorPlatform,
57
+ IRoute,
58
+ } from '../../core';
54
59
  import { collectRegistry } from '../collect-registry';
55
60
  import { NavigationContext } from '../navigation-context';
56
61
  import { Screen } from '../screen';
57
- import type { IReactScreenOptions, IScreenOptionsArgs, IScreenProps } from '../screen';
62
+ import type {
63
+ IReactScreenOptions,
64
+ IScreenOptionsArgs,
65
+ IScreenProps,
66
+ } from '../screen';
58
67
 
59
68
  export type { INavigatorHandle } from '../../core';
60
69
 
@@ -67,11 +76,15 @@ export type IStackProps = {
67
76
  // backTitleVisible defaults to `true` on both platforms per the codegen spec's own default
68
77
  // (CT.WithDefault<boolean, 'true'>) - no ios/android divergence in v1 scope, so a single constant
69
78
  // stands in for the per-platform injection point ISliderPlatform-style adapters use elsewhere.
70
- const NAVIGATOR_PLATFORM: INavigatorPlatform = { defaultHeaderBackTitleVisible: true };
79
+ const NAVIGATOR_PLATFORM: INavigatorPlatform = {
80
+ defaultHeaderBackTitleVisible: true,
81
+ };
71
82
 
72
83
  type IScreenRegistryEntry = Omit<IScreenProps, 'name'>;
73
84
 
74
- function isScreenElement(child: ReactNode): child is ReactElement<IScreenProps> {
85
+ function isScreenElement(
86
+ child: ReactNode,
87
+ ): child is ReactElement<IScreenProps> {
75
88
  return isValidElement(child) && child.type === Screen;
76
89
  }
77
90
 
@@ -80,258 +93,308 @@ function resolveScreenOptions(
80
93
  optionsArgs: IScreenOptionsArgs,
81
94
  screenOptions: IReactScreenOptions | undefined,
82
95
  ): IReactScreenOptions {
83
- const own = typeof entry.options === 'function' ? entry.options(optionsArgs) : entry.options;
96
+ const own =
97
+ typeof entry.options === 'function'
98
+ ? entry.options(optionsArgs)
99
+ : entry.options;
84
100
  return { ...screenOptions, ...own };
85
101
  }
86
102
 
87
- const StackImpl = forwardRef<INavigatorHandle, IStackProps>((props, forwardedRef) => {
88
- // Read BEFORE establishing this Stack's own Context value below - becomes the `parent` link a
89
- // nested screen's useNavigation().getParent() walks (e.g. this Stack rendered as a Tab screen's
90
- // content reaches that Tab via this value). undefined when this Stack is the nesting root.
91
- const ambientContext = useContext(NavigationContext);
92
- const registry = useMemo(
93
- () => collectRegistry(props.children, isScreenElement),
94
- [props.children],
95
- );
96
- const routeIdPrefix = useId();
97
- const routeSequence = useRef(0);
98
- // One emitter per route.key, keyed exactly like routeSequence's counter is scoped - created
99
- // lazily the first time a route is rendered, pruned once it's popped off the stack (below).
100
- const emitters = useRef(new Map<string, INavigationEmitter>()).current;
101
- // Investigation instrumentation (flicker-on-focus bug): route keys whose resolved
102
- // transition/animation-timing props have already been logged, so the once-per-mount dump below
103
- // doesn't spam on every re-render. Kept behind DEBUG, never removed.
104
- const loggedScreenPropKeys = useRef(new Set<string>()).current;
103
+ const StackImpl = forwardRef<INavigatorHandle, IStackProps>(
104
+ (props, forwardedRef) => {
105
+ // Read BEFORE establishing this Stack's own Context value below - becomes the `parent` link a
106
+ // nested screen's useNavigation().getParent() walks (e.g. this Stack rendered as a Tab screen's
107
+ // content reaches that Tab via this value). undefined when this Stack is the nesting root.
108
+ const ambientContext = useContext(NavigationContext);
109
+ const registry = useMemo(
110
+ () => collectRegistry(props.children, isScreenElement),
111
+ [props.children],
112
+ );
113
+ const routeIdPrefix = useId();
114
+ const routeSequence = useRef(0);
115
+ // One emitter per route.key, keyed exactly like routeSequence's counter is scoped - created
116
+ // lazily the first time a route is rendered, pruned once it's popped off the stack (below).
117
+ const emitters = useRef(new Map<string, INavigationEmitter>()).current;
118
+ // Investigation instrumentation (flicker-on-focus bug): route keys whose resolved
119
+ // transition/animation-timing props have already been logged, so the once-per-mount dump below
120
+ // doesn't spam on every re-render. Kept behind DEBUG, never removed.
121
+ const loggedScreenPropKeys = useRef(new Set<string>()).current;
105
122
 
106
- const emitterFor = useCallback(
107
- (routeKey: string): INavigationEmitter => {
108
- let emitter = emitters.get(routeKey);
109
- if (!emitter) {
110
- emitter = createNavigationEmitter();
111
- emitters.set(routeKey, emitter);
112
- }
113
- return emitter;
114
- },
115
- [emitters],
116
- );
123
+ const emitterFor = useCallback(
124
+ (routeKey: string): INavigationEmitter => {
125
+ let emitter = emitters.get(routeKey);
126
+ if (!emitter) {
127
+ emitter = createNavigationEmitter();
128
+ emitters.set(routeKey, emitter);
129
+ }
130
+ return emitter;
131
+ },
132
+ [emitters],
133
+ );
117
134
 
118
- const createRoute = useCallback(
119
- (name: string, params: unknown): IRoute<unknown> => {
120
- routeSequence.current += 1;
121
- return { key: `${routeIdPrefix}-${name}-${routeSequence.current}`, name, params };
122
- },
123
- [routeIdPrefix],
124
- );
135
+ const createRoute = useCallback(
136
+ (name: string, params: unknown): IRoute<unknown> => {
137
+ routeSequence.current += 1;
138
+ return {
139
+ key: `${routeIdPrefix}-${name}-${routeSequence.current}`,
140
+ name,
141
+ params,
142
+ };
143
+ },
144
+ [routeIdPrefix],
145
+ );
125
146
 
126
- const registeredNames = useMemo(() => [...registry.keys()], [registry]);
147
+ const registeredNames = useMemo(() => [...registry.keys()], [registry]);
127
148
 
128
- const [dispatchedState, dispatch] = useReducer(navigatorReducer, undefined, () => {
129
- const initialRouteName = props.initialRouteName ?? registry.keys().next().value;
130
- if (initialRouteName === undefined) {
131
- dlog('Stack: no <Stack.Screen> children registered');
132
- return createInitialNavigatorState({ key: routeIdPrefix, name: '', params: undefined });
133
- }
134
- return createInitialNavigatorState(
135
- createRoute(initialRouteName, registry.get(initialRouteName)?.initialParams),
149
+ const [dispatchedState, dispatch] = useReducer(
150
+ navigatorReducer,
151
+ undefined,
152
+ () => {
153
+ const initialRouteName =
154
+ props.initialRouteName ?? registry.keys().next().value;
155
+ if (initialRouteName === undefined) {
156
+ dlog('Stack: no <Stack.Screen> children registered');
157
+ return createInitialNavigatorState({
158
+ key: routeIdPrefix,
159
+ name: '',
160
+ params: undefined,
161
+ });
162
+ }
163
+ return createInitialNavigatorState(
164
+ createRoute(
165
+ initialRouteName,
166
+ registry.get(initialRouteName)?.initialParams,
167
+ ),
168
+ );
169
+ },
136
170
  );
137
- });
138
171
 
139
- // A <Stack.Screen> marker can unregister while its route is still in the pushed history, which
140
- // would leave that entry with nothing to render (reconcileStackRoutes' header). Reconciling here
141
- // rather than in an effect repairs the CURRENT paint - the call is pure and hands back the same
142
- // reference when nothing changed, so it is safe during render; the effect below only PERSISTS
143
- // the repair, so the next push builds on the pruned history instead of the phantom one.
144
- const state = reconcileStackRoutes(dispatchedState, registeredNames);
172
+ // A <Stack.Screen> marker can unregister while its route is still in the pushed history, which
173
+ // would leave that entry with nothing to render (reconcileStackRoutes' header). Reconciling here
174
+ // rather than in an effect repairs the CURRENT paint - the call is pure and hands back the same
175
+ // reference when nothing changed, so it is safe during render; the effect below only PERSISTS
176
+ // the repair, so the next push builds on the pruned history instead of the phantom one.
177
+ const state = reconcileStackRoutes(dispatchedState, registeredNames);
145
178
 
146
- useEffect(() => {
147
- if (state !== dispatchedState) dispatch({ type: 'reset', state });
148
- }, [state, dispatchedState]);
179
+ useEffect(() => {
180
+ if (state !== dispatchedState) dispatch({ type: 'reset', state });
181
+ }, [state, dispatchedState]);
149
182
 
150
- const handle = useMemo<INavigatorHandle>(
151
- () => ({
152
- push: (name, params) => dispatch({ type: 'push', route: createRoute(name, params) }),
153
- pop: count => dispatch({ type: 'pop', count }),
154
- popToTop: () => dispatch({ type: 'popToTop' }),
155
- popTo: key => dispatch({ type: 'popTo', key }),
156
- replace: (name, params) => dispatch({ type: 'replace', route: createRoute(name, params) }),
157
- setParams: (params, key) => dispatch({ type: 'setParams', key, params }),
158
- reset: nextState => dispatch({ type: 'reset', state: nextState }),
159
- canGoBack: () => state.routes.length > 1,
160
- }),
161
- [createRoute, state.routes.length],
162
- );
183
+ const handle = useMemo<INavigatorHandle>(
184
+ () => ({
185
+ push: (name, params) =>
186
+ dispatch({ type: 'push', route: createRoute(name, params) }),
187
+ pop: count => dispatch({ type: 'pop', count }),
188
+ popToTop: () => dispatch({ type: 'popToTop' }),
189
+ popTo: key => dispatch({ type: 'popTo', key }),
190
+ replace: (name, params) =>
191
+ dispatch({ type: 'replace', route: createRoute(name, params) }),
192
+ setParams: (params, key) =>
193
+ dispatch({ type: 'setParams', key, params }),
194
+ reset: nextState => dispatch({ type: 'reset', state: nextState }),
195
+ canGoBack: () => state.routes.length > 1,
196
+ }),
197
+ [createRoute, state.routes.length],
198
+ );
163
199
 
164
- useImperativeHandle(forwardedRef, () => handle, [handle]);
200
+ useImperativeHandle(forwardedRef, () => handle, [handle]);
165
201
 
166
- // Broadcasts the router state to every still-live route's emitter (useNavigationState's
167
- // source) after each commit, and prunes emitters for routes popped off the stack - mirroring
168
- // React's own subscribe-in-effect pattern rather than emitting mid-render, which would call a
169
- // descendant's setState while this component is still rendering.
170
- useEffect(() => {
171
- for (const route of state.routes) {
172
- emitterFor(route.key).emit(NAVIGATION_EVENT_STATE, state);
173
- }
174
- for (const routeKey of emitters.keys()) {
175
- if (!state.routes.some(route => route.key === routeKey)) emitters.delete(routeKey);
176
- }
177
- }, [state, emitterFor, emitters]);
202
+ // Broadcasts the router state to every still-live route's emitter (useNavigationState's
203
+ // source) after each commit, and prunes emitters for routes popped off the stack - mirroring
204
+ // React's own subscribe-in-effect pattern rather than emitting mid-render, which would call a
205
+ // descendant's setState while this component is still rendering.
206
+ useEffect(() => {
207
+ for (const route of state.routes) {
208
+ emitterFor(route.key).emit(NAVIGATION_EVENT_STATE, state);
209
+ }
210
+ for (const routeKey of emitters.keys()) {
211
+ if (!state.routes.some(route => route.key === routeKey))
212
+ emitters.delete(routeKey);
213
+ }
214
+ }, [state, emitterFor, emitters]);
178
215
 
179
- // Investigation instrumentation (flicker-on-focus bug): STACK_ON_FINISH_TRANSITIONING is the
180
- // native signal that the WHOLE push/pop animation has finished (as opposed to onAppear/
181
- // onDisappear, which are per-screen) - logging it lets the per-screen appear/disappear
182
- // timestamps above be checked against the actual transition-complete moment. Kept behind
183
- // DEBUG, never removed.
184
- const stackProps = resolveStackProps({
185
- passthrough: {
186
- [STACK_ON_FINISH_TRANSITIONING]: () =>
187
- dlog(`Stack: onFinishTransitioning at t=${Date.now()}`),
188
- },
189
- });
216
+ // Investigation instrumentation (flicker-on-focus bug): STACK_ON_FINISH_TRANSITIONING is the
217
+ // native signal that the WHOLE push/pop animation has finished (as opposed to onAppear/
218
+ // onDisappear, which are per-screen) - logging it lets the per-screen appear/disappear
219
+ // timestamps above be checked against the actual transition-complete moment. Kept behind
220
+ // DEBUG, never removed.
221
+ const stackProps = resolveStackProps({
222
+ passthrough: {
223
+ [STACK_ON_FINISH_TRANSITIONING]: () =>
224
+ dlog(`Stack: onFinishTransitioning at t=${Date.now()}`),
225
+ },
226
+ });
190
227
 
191
- const children = state.routes.map((route, index) => {
192
- const entry = registry.get(route.name);
193
- if (!entry) {
194
- dlog(`Stack: no screen registered for route name "${route.name}"`);
195
- return null;
196
- }
228
+ const children = state.routes.map((route, index) => {
229
+ const entry = registry.get(route.name);
230
+ if (!entry) {
231
+ dlog(`Stack: no screen registered for route name "${route.name}"`);
232
+ return null;
233
+ }
197
234
 
198
- const screenOptionsArgs: IScreenOptionsArgs = { route, navigation: handle };
199
- const mergedOptions = resolveScreenOptions(entry, screenOptionsArgs, props.screenOptions);
200
- const activityState = computeActivityState(index, state.routes.length);
201
- // Investigation instrumentation (flicker-on-focus bug): fires on EVERY Stack render, not just
202
- // on transitions, so the log stream shows whether a route's activityState/index ever changes
203
- // outside of a push/pop dispatch (e.g. from an unrelated re-render racing the native
204
- // transition). Kept behind DEBUG per <keep_logs_gate_behind_DEBUG> - never remove.
205
- dlog(
206
- `Stack: render route "${route.name}" index=${index}/${state.routes.length - 1} ` +
207
- `activityState=${activityState} at t=${Date.now()}`,
208
- );
235
+ const screenOptionsArgs: IScreenOptionsArgs = {
236
+ route,
237
+ navigation: handle,
238
+ };
239
+ const mergedOptions = resolveScreenOptions(
240
+ entry,
241
+ screenOptionsArgs,
242
+ props.screenOptions,
243
+ );
244
+ const activityState = computeActivityState(index, state.routes.length);
245
+ // Investigation instrumentation (flicker-on-focus bug): fires on EVERY Stack render, not just
246
+ // on transitions, so the log stream shows whether a route's activityState/index ever changes
247
+ // outside of a push/pop dispatch (e.g. from an unrelated re-render racing the native
248
+ // transition). Kept behind DEBUG per <keep_logs_gate_behind_DEBUG> - never remove.
249
+ dlog(
250
+ `Stack: render route "${route.name}" index=${index}/${state.routes.length - 1} ` +
251
+ `activityState=${activityState} at t=${Date.now()}`,
252
+ );
209
253
 
210
- const routeEmitter = emitterFor(route.key);
254
+ const routeEmitter = emitterFor(route.key);
211
255
 
212
- const searchBarOptions = mergedOptions.headerSearchBarOptions;
213
- const plan = resolveScreenRenderPlan({
214
- screenId: route.key,
215
- index,
216
- routeCount: state.routes.length,
217
- options: mergedOptions,
218
- platform: NAVIGATOR_PLATFORM,
219
- isAndroid: Platform.OS === 'android',
220
- screenPassthrough: {
221
- [SCREEN_ON_DISMISSED]: () => dispatch({ type: 'pop', count: 1 }),
222
- [SCREEN_ON_HEADER_BACK_BUTTON_CLICKED]: () => dispatch({ type: 'pop', count: 1 }),
223
- // onAppear/onDisappear are the definitive visibility boundary (post-transition-animation),
224
- // so 'focus'/'blur' fire exactly once per transition; onWillAppear/onWillDisappear fire
225
- // BEFORE the animation runs, so wiring them to emit() too would double-invoke
226
- // useFocusEffect per transition - they only get a debug log here.
227
- [SCREEN_ON_WILL_APPEAR]: () =>
228
- dlog(`Stack: route "${route.name}" will appear at t=${Date.now()}`),
229
- [SCREEN_ON_APPEAR]: () => {
230
- dlog(`Stack: route "${route.name}" appeared (focus) at t=${Date.now()}`);
231
- routeEmitter.emit(NAVIGATION_EVENT_FOCUS);
232
- },
233
- [SCREEN_ON_WILL_DISAPPEAR]: () =>
234
- dlog(`Stack: route "${route.name}" will disappear at t=${Date.now()}`),
235
- [SCREEN_ON_DISAPPEAR]: () => {
236
- dlog(`Stack: route "${route.name}" disappeared (blur) at t=${Date.now()}`);
237
- routeEmitter.emit(NAVIGATION_EVENT_BLUR);
238
- },
239
- },
240
- searchBarPassthrough: searchBarOptions
241
- ? {
242
- ...buildSearchBarPassthrough(searchBarOptions, message =>
243
- dlog(`Stack: route "${route.name}" ${message}`),
256
+ const searchBarOptions = mergedOptions.headerSearchBarOptions;
257
+ const plan = resolveScreenRenderPlan({
258
+ screenId: route.key,
259
+ index,
260
+ routeCount: state.routes.length,
261
+ options: mergedOptions,
262
+ platform: NAVIGATOR_PLATFORM,
263
+ isAndroid: Platform.OS === 'android',
264
+ screenPassthrough: {
265
+ [SCREEN_ON_DISMISSED]: () => dispatch({ type: 'pop', count: 1 }),
266
+ [SCREEN_ON_HEADER_BACK_BUTTON_CLICKED]: () =>
267
+ dispatch({ type: 'pop', count: 1 }),
268
+ // onAppear/onDisappear are the definitive visibility boundary (post-transition-animation),
269
+ // so 'focus'/'blur' fire exactly once per transition; onWillAppear/onWillDisappear fire
270
+ // BEFORE the animation runs, so wiring them to emit() too would double-invoke
271
+ // useFocusEffect per transition - they only get a debug log here.
272
+ [SCREEN_ON_WILL_APPEAR]: () =>
273
+ dlog(`Stack: route "${route.name}" will appear at t=${Date.now()}`),
274
+ [SCREEN_ON_APPEAR]: () => {
275
+ dlog(
276
+ `Stack: route "${route.name}" appeared (focus) at t=${Date.now()}`,
277
+ );
278
+ routeEmitter.emit(NAVIGATION_EVENT_FOCUS);
279
+ },
280
+ [SCREEN_ON_WILL_DISAPPEAR]: () =>
281
+ dlog(
282
+ `Stack: route "${route.name}" will disappear at t=${Date.now()}`,
244
283
  ),
245
- // The imperative ref (SearchBarCommands): a callback ref attached straight to the
246
- // RNSSearchBar host element via `passthrough.ref` - createElement extracts `ref` from
247
- // a props object same as ScrollView/TextInput's own ref binding (see
248
- // descriptor-to-react's comment), so the host instance IS the ISymbioteNode. Built
249
- // fresh per mount/unmount, never captured eagerly (buildSearchBarHandle's own
250
- // lazy-getter contract is satisfied trivially here since `node` is already resolved).
251
- ref: (node: ISymbioteNode | null) => {
252
- // Investigation instrumentation (search-bar-ref "node not committed" bug): compare
253
- // this debugNodeId against the mirror.set/dispatchViewCommand logs in commit.ts -
254
- // same id on both sides proves the ref really does hold the committed node; a
255
- // mismatch proves a stale closure instead. Kept behind DEBUG, never removed.
256
- dlog(
257
- `Stack: search bar ref callback, node=${node === null ? 'null' : debugNodeId(node)} at t=${Date.now()}`,
258
- );
259
- const appRef = searchBarOptions.ref;
260
- if (!appRef) return;
261
- appRef.current = node === null ? null : buildSearchBarHandle(() => node);
284
+ [SCREEN_ON_DISAPPEAR]: () => {
285
+ dlog(
286
+ `Stack: route "${route.name}" disappeared (blur) at t=${Date.now()}`,
287
+ );
288
+ routeEmitter.emit(NAVIGATION_EVENT_BLUR);
289
+ },
290
+ },
291
+ searchBarPassthrough: searchBarOptions
292
+ ? {
293
+ ...buildSearchBarPassthrough(searchBarOptions, message =>
294
+ dlog(`Stack: route "${route.name}" ${message}`),
295
+ ),
296
+ // The imperative ref (SearchBarCommands): a callback ref attached straight to the
297
+ // RNSSearchBar host element via `passthrough.ref` - createElement extracts `ref` from
298
+ // a props object same as ScrollView/TextInput's own ref binding (see
299
+ // descriptor-to-react's comment), so the host instance IS the ISymbioteNode. Built
300
+ // fresh per mount/unmount, never captured eagerly (buildSearchBarHandle's own
301
+ // lazy-getter contract is satisfied trivially here since `node` is already resolved).
302
+ ref: (node: ISymbioteNode | null) => {
303
+ // Investigation instrumentation (search-bar-ref "node not committed" bug): compare
304
+ // this debugNodeId against the mirror.set/dispatchViewCommand logs in commit.ts -
305
+ // same id on both sides proves the ref really does hold the committed node; a
306
+ // mismatch proves a stale closure instead. Kept behind DEBUG, never removed.
307
+ dlog(
308
+ `Stack: search bar ref callback, node=${node === null ? 'null' : debugNodeId(node)} at t=${Date.now()}`,
309
+ );
310
+ const appRef = searchBarOptions.ref;
311
+ if (!appRef) return;
312
+ appRef.current =
313
+ node === null ? null : buildSearchBarHandle(() => node);
314
+ },
315
+ }
316
+ : undefined,
317
+ });
318
+ // Investigation instrumentation (flicker-on-focus bug): the actual timing/z-order-relevant
319
+ // values resolved onto the native RNSScreen, once per route.key (not every render) - rules a
320
+ // stackAnimation/transitionDuration mismatch against react-native-screens' own native default
321
+ // in or out. Kept behind DEBUG, never removed.
322
+ if (!loggedScreenPropKeys.has(route.key)) {
323
+ loggedScreenPropKeys.add(route.key);
324
+ dlog(
325
+ `Stack: route "${route.name}" resolved screen props ` +
326
+ `stackAnimation=${String(plan.screenProps.stackAnimation)} ` +
327
+ `stackPresentation=${String(plan.screenProps.stackPresentation)} ` +
328
+ `transitionDuration=${String(plan.screenProps.transitionDuration)} ` +
329
+ `gestureEnabled=${String(plan.screenProps.gestureEnabled)} at t=${Date.now()}`,
330
+ );
331
+ }
332
+
333
+ // Must not be flattened away (collapsable: false) - react-native-screens' native side
334
+ // finds THIS specific view type by class check to register a formSheet's content for
335
+ // sizing (see RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME's comment in core/constants.ts). A
336
+ // `push` screen doesn't need it, but a `formSheet` one is otherwise left with no content
337
+ // ever attached natively.
338
+ const content = createElement(
339
+ RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME,
340
+ plan.contentWrapperProps,
341
+ createElement(
342
+ NavigationContext.Provider,
343
+ {
344
+ value: {
345
+ route,
346
+ navigation: handle,
347
+ emitter: routeEmitter,
348
+ parent: ambientContext,
262
349
  },
263
- }
264
- : undefined,
265
- });
266
- // Investigation instrumentation (flicker-on-focus bug): the actual timing/z-order-relevant
267
- // values resolved onto the native RNSScreen, once per route.key (not every render) - rules a
268
- // stackAnimation/transitionDuration mismatch against react-native-screens' own native default
269
- // in or out. Kept behind DEBUG, never removed.
270
- if (!loggedScreenPropKeys.has(route.key)) {
271
- loggedScreenPropKeys.add(route.key);
272
- dlog(
273
- `Stack: route "${route.name}" resolved screen props ` +
274
- `stackAnimation=${String(plan.screenProps.stackAnimation)} ` +
275
- `stackPresentation=${String(plan.screenProps.stackPresentation)} ` +
276
- `transitionDuration=${String(plan.screenProps.transitionDuration)} ` +
277
- `gestureEnabled=${String(plan.screenProps.gestureEnabled)} at t=${Date.now()}`,
350
+ },
351
+ createElement(entry.component),
352
+ ),
278
353
  );
279
- }
280
-
281
- // Must not be flattened away (collapsable: false) - react-native-screens' native side
282
- // finds THIS specific view type by class check to register a formSheet's content for
283
- // sizing (see RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME's comment in core/constants.ts). A
284
- // `push` screen doesn't need it, but a `formSheet` one is otherwise left with no content
285
- // ever attached natively.
286
- const content = createElement(
287
- RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME,
288
- plan.contentWrapperProps,
289
- createElement(
290
- NavigationContext.Provider,
291
- { value: { route, navigation: handle, emitter: routeEmitter, parent: ambientContext } },
292
- createElement(entry.component),
293
- ),
294
- );
295
354
 
296
- // react-native-screens' own Screen.tsx swaps in a DIFFERENT Fabric component
297
- // ('RNSModalScreen') for a modally-presented screen - its native updateLayoutMetrics: relies on
298
- // this exact class to know it must NOT apply Yoga's computed frame (see resolveScreenViewName's
299
- // comment in core/render-stack.ts). The nested Screen isHeaderInModal adds below is always
300
- // plain 'RNSScreen': react-native-screens' own inner Screen never carries a presentation prop
301
- // either, since it exists purely to host the header, not to be modally presented itself.
355
+ // react-native-screens' own Screen.tsx swaps in a DIFFERENT Fabric component
356
+ // ('RNSModalScreen') for a modally-presented screen - its native updateLayoutMetrics: relies on
357
+ // this exact class to know it must NOT apply Yoga's computed frame (see resolveScreenViewName's
358
+ // comment in core/render-stack.ts). The nested Screen isHeaderInModal adds below is always
359
+ // plain 'RNSScreen': react-native-screens' own inner Screen never carries a presentation prop
360
+ // either, since it exists purely to host the header, not to be modally presented itself.
302
361
 
303
- // A modal/formSheet screen has no UINavigationController of its own on iOS - nest an inner
304
- // RNSScreenStack/RNSScreen purely to host the native header bar (see isHeaderInModal's
305
- // comment in core/render-stack.ts). Skipping this leaves RNSScreenStackHeaderConfig with no
306
- // navigation controller to attach to, so the header silently never renders.
307
- return plan.inModal
308
- ? createElement(
309
- plan.screenViewName,
310
- { key: route.key, ...plan.screenProps },
311
- createElement(
312
- RNS_SCREEN_STACK_VIEW_NAME,
313
- { style: plan.innerStackStyle },
362
+ // A modal/formSheet screen has no UINavigationController of its own on iOS - nest an inner
363
+ // RNSScreenStack/RNSScreen purely to host the native header bar (see isHeaderInModal's
364
+ // comment in core/render-stack.ts). Skipping this leaves RNSScreenStackHeaderConfig with no
365
+ // navigation controller to attach to, so the header silently never renders.
366
+ return plan.inModal
367
+ ? createElement(
368
+ plan.screenViewName,
369
+ { key: route.key, ...plan.screenProps },
314
370
  createElement(
315
- RNS_SCREEN_VIEW_NAME,
316
- // activityState mirrors the outer Screen's own value - react-native-screens'
317
- // RNSScreen.mm treats an unset/inactive nested screen as not yet pushed, leaving it
318
- // parked at its pre-push transition position (off past the bottom edge) instead of
319
- // its real, presented frame.
320
- { style: plan.innerScreenStyle, activityState: plan.activityState },
321
- descriptorToReact(plan.headerConfig),
322
- content,
371
+ RNS_SCREEN_STACK_VIEW_NAME,
372
+ { style: plan.innerStackStyle },
373
+ createElement(
374
+ RNS_SCREEN_VIEW_NAME,
375
+ // activityState mirrors the outer Screen's own value - react-native-screens'
376
+ // RNSScreen.mm treats an unset/inactive nested screen as not yet pushed, leaving it
377
+ // parked at its pre-push transition position (off past the bottom edge) instead of
378
+ // its real, presented frame.
379
+ {
380
+ style: plan.innerScreenStyle,
381
+ activityState: plan.activityState,
382
+ },
383
+ descriptorToReact(plan.headerConfig),
384
+ content,
385
+ ),
323
386
  ),
324
- ),
325
- )
326
- : createElement(
327
- plan.screenViewName,
328
- { key: route.key, ...plan.screenProps },
329
- descriptorToReact(plan.headerConfig),
330
- content,
331
- );
332
- });
387
+ )
388
+ : createElement(
389
+ plan.screenViewName,
390
+ { key: route.key, ...plan.screenProps },
391
+ descriptorToReact(plan.headerConfig),
392
+ content,
393
+ );
394
+ });
333
395
 
334
- return createElement(RNS_SCREEN_STACK_VIEW_NAME, stackProps, ...children);
335
- });
396
+ return createElement(RNS_SCREEN_STACK_VIEW_NAME, stackProps, ...children);
397
+ },
398
+ );
336
399
 
337
400
  export const Stack = Object.assign(StackImpl, { Screen });
@@ -16,7 +16,9 @@ export type ITabScreenOptionsArgs = {
16
16
  navigation: ITabNavigatorHandle;
17
17
  };
18
18
 
19
- export type ITabScreenOptionsResolver = (args: ITabScreenOptionsArgs) => ITabOptions;
19
+ export type ITabScreenOptionsResolver = (
20
+ args: ITabScreenOptionsArgs,
21
+ ) => ITabOptions;
20
22
 
21
23
  export type ITabScreenProps = {
22
24
  name: string;