@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.
Files changed (237) hide show
  1. package/README.md +89 -14
  2. package/build/angular/drawer/index.js +23 -9
  3. package/build/angular/index.d.ts +4 -4
  4. package/build/angular/index.js +1 -1
  5. package/build/angular/injectors/inject-navigation.js +1 -1
  6. package/build/angular/navigation-scope.directive.js +1 -1
  7. package/build/angular/search-bar-ref.directive.js +1 -1
  8. package/build/angular/stack/index.js +20 -6
  9. package/build/angular/tabs/index.js +21 -7
  10. package/build/core/drawer-options/index.js +19 -5
  11. package/build/core/drawer-router-state/index.js +8 -2
  12. package/build/core/guards.js +3 -1
  13. package/build/core/header-bar-buttons.js +14 -4
  14. package/build/core/index.d.ts +4 -4
  15. package/build/core/index.js +2 -2
  16. package/build/core/navigator-state/index.js +12 -4
  17. package/build/core/render-drawer.js +4 -1
  18. package/build/core/render-stack.js +13 -4
  19. package/build/core/screen-options.js +1 -1
  20. package/build/core/search-bar-commands.js +1 -1
  21. package/build/core/state-persistence/index.js +6 -2
  22. package/build/core/tab-router-state/index.js +8 -2
  23. package/build/react/drawer/index.js +36 -13
  24. package/build/react/index.d.ts +7 -7
  25. package/build/react/index.js +3 -3
  26. package/build/react/stack/index.js +34 -8
  27. package/build/react/tabs/index.js +14 -4
  28. package/build/solid/drawer/index.d.ts +22 -0
  29. package/build/solid/drawer/index.js +394 -0
  30. package/build/solid/drawer-screen-props.d.ts +13 -0
  31. package/build/solid/drawer-screen-props.js +2 -0
  32. package/build/solid/host.d.ts +2 -0
  33. package/build/solid/host.js +23 -0
  34. package/build/solid/index.d.ts +19 -0
  35. package/build/solid/index.js +24 -0
  36. package/build/solid/linking.d.ts +3 -0
  37. package/build/solid/linking.js +55 -0
  38. package/build/solid/navigation-context.d.ts +13 -0
  39. package/build/solid/navigation-context.js +37 -0
  40. package/build/solid/primitives/create-focus-effect.d.ts +1 -0
  41. package/build/solid/primitives/create-focus-effect.js +30 -0
  42. package/build/solid/primitives/create-is-focused.d.ts +2 -0
  43. package/build/solid/primitives/create-is-focused.js +31 -0
  44. package/build/solid/primitives/create-navigation-state.d.ts +3 -0
  45. package/build/solid/primitives/create-navigation-state.js +30 -0
  46. package/build/solid/primitives/index.d.ts +12 -0
  47. package/build/solid/primitives/index.js +8 -0
  48. package/build/solid/primitives/use-drawer-navigation.d.ts +5 -0
  49. package/build/solid/primitives/use-drawer-navigation.js +15 -0
  50. package/build/solid/primitives/use-navigation.d.ts +8 -0
  51. package/build/solid/primitives/use-navigation.js +23 -0
  52. package/build/solid/primitives/use-route.d.ts +3 -0
  53. package/build/solid/primitives/use-route.js +14 -0
  54. package/build/solid/primitives/use-stack-navigation.d.ts +5 -0
  55. package/build/solid/primitives/use-stack-navigation.js +19 -0
  56. package/build/solid/primitives/use-tab-navigation.d.ts +5 -0
  57. package/build/solid/primitives/use-tab-navigation.js +15 -0
  58. package/build/solid/screen-props.d.ts +19 -0
  59. package/build/solid/screen-props.js +9 -0
  60. package/build/solid/screen-registry.d.ts +31 -0
  61. package/build/solid/screen-registry.js +86 -0
  62. package/build/solid/screen.d.ts +6 -0
  63. package/build/solid/screen.js +69 -0
  64. package/build/solid/stack/index.d.ts +15 -0
  65. package/build/solid/stack/index.js +348 -0
  66. package/build/solid/tab-screen-props.d.ts +13 -0
  67. package/build/solid/tab-screen-props.js +3 -0
  68. package/build/solid/tabs/index.d.ts +14 -0
  69. package/build/solid/tabs/index.js +228 -0
  70. package/build/svelte/descriptor-subtree.js +3 -1
  71. package/build/svelte/drawer/index.svelte +163 -25
  72. package/build/svelte/drawer-screen.svelte +2 -1
  73. package/build/svelte/index.d.ts +3 -3
  74. package/build/svelte/index.js +1 -1
  75. package/build/svelte/navigation-scope.svelte +4 -1
  76. package/build/svelte/runes/use-navigation.svelte.js +5 -1
  77. package/build/svelte/stack/index.svelte +43 -6
  78. package/build/svelte/stack/stack-screen.svelte +66 -9
  79. package/build/svelte/tabs/index.svelte +42 -20
  80. package/build/vue/composables/use-navigation.js +5 -1
  81. package/build/vue/drawer/index.js +30 -8
  82. package/build/vue/index.d.ts +3 -3
  83. package/build/vue/index.js +1 -1
  84. package/build/vue/navigation-context.js +1 -1
  85. package/build/vue/stack/index.js +17 -6
  86. package/build/vue/tabs/index.js +18 -5
  87. package/build-ngc/angular/drawer/index.js +31 -11
  88. package/build-ngc/angular/drawer/index.js.map +1 -1
  89. package/build-ngc/angular/index.d.ts +4 -4
  90. package/build-ngc/angular/index.js.map +1 -1
  91. package/build-ngc/angular/injectors/inject-focus-effect.js.map +1 -1
  92. package/build-ngc/angular/injectors/inject-is-focused.js.map +1 -1
  93. package/build-ngc/angular/injectors/inject-navigation-state.js.map +1 -1
  94. package/build-ngc/angular/injectors/inject-navigation.js +1 -1
  95. package/build-ngc/angular/injectors/inject-navigation.js.map +1 -1
  96. package/build-ngc/angular/linking.js.map +1 -1
  97. package/build-ngc/angular/navigation-context.service.js.map +1 -1
  98. package/build-ngc/angular/navigation-scope.directive.js +1 -1
  99. package/build-ngc/angular/navigation-scope.directive.js.map +1 -1
  100. package/build-ngc/angular/screen.directive.js.map +1 -1
  101. package/build-ngc/angular/search-bar-ref.directive.js +1 -1
  102. package/build-ngc/angular/search-bar-ref.directive.js.map +1 -1
  103. package/build-ngc/angular/stack/index.js +26 -8
  104. package/build-ngc/angular/stack/index.js.map +1 -1
  105. package/build-ngc/angular/tab-screen.directive.js.map +1 -1
  106. package/build-ngc/angular/tabs/index.js +21 -7
  107. package/build-ngc/angular/tabs/index.js.map +1 -1
  108. package/build-ngc/core/constants.js.map +1 -1
  109. package/build-ngc/core/drawer-options/index.js +19 -5
  110. package/build-ngc/core/drawer-options/index.js.map +1 -1
  111. package/build-ngc/core/drawer-router-state/index.js +8 -2
  112. package/build-ngc/core/drawer-router-state/index.js.map +1 -1
  113. package/build-ngc/core/guards.js +3 -1
  114. package/build-ngc/core/guards.js.map +1 -1
  115. package/build-ngc/core/header-bar-buttons.js +14 -4
  116. package/build-ngc/core/header-bar-buttons.js.map +1 -1
  117. package/build-ngc/core/index.d.ts +4 -4
  118. package/build-ngc/core/index.js +2 -2
  119. package/build-ngc/core/index.js.map +1 -1
  120. package/build-ngc/core/linking-config/index.js.map +1 -1
  121. package/build-ngc/core/navigation-events/index.js.map +1 -1
  122. package/build-ngc/core/navigator-handles/index.js.map +1 -1
  123. package/build-ngc/core/navigator-state/index.js +12 -4
  124. package/build-ngc/core/navigator-state/index.js.map +1 -1
  125. package/build-ngc/core/render-drawer.js +4 -1
  126. package/build-ngc/core/render-drawer.js.map +1 -1
  127. package/build-ngc/core/render-stack.js +13 -4
  128. package/build-ngc/core/render-stack.js.map +1 -1
  129. package/build-ngc/core/render-tabs.js.map +1 -1
  130. package/build-ngc/core/screen-options.js +1 -1
  131. package/build-ngc/core/screen-options.js.map +1 -1
  132. package/build-ngc/core/search-bar-commands.js +1 -1
  133. package/build-ngc/core/search-bar-commands.js.map +1 -1
  134. package/build-ngc/core/stack-router-state/index.js.map +1 -1
  135. package/build-ngc/core/state-persistence/index.js +6 -2
  136. package/build-ngc/core/state-persistence/index.js.map +1 -1
  137. package/build-ngc/core/tab-router-state/index.js +8 -2
  138. package/build-ngc/core/tab-router-state/index.js.map +1 -1
  139. package/package.json +40 -15
  140. package/src/angular/drawer/index.ts +69 -25
  141. package/src/angular/index.ts +20 -4
  142. package/src/angular/injectors/inject-focus-effect.ts +8 -2
  143. package/src/angular/injectors/inject-is-focused.ts +6 -4
  144. package/src/angular/injectors/inject-navigation-state.ts +7 -4
  145. package/src/angular/injectors/inject-navigation.ts +12 -3
  146. package/src/angular/linking.ts +4 -1
  147. package/src/angular/navigation-context.service.ts +5 -2
  148. package/src/angular/navigation-scope.directive.ts +6 -2
  149. package/src/angular/screen.directive.ts +13 -3
  150. package/src/angular/search-bar-ref.directive.ts +10 -2
  151. package/src/angular/stack/index.ts +68 -22
  152. package/src/angular/tab-screen.directive.ts +3 -1
  153. package/src/angular/tabs/index.ts +36 -12
  154. package/src/core/constants.ts +6 -3
  155. package/src/core/drawer-options/index.ts +40 -13
  156. package/src/core/drawer-router-state/index.ts +11 -3
  157. package/src/core/guards.ts +3 -1
  158. package/src/core/header-bar-buttons.ts +41 -12
  159. package/src/core/index.ts +18 -4
  160. package/src/core/linking-config/index.ts +31 -10
  161. package/src/core/navigation-events/index.ts +8 -2
  162. package/src/core/navigator-handles/index.ts +8 -3
  163. package/src/core/navigator-props.ts +13 -4
  164. package/src/core/navigator-state/index.ts +24 -7
  165. package/src/core/render-drawer.ts +24 -9
  166. package/src/core/render-stack.ts +52 -13
  167. package/src/core/render-tabs.ts +24 -6
  168. package/src/core/screen-options.ts +15 -5
  169. package/src/core/search-bar-commands.ts +12 -3
  170. package/src/core/stack-router-state/index.ts +5 -2
  171. package/src/core/state-persistence/index.ts +13 -3
  172. package/src/core/tab-router-state/index.ts +10 -3
  173. package/src/react/drawer/index.ts +408 -344
  174. package/src/react/drawer-screen.ts +8 -2
  175. package/src/react/hooks/use-focus-effect.ts +8 -2
  176. package/src/react/hooks/use-is-focused.ts +6 -2
  177. package/src/react/hooks/use-navigation.ts +8 -2
  178. package/src/react/index.ts +37 -7
  179. package/src/react/linking.ts +4 -1
  180. package/src/react/navigation-context.ts +6 -2
  181. package/src/react/screen.ts +12 -3
  182. package/src/react/stack/index.ts +294 -231
  183. package/src/react/tab-screen.ts +3 -1
  184. package/src/react/tabs/index.ts +188 -158
  185. package/src/solid/drawer/index.ts +570 -0
  186. package/src/solid/drawer-screen-props.ts +24 -0
  187. package/src/solid/host.ts +29 -0
  188. package/src/solid/index.ts +87 -0
  189. package/src/solid/linking.ts +74 -0
  190. package/src/solid/navigation-context.ts +56 -0
  191. package/src/solid/primitives/create-focus-effect.ts +41 -0
  192. package/src/solid/primitives/create-is-focused.ts +41 -0
  193. package/src/solid/primitives/create-navigation-state.ts +44 -0
  194. package/src/solid/primitives/index.ts +12 -0
  195. package/src/solid/primitives/use-drawer-navigation.ts +25 -0
  196. package/src/solid/primitives/use-navigation.ts +44 -0
  197. package/src/solid/primitives/use-route.ts +18 -0
  198. package/src/solid/primitives/use-stack-navigation.ts +29 -0
  199. package/src/solid/primitives/use-tab-navigation.ts +25 -0
  200. package/src/solid/screen-props.ts +51 -0
  201. package/src/solid/screen-registry.ts +159 -0
  202. package/src/solid/screen.ts +80 -0
  203. package/src/solid/stack/index.ts +528 -0
  204. package/src/solid/tab-screen-props.ts +21 -0
  205. package/src/solid/tabs/index.ts +327 -0
  206. package/src/svelte/attachments.ts +3 -1
  207. package/src/svelte/descriptor-subtree.ts +3 -1
  208. package/src/svelte/drawer/index.svelte +163 -25
  209. package/src/svelte/drawer-screen-props.ts +8 -2
  210. package/src/svelte/drawer-screen.svelte +2 -1
  211. package/src/svelte/fabric-tree.test-helper.ts +37 -3
  212. package/src/svelte/index.ts +15 -3
  213. package/src/svelte/linking.svelte.ts +11 -5
  214. package/src/svelte/navigation-scope.svelte +4 -1
  215. package/src/svelte/runes/use-drawer-navigation.svelte.ts +3 -1
  216. package/src/svelte/runes/use-focus-effect.svelte.ts +8 -2
  217. package/src/svelte/runes/use-navigation-state.svelte.ts +3 -1
  218. package/src/svelte/runes/use-navigation.svelte.ts +13 -3
  219. package/src/svelte/runes/use-stack-navigation.svelte.ts +3 -1
  220. package/src/svelte/screen-props.ts +7 -2
  221. package/src/svelte/screen-registry.ts +9 -3
  222. package/src/svelte/stack/index.svelte +43 -6
  223. package/src/svelte/stack/stack-screen.svelte +66 -9
  224. package/src/svelte/svelte-compile.test-helper.ts +16 -12
  225. package/src/svelte/tab-screen-props.ts +3 -1
  226. package/src/svelte/tabs/index.svelte +42 -20
  227. package/src/vue/composables/use-navigation-state.ts +7 -4
  228. package/src/vue/composables/use-navigation.ts +13 -3
  229. package/src/vue/drawer/index.ts +122 -36
  230. package/src/vue/drawer-screen.ts +12 -5
  231. package/src/vue/index.ts +15 -3
  232. package/src/vue/linking.ts +7 -2
  233. package/src/vue/navigation-context.ts +17 -5
  234. package/src/vue/screen.ts +7 -2
  235. package/src/vue/stack/index.ts +82 -34
  236. package/src/vue/tab-screen.ts +3 -1
  237. package/src/vue/tabs/index.ts +51 -13
@@ -55,7 +55,9 @@ export type ITabProps = {
55
55
 
56
56
  type ITabRegistryEntry = Omit<ITabScreenProps, 'name'>;
57
57
 
58
- function isTabScreenElement(child: ReactNode): child is ReactElement<ITabScreenProps> {
58
+ function isTabScreenElement(
59
+ child: ReactNode,
60
+ ): child is ReactElement<ITabScreenProps> {
59
61
  return isValidElement(child) && child.type === TabScreen;
60
62
  }
61
63
 
@@ -64,174 +66,202 @@ function resolveTabOptions(
64
66
  optionsArgs: ITabScreenOptionsArgs,
65
67
  screenOptions: ITabOptions | undefined,
66
68
  ): ITabOptions {
67
- const own = typeof entry.options === 'function' ? entry.options(optionsArgs) : entry.options;
69
+ const own =
70
+ typeof entry.options === 'function'
71
+ ? entry.options(optionsArgs)
72
+ : entry.options;
68
73
  return { ...screenOptions, ...own };
69
74
  }
70
75
 
71
76
  const TAB_CONTENT_STYLE = { flex: 1 };
72
77
  const TAB_ROOT_STYLE = { flex: 1 };
73
78
 
74
- const TabImpl = forwardRef<ITabNavigatorHandle, ITabProps>((props, forwardedRef) => {
75
- // Read BEFORE establishing this Tab's own Context value below - becomes the `parent` link a
76
- // nested screen's useNavigation().getParent() walks (e.g. this Tab rendered as a Stack screen's
77
- // content reaches that Stack via this value). undefined when this Tab is the nesting root.
78
- const ambientContext = useContext(NavigationContext);
79
- const registry = useMemo(
80
- () => collectRegistry(props.children, isTabScreenElement),
81
- [props.children],
82
- );
83
- const routeIdPrefix = useId();
84
-
85
- const routes = useMemo<IRoute<unknown>[]>(
86
- () =>
87
- Array.from(registry.entries()).map(([name, entry]) => ({
88
- key: `${routeIdPrefix}-${name}`,
89
- name,
90
- params: entry.initialParams,
91
- })),
92
- [registry, routeIdPrefix],
93
- );
94
-
95
- // A <Tab.Screen> can appear or disappear after mount (a marker behind a conditional, a
96
- // data-driven screen list), so the route list is re-derived from `children` on every render and
97
- // the STORED state is reconciled against it (reconcileTabRoutes, core - it preserves each
98
- // surviving route's key and accumulated params and keeps focus on the same route NAME) rather
99
- // than staying frozen at whatever the first render saw. `null` means no marker had registered
100
- // yet: seeding an empty list would resolve initialRouteName against nothing and lose it, so the
101
- // state stays derived until either a marker arrives or something dispatches.
102
- const [storedState, setStoredState] = useState<ITabRouterState | null>(() =>
103
- routes.length === 0 ? null : createInitialTabState(routes, props.initialRouteName),
104
- );
105
-
106
- const resolveState = useCallback(
107
- (stored: ITabRouterState | null): ITabRouterState =>
108
- stored === null
109
- ? createInitialTabState(routes, props.initialRouteName)
110
- : reconcileTabRoutes(stored, routes),
111
- [routes, props.initialRouteName],
112
- );
113
-
114
- const state = resolveState(storedState);
115
-
116
- if (routes.length === 0) dlog('Tab: no <Tab.Screen> children registered');
117
-
118
- // The updater re-resolves rather than closing over `state` so that two dispatches batched into
119
- // one render still compose: the second reduces over the first's result, not a stale snapshot.
120
- const dispatch = useCallback(
121
- (action: ITabRouterAction) =>
122
- setStoredState(current => tabRouterReducer(resolveState(current), action)),
123
- [resolveState],
124
- );
125
-
126
- const jumpTo = useCallback(
127
- (name: string, params?: unknown) => dispatch({ type: 'jumpTo', name, params }),
128
- [dispatch],
129
- );
130
- const setParams = useCallback(
131
- (params: unknown, key: string) => dispatch({ type: 'setParams', key, params }),
132
- [dispatch],
133
- );
134
-
135
- const handle = useMemo<ITabNavigatorHandle>(() => ({ jumpTo, setParams }), [jumpTo, setParams]);
136
-
137
- useImperativeHandle(forwardedRef, () => handle, [handle]);
138
-
139
- const focusedRoute: IRoute<unknown> | undefined = state.routes[state.index];
140
-
141
- const items: ITabBarItemView[] = state.routes.map((route, index) => {
142
- const entry = registry.get(route.name);
143
- const focused = isFocusedRoute(index, state.index);
144
- if (!entry) {
145
- dlog(`Tab: no screen registered for route name "${route.name}"`);
79
+ const TabImpl = forwardRef<ITabNavigatorHandle, ITabProps>(
80
+ (props, forwardedRef) => {
81
+ // Read BEFORE establishing this Tab's own Context value below - becomes the `parent` link a
82
+ // nested screen's useNavigation().getParent() walks (e.g. this Tab rendered as a Stack screen's
83
+ // content reaches that Stack via this value). undefined when this Tab is the nesting root.
84
+ const ambientContext = useContext(NavigationContext);
85
+ const registry = useMemo(
86
+ () => collectRegistry(props.children, isTabScreenElement),
87
+ [props.children],
88
+ );
89
+ const routeIdPrefix = useId();
90
+
91
+ const routes = useMemo<IRoute<unknown>[]>(
92
+ () =>
93
+ Array.from(registry.entries()).map(([name, entry]) => ({
94
+ key: `${routeIdPrefix}-${name}`,
95
+ name,
96
+ params: entry.initialParams,
97
+ })),
98
+ [registry, routeIdPrefix],
99
+ );
100
+
101
+ // A <Tab.Screen> can appear or disappear after mount (a marker behind a conditional, a
102
+ // data-driven screen list), so the route list is re-derived from `children` on every render and
103
+ // the STORED state is reconciled against it (reconcileTabRoutes, core - it preserves each
104
+ // surviving route's key and accumulated params and keeps focus on the same route NAME) rather
105
+ // than staying frozen at whatever the first render saw. `null` means no marker had registered
106
+ // yet: seeding an empty list would resolve initialRouteName against nothing and lose it, so the
107
+ // state stays derived until either a marker arrives or something dispatches.
108
+ const [storedState, setStoredState] = useState<ITabRouterState | null>(
109
+ () =>
110
+ routes.length === 0
111
+ ? null
112
+ : createInitialTabState(routes, props.initialRouteName),
113
+ );
114
+
115
+ const resolveState = useCallback(
116
+ (stored: ITabRouterState | null): ITabRouterState =>
117
+ stored === null
118
+ ? createInitialTabState(routes, props.initialRouteName)
119
+ : reconcileTabRoutes(stored, routes),
120
+ [routes, props.initialRouteName],
121
+ );
122
+
123
+ const state = resolveState(storedState);
124
+
125
+ if (routes.length === 0) dlog('Tab: no <Tab.Screen> children registered');
126
+
127
+ // The updater re-resolves rather than closing over `state` so that two dispatches batched into
128
+ // one render still compose: the second reduces over the first's result, not a stale snapshot.
129
+ const dispatch = useCallback(
130
+ (action: ITabRouterAction) =>
131
+ setStoredState(current =>
132
+ tabRouterReducer(resolveState(current), action),
133
+ ),
134
+ [resolveState],
135
+ );
136
+
137
+ const jumpTo = useCallback(
138
+ (name: string, params?: unknown) =>
139
+ dispatch({ type: 'jumpTo', name, params }),
140
+ [dispatch],
141
+ );
142
+ const setParams = useCallback(
143
+ (params: unknown, key: string) =>
144
+ dispatch({ type: 'setParams', key, params }),
145
+ [dispatch],
146
+ );
147
+
148
+ const handle = useMemo<ITabNavigatorHandle>(
149
+ () => ({ jumpTo, setParams }),
150
+ [jumpTo, setParams],
151
+ );
152
+
153
+ useImperativeHandle(forwardedRef, () => handle, [handle]);
154
+
155
+ const focusedRoute: IRoute<unknown> | undefined = state.routes[state.index];
156
+
157
+ const items: ITabBarItemView[] = state.routes.map((route, index) => {
158
+ const entry = registry.get(route.name);
159
+ const focused = isFocusedRoute(index, state.index);
160
+ if (!entry) {
161
+ dlog(`Tab: no screen registered for route name "${route.name}"`);
162
+ return {
163
+ key: route.key,
164
+ focused,
165
+ label: route.name,
166
+ passthrough: {},
167
+ };
168
+ }
169
+
170
+ const options = resolveTabOptions(
171
+ entry,
172
+ { route, navigation: handle },
173
+ props.screenOptions,
174
+ );
175
+
146
176
  return {
147
177
  key: route.key,
148
178
  focused,
149
- label: route.name,
179
+ label: options.tabBarLabel ?? options.title ?? route.name,
180
+ icon: options.tabBarIcon,
181
+ badge: options.tabBarBadge,
182
+ activeTintColor: options.tabBarActiveTintColor,
183
+ inactiveTintColor: options.tabBarInactiveTintColor,
184
+ passthrough: {
185
+ onPress: () => jumpTo(route.name),
186
+ accessibilityRole: 'tab',
187
+ accessibilityState: { selected: focused },
188
+ },
189
+ };
190
+ });
191
+
192
+ const focusedEntry = focusedRoute
193
+ ? registry.get(focusedRoute.name)
194
+ : undefined;
195
+ const focusedOptions: ITabOptions | undefined =
196
+ focusedEntry && focusedRoute
197
+ ? resolveTabOptions(
198
+ focusedEntry,
199
+ { route: focusedRoute, navigation: handle },
200
+ props.screenOptions,
201
+ )
202
+ : props.screenOptions;
203
+
204
+ const tabBar = descriptorToReact(
205
+ renderTabBar({
206
+ items,
207
+ style: focusedOptions?.tabBarStyle,
150
208
  passthrough: {},
209
+ }),
210
+ );
211
+
212
+ // Only the focused route's screen is ever mounted (unlike Stack, which keeps every pushed route
213
+ // alive), so a fresh emitter per focus change is sufficient - no per-route emitter map to prune,
214
+ // because the previous screen's whole subtree (and any listeners it registered) is torn down by
215
+ // an ordinary React unmount when focus moves on. Keyed on the route KEY rather than the route
216
+ // object so a setParams-only change (new route object, same key) doesn't spuriously re-fire
217
+ // focus/blur.
218
+ const focusedRouteKey = focusedRoute?.key;
219
+ const routeEmitter = useMemo(
220
+ () => createNavigationEmitter(),
221
+ [focusedRouteKey],
222
+ );
223
+
224
+ // Tab paints its own bar in pure JS - there is no native onAppear/onDisappear to hook (unlike
225
+ // Stack's RNSScreen), so focus/blur is synthesized here: mount = focus, cleanup = blur, exactly
226
+ // what an effect keyed on focusedRouteKey already encodes - no diffFocusedRoute indirection
227
+ // needed (unlike Vue/Angular, which diff real prev/next keys inside an imperative watch/CD
228
+ // callback that has no mount/cleanup pairing of its own).
229
+ useEffect(() => {
230
+ if (focusedRouteKey === undefined) return undefined;
231
+ dlog(`Tab: route "${focusedRoute?.name}" focused`);
232
+ routeEmitter.emit(NAVIGATION_EVENT_FOCUS);
233
+ return () => {
234
+ dlog(`Tab: route "${focusedRoute?.name}" blurred`);
235
+ routeEmitter.emit(NAVIGATION_EVENT_BLUR);
151
236
  };
152
- }
153
-
154
- const options = resolveTabOptions(entry, { route, navigation: handle }, props.screenOptions);
155
-
156
- return {
157
- key: route.key,
158
- focused,
159
- label: options.tabBarLabel ?? options.title ?? route.name,
160
- icon: options.tabBarIcon,
161
- badge: options.tabBarBadge,
162
- activeTintColor: options.tabBarActiveTintColor,
163
- inactiveTintColor: options.tabBarInactiveTintColor,
164
- passthrough: {
165
- onPress: () => jumpTo(route.name),
166
- accessibilityRole: 'tab',
167
- accessibilityState: { selected: focused },
168
- },
169
- };
170
- });
171
-
172
- const focusedEntry = focusedRoute ? registry.get(focusedRoute.name) : undefined;
173
- const focusedOptions: ITabOptions | undefined =
174
- focusedEntry && focusedRoute
175
- ? resolveTabOptions(
176
- focusedEntry,
177
- { route: focusedRoute, navigation: handle },
178
- props.screenOptions,
179
- )
180
- : props.screenOptions;
181
-
182
- const tabBar = descriptorToReact(
183
- renderTabBar({ items, style: focusedOptions?.tabBarStyle, passthrough: {} }),
184
- );
185
-
186
- // Only the focused route's screen is ever mounted (unlike Stack, which keeps every pushed route
187
- // alive), so a fresh emitter per focus change is sufficient - no per-route emitter map to prune,
188
- // because the previous screen's whole subtree (and any listeners it registered) is torn down by
189
- // an ordinary React unmount when focus moves on. Keyed on the route KEY rather than the route
190
- // object so a setParams-only change (new route object, same key) doesn't spuriously re-fire
191
- // focus/blur.
192
- const focusedRouteKey = focusedRoute?.key;
193
- const routeEmitter = useMemo(() => createNavigationEmitter(), [focusedRouteKey]);
194
-
195
- // Tab paints its own bar in pure JS - there is no native onAppear/onDisappear to hook (unlike
196
- // Stack's RNSScreen), so focus/blur is synthesized here: mount = focus, cleanup = blur, exactly
197
- // what an effect keyed on focusedRouteKey already encodes - no diffFocusedRoute indirection
198
- // needed (unlike Vue/Angular, which diff real prev/next keys inside an imperative watch/CD
199
- // callback that has no mount/cleanup pairing of its own).
200
- useEffect(() => {
201
- if (focusedRouteKey === undefined) return undefined;
202
- dlog(`Tab: route "${focusedRoute?.name}" focused`);
203
- routeEmitter.emit(NAVIGATION_EVENT_FOCUS);
204
- return () => {
205
- dlog(`Tab: route "${focusedRoute?.name}" blurred`);
206
- routeEmitter.emit(NAVIGATION_EVENT_BLUR);
207
- };
208
- // focusedRoute omitted deliberately: only its .key (tracked via focusedRouteKey) should
209
- // re-run this effect - see the comment above on focusedRouteKey.
210
- // eslint-disable-next-line react-hooks/exhaustive-deps
211
- }, [routeEmitter, focusedRouteKey]);
212
-
213
- const content =
214
- focusedEntry && focusedRoute
215
- ? createElement(
216
- NavigationContext.Provider,
217
- {
218
- value: {
219
- route: focusedRoute,
220
- navigation: handle,
221
- emitter: routeEmitter,
222
- parent: ambientContext,
237
+ // focusedRoute omitted deliberately: only its .key (tracked via focusedRouteKey) should
238
+ // re-run this effect - see the comment above on focusedRouteKey.
239
+ // eslint-disable-next-line react-hooks/exhaustive-deps
240
+ }, [routeEmitter, focusedRouteKey]);
241
+
242
+ const content =
243
+ focusedEntry && focusedRoute
244
+ ? createElement(
245
+ NavigationContext.Provider,
246
+ {
247
+ value: {
248
+ route: focusedRoute,
249
+ navigation: handle,
250
+ emitter: routeEmitter,
251
+ parent: ambientContext,
252
+ },
223
253
  },
224
- },
225
- createElement(focusedEntry.component),
226
- )
227
- : null;
228
-
229
- return createElement(
230
- 'symbiote-view',
231
- { style: TAB_ROOT_STYLE },
232
- createElement('symbiote-view', { style: TAB_CONTENT_STYLE }, content),
233
- tabBar,
234
- );
235
- });
254
+ createElement(focusedEntry.component),
255
+ )
256
+ : null;
257
+
258
+ return createElement(
259
+ 'symbiote-view',
260
+ { style: TAB_ROOT_STYLE },
261
+ createElement('symbiote-view', { style: TAB_CONTENT_STYLE }, content),
262
+ tabBar,
263
+ );
264
+ },
265
+ );
236
266
 
237
267
  export const Tab = Object.assign(TabImpl, { Screen: TabScreen });