@symbiote-native/navigation 2.0.3 → 2.1.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/build/angular/drawer/index.js +6 -4
- package/build/angular/stack/index.d.ts +2 -0
- package/build/angular/stack/index.js +23 -3
- package/build/angular/tabs/index.d.ts +1 -1
- package/build/angular/tabs/index.js +20 -7
- package/build/core/index.d.ts +2 -1
- package/build/core/index.js +5 -1
- package/build/core/stack-router-state/index.d.ts +2 -0
- package/build/core/stack-router-state/index.js +44 -0
- package/build/core/tab-router-state/index.d.ts +1 -0
- package/build/core/tab-router-state/index.js +19 -0
- package/build/react/drawer/index.js +23 -13
- package/build/react/stack/index.js +13 -2
- package/build/react/tabs/index.js +23 -9
- package/build/svelte/attachments.d.ts +5 -0
- package/build/svelte/attachments.js +61 -0
- package/build/svelte/descriptor-subtree.d.ts +3 -0
- package/build/svelte/descriptor-subtree.js +37 -0
- package/build/svelte/drawer/drawer-props.d.ts +14 -0
- package/build/svelte/drawer/drawer-props.js +3 -0
- package/build/svelte/drawer/index.d.ts +10 -0
- package/build/svelte/drawer/index.js +6 -0
- package/build/svelte/drawer/index.svelte +421 -0
- package/build/svelte/drawer/index.svelte.d.ts +10 -0
- package/build/svelte/drawer-screen-props.d.ts +13 -0
- package/build/svelte/drawer-screen-props.js +4 -0
- package/build/svelte/drawer-screen.svelte +24 -0
- package/build/svelte/drawer-screen.svelte.d.ts +4 -0
- package/build/svelte/index.d.ts +21 -0
- package/build/svelte/index.js +20 -0
- package/build/svelte/linking.svelte.d.ts +2 -0
- package/build/svelte/linking.svelte.js +55 -0
- package/build/svelte/navigation-context.d.ts +14 -0
- package/build/svelte/navigation-context.js +44 -0
- package/build/svelte/navigation-scope.svelte +24 -0
- package/build/svelte/navigation-scope.svelte.d.ts +9 -0
- package/build/svelte/registry-host.d.ts +1 -0
- package/build/svelte/registry-host.js +26 -0
- package/build/svelte/runes/index.d.ts +12 -0
- package/build/svelte/runes/index.js +12 -0
- package/build/svelte/runes/use-drawer-navigation.svelte.d.ts +6 -0
- package/build/svelte/runes/use-drawer-navigation.svelte.js +19 -0
- package/build/svelte/runes/use-focus-effect.svelte.d.ts +1 -0
- package/build/svelte/runes/use-focus-effect.svelte.js +31 -0
- package/build/svelte/runes/use-is-focused.svelte.d.ts +3 -0
- package/build/svelte/runes/use-is-focused.svelte.js +34 -0
- package/build/svelte/runes/use-navigation-state.svelte.d.ts +4 -0
- package/build/svelte/runes/use-navigation-state.svelte.js +39 -0
- package/build/svelte/runes/use-navigation.svelte.d.ts +9 -0
- package/build/svelte/runes/use-navigation.svelte.js +22 -0
- package/build/svelte/runes/use-route.svelte.d.ts +4 -0
- package/build/svelte/runes/use-route.svelte.js +12 -0
- package/build/svelte/runes/use-stack-navigation.svelte.d.ts +6 -0
- package/build/svelte/runes/use-stack-navigation.svelte.js +22 -0
- package/build/svelte/runes/use-tab-navigation.svelte.d.ts +6 -0
- package/build/svelte/runes/use-tab-navigation.svelte.js +19 -0
- package/build/svelte/screen-props.d.ts +21 -0
- package/build/svelte/screen-props.js +6 -0
- package/build/svelte/screen-registry.d.ts +17 -0
- package/build/svelte/screen-registry.js +57 -0
- package/build/svelte/screen.svelte +28 -0
- package/build/svelte/screen.svelte.d.ts +4 -0
- package/build/svelte/stack/index.d.ts +14 -0
- package/build/svelte/stack/index.js +7 -0
- package/build/svelte/stack/index.svelte +224 -0
- package/build/svelte/stack/index.svelte.d.ts +15 -0
- package/build/svelte/stack/stack-props.d.ts +20 -0
- package/build/svelte/stack/stack-props.js +4 -0
- package/build/svelte/stack/stack-screen.svelte +148 -0
- package/build/svelte/stack/stack-screen.svelte.d.ts +4 -0
- package/build/svelte/tab-screen-props.d.ts +13 -0
- package/build/svelte/tab-screen-props.js +4 -0
- package/build/svelte/tab-screen.svelte +26 -0
- package/build/svelte/tab-screen.svelte.d.ts +4 -0
- package/build/svelte/tabs/index.d.ts +8 -0
- package/build/svelte/tabs/index.js +6 -0
- package/build/svelte/tabs/index.svelte +247 -0
- package/build/svelte/tabs/index.svelte.d.ts +8 -0
- package/build/svelte/tabs/tab-props.d.ts +7 -0
- package/build/svelte/tabs/tab-props.js +3 -0
- package/build/vue/drawer/index.js +6 -5
- package/build/vue/stack/index.js +26 -12
- package/build/vue/tabs/index.js +48 -35
- package/build-ngc/angular/drawer/index.js +9 -7
- package/build-ngc/angular/drawer/index.js.map +1 -1
- package/build-ngc/angular/drawer-screen.directive.js +3 -3
- package/build-ngc/angular/navigation-context.service.js +3 -3
- package/build-ngc/angular/navigation-scope.directive.js +3 -3
- package/build-ngc/angular/screen.directive.js +3 -3
- package/build-ngc/angular/search-bar-ref.directive.js +3 -3
- package/build-ngc/angular/stack/index.d.ts +2 -0
- package/build-ngc/angular/stack/index.js +26 -6
- package/build-ngc/angular/stack/index.js.map +1 -1
- package/build-ngc/angular/tab-screen.directive.js +3 -3
- package/build-ngc/angular/tabs/index.d.ts +1 -1
- package/build-ngc/angular/tabs/index.js +23 -10
- package/build-ngc/angular/tabs/index.js.map +1 -1
- package/build-ngc/core/index.d.ts +2 -1
- package/build-ngc/core/index.js +5 -1
- package/build-ngc/core/index.js.map +1 -1
- package/build-ngc/core/stack-router-state/index.d.ts +2 -0
- package/build-ngc/core/stack-router-state/index.js +45 -0
- package/build-ngc/core/stack-router-state/index.js.map +1 -0
- package/build-ngc/core/tab-router-state/index.d.ts +1 -0
- package/build-ngc/core/tab-router-state/index.js +19 -0
- package/build-ngc/core/tab-router-state/index.js.map +1 -1
- package/package.json +28 -14
- package/src/angular/drawer/index.ts +6 -4
- package/src/angular/stack/index.ts +23 -2
- package/src/angular/tabs/index.ts +19 -5
- package/src/core/index.ts +11 -1
- package/src/core/stack-router-state/index.ts +49 -0
- package/src/core/tab-router-state/index.ts +25 -0
- package/src/react/drawer/index.ts +28 -13
- package/src/react/stack/index.ts +15 -1
- package/src/react/tabs/index.ts +43 -10
- package/src/svelte/attachments.ts +68 -0
- package/src/svelte/descriptor-subtree.ts +49 -0
- package/src/svelte/drawer/drawer-props.ts +28 -0
- package/src/svelte/drawer/index.svelte +421 -0
- package/src/svelte/drawer/index.ts +10 -0
- package/src/svelte/drawer-screen-props.ts +20 -0
- package/src/svelte/drawer-screen.svelte +24 -0
- package/src/svelte/fabric-tree.test-helper.ts +61 -0
- package/src/svelte/index.ts +71 -0
- package/src/svelte/linking.svelte.ts +66 -0
- package/src/svelte/navigation-context.ts +62 -0
- package/src/svelte/navigation-scope.svelte +24 -0
- package/src/svelte/registry-host.ts +26 -0
- package/src/svelte/runes/index.ts +17 -0
- package/src/svelte/runes/use-drawer-navigation.svelte.ts +29 -0
- package/src/svelte/runes/use-focus-effect.svelte.ts +35 -0
- package/src/svelte/runes/use-is-focused.svelte.ts +38 -0
- package/src/svelte/runes/use-navigation-state.svelte.ts +46 -0
- package/src/svelte/runes/use-navigation.svelte.ts +39 -0
- package/src/svelte/runes/use-route.svelte.ts +16 -0
- package/src/svelte/runes/use-stack-navigation.svelte.ts +32 -0
- package/src/svelte/runes/use-tab-navigation.svelte.ts +29 -0
- package/src/svelte/screen-props.ts +45 -0
- package/src/svelte/screen-registry.ts +92 -0
- package/src/svelte/screen.svelte +28 -0
- package/src/svelte/stack/index.svelte +224 -0
- package/src/svelte/stack/index.ts +11 -0
- package/src/svelte/stack/stack-props.ts +31 -0
- package/src/svelte/stack/stack-screen.svelte +148 -0
- package/src/svelte/svelte-ambient.d.ts +6 -0
- package/src/svelte/svelte-compile.test-helper.ts +143 -0
- package/src/svelte/tab-screen-props.ts +20 -0
- package/src/svelte/tab-screen.svelte +26 -0
- package/src/svelte/tabs/index.svelte +247 -0
- package/src/svelte/tabs/index.ts +10 -0
- package/src/svelte/tabs/tab-props.ts +13 -0
- package/src/vue/drawer/index.ts +6 -5
- package/src/vue/stack/index.ts +28 -11
- package/src/vue/tabs/index.ts +68 -65
|
@@ -243,12 +243,14 @@ let Drawer = (() => {
|
|
|
243
243
|
this.dispatch({ type: 'toggleDrawer' });
|
|
244
244
|
};
|
|
245
245
|
jumpTo = (name) => {
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
246
|
+
// Both sides of the dispatch, mirroring vue/drawer/index.ts's jumpTo: an unregistered name is
|
|
247
|
+
// a documented reducer no-op that hands the SAME state back, so animating off the pre-dispatch
|
|
248
|
+
// snapshot alone would slide the panel shut while the router still says isOpen. The signal is
|
|
249
|
+
// set synchronously inside dispatch, so the second read is already the reducer's own answer.
|
|
249
250
|
const wasOpen = this.stateSignal()?.isOpen ?? false;
|
|
250
251
|
this.dispatch({ type: 'jumpTo', name });
|
|
251
|
-
|
|
252
|
+
const isOpenNow = this.stateSignal()?.isOpen ?? false;
|
|
253
|
+
if (wasOpen && !isOpenNow)
|
|
252
254
|
this.animateProgressTo(false);
|
|
253
255
|
};
|
|
254
256
|
panResponder = PanResponder.create({
|
|
@@ -32,9 +32,11 @@ export declare class Stack implements AfterContentInit, OnDestroy, INavigatorHan
|
|
|
32
32
|
ngAfterContentInit(): void;
|
|
33
33
|
ngOnDestroy(): void;
|
|
34
34
|
private rebuildRegistry;
|
|
35
|
+
private reconcileWithRegistry;
|
|
35
36
|
private initializeState;
|
|
36
37
|
private createRoute;
|
|
37
38
|
private dispatch;
|
|
39
|
+
private commitState;
|
|
38
40
|
emitterFor(routeKey: string): INavigationEmitter;
|
|
39
41
|
private mergedOptionsFor;
|
|
40
42
|
componentFor(route: IRoute<unknown>): Type<unknown> | null;
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// self-registers instead: `adapters/angular/babel-register-composed.cjs` (a Metro babel preset
|
|
36
36
|
// applied bundle-wide, not scoped to adapters/angular) scans this package's own AOT-compiled
|
|
37
37
|
// (`ngc`) `ɵɵngDeclareComponent({selector: 'Stack', ...})` output and auto-calls
|
|
38
|
-
// `registerComposedComponent('Stack')` at bundle time - same mechanism
|
|
38
|
+
// `registerComposedComponent('Stack')` at bundle time - same mechanism `examples/angular`
|
|
39
39
|
// navigation-demo screens and `@symbiote-native/slider`'s `Slider` rely on for their own composed
|
|
40
40
|
// components mounted statically or via `NgComponentOutlet`. Unregistered, `createElement('Stack')`
|
|
41
41
|
// falls through to a real `createNode` call and RN paints its own "Unimplemented component"
|
|
@@ -81,7 +81,7 @@ import { ChangeDetectionStrategy, Component, ContentChildren, Input, NO_ERRORS_S
|
|
|
81
81
|
import { NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
|
|
82
82
|
import { SymbioteHostPropsDirective } from '@symbiote-native/angular';
|
|
83
83
|
import { Platform, dlog } from '@symbiote-native/engine';
|
|
84
|
-
import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_STATE, RNS_MODAL_SCREEN_VIEW_NAME, SCREEN_ON_APPEAR, SCREEN_ON_DISAPPEAR, SCREEN_ON_DISMISSED, SCREEN_ON_HEADER_BACK_BUTTON_CLICKED, STACK_ON_FINISH_TRANSITIONING, buildSearchBarPassthrough, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, resolveHeaderInModalStackStyle, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
|
|
84
|
+
import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_STATE, RNS_MODAL_SCREEN_VIEW_NAME, SCREEN_ON_APPEAR, SCREEN_ON_DISAPPEAR, SCREEN_ON_DISMISSED, SCREEN_ON_HEADER_BACK_BUTTON_CLICKED, STACK_ON_FINISH_TRANSITIONING, buildSearchBarPassthrough, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, reconcileStackRoutes, resolveHeaderInModalStackStyle, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
|
|
85
85
|
import { NavigationScopeDirective } from '../navigation-scope.directive.js';
|
|
86
86
|
import { SearchBarRefDirective } from '../search-bar-ref.directive.js';
|
|
87
87
|
import { ScreenDirective } from '../screen.directive.js';
|
|
@@ -246,6 +246,21 @@ let Stack = (() => {
|
|
|
246
246
|
for (const screen of this.screenChildren) {
|
|
247
247
|
this.registry.set(screen.name, screen);
|
|
248
248
|
}
|
|
249
|
+
this.reconcileWithRegistry();
|
|
250
|
+
}
|
|
251
|
+
// A `<ng-template symbioteScreen>` marker can leave the @ContentChildren query (a marker behind
|
|
252
|
+
// an `@if`, a data-driven screen list) while its route is still in the pushed history, which
|
|
253
|
+
// would leave that entry with nothing for componentFor() to mount (reconcileStackRoutes' header).
|
|
254
|
+
// The signal write lives HERE, in the query-change callback, and never in a computed/template
|
|
255
|
+
// accessor - reconciliation is a reaction to the registry changing, not a derivation of it.
|
|
256
|
+
reconcileWithRegistry() {
|
|
257
|
+
const current = this.stateSignal();
|
|
258
|
+
if (current === undefined)
|
|
259
|
+
return;
|
|
260
|
+
const next = reconcileStackRoutes(current, [...this.registry.keys()]);
|
|
261
|
+
if (next === current)
|
|
262
|
+
return;
|
|
263
|
+
this.commitState(next);
|
|
249
264
|
}
|
|
250
265
|
initializeState() {
|
|
251
266
|
if (this.stateSignal() !== undefined)
|
|
@@ -266,7 +281,12 @@ let Stack = (() => {
|
|
|
266
281
|
const current = this.stateSignal();
|
|
267
282
|
if (current === undefined)
|
|
268
283
|
return;
|
|
269
|
-
|
|
284
|
+
this.commitState(navigatorReducer(current, action));
|
|
285
|
+
}
|
|
286
|
+
// The one write path into stateSignal, shared by a dispatched action and a registry-driven
|
|
287
|
+
// reconciliation: both invalidate the plan cache, broadcast to every still-live route, and prune
|
|
288
|
+
// the emitters of routes that are gone.
|
|
289
|
+
commitState(next) {
|
|
270
290
|
this.planCache.clear();
|
|
271
291
|
this.stateSignal.set(next);
|
|
272
292
|
const liveRouteKeys = new Set(next.routes.map(route => route.key));
|
|
@@ -32,7 +32,7 @@ export declare class Tab implements AfterContentInit, OnDestroy, ITabNavigatorHa
|
|
|
32
32
|
ngOnDestroy(): void;
|
|
33
33
|
private rebuildRegistry;
|
|
34
34
|
private routesFromRegistry;
|
|
35
|
-
private
|
|
35
|
+
private syncRegistry;
|
|
36
36
|
private dispatch;
|
|
37
37
|
private resolveTabOptions;
|
|
38
38
|
focusedRoute(): IRoute<unknown> | undefined;
|
|
@@ -53,7 +53,7 @@ import { ChangeDetectionStrategy, Component, ContentChildren, Input, QueryList,
|
|
|
53
53
|
import { NgComponentOutlet } from '@angular/common';
|
|
54
54
|
import { dlog } from '@symbiote-native/engine';
|
|
55
55
|
import { DescriptorOutlet, View } from '@symbiote-native/angular';
|
|
56
|
-
import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, diffFocusedRoute, isFocusedRoute, renderTabBar, tabRouterReducer, } from '../../core/index.js';
|
|
56
|
+
import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, diffFocusedRoute, isFocusedRoute, reconcileTabRoutes, renderTabBar, tabRouterReducer, } from '../../core/index.js';
|
|
57
57
|
import { NavigationScopeDirective } from '../navigation-scope.directive.js';
|
|
58
58
|
import { TabScreenDirective } from '../tab-screen.directive.js';
|
|
59
59
|
const TAB_ROOT_STYLE = { flex: 1 };
|
|
@@ -128,10 +128,9 @@ let Tab = (() => {
|
|
|
128
128
|
jumpTo = (name, params) => this.dispatch({ type: 'jumpTo', name, params });
|
|
129
129
|
setParams = (params, key) => this.dispatch({ type: 'setParams', key, params });
|
|
130
130
|
ngAfterContentInit() {
|
|
131
|
-
this.
|
|
132
|
-
this.initializeState();
|
|
131
|
+
this.syncRegistry();
|
|
133
132
|
this.tabScreenChildrenSubscription = this.tabScreenChildren.changes.subscribe(() => {
|
|
134
|
-
this.
|
|
133
|
+
this.syncRegistry();
|
|
135
134
|
});
|
|
136
135
|
}
|
|
137
136
|
ngOnDestroy() {
|
|
@@ -152,12 +151,26 @@ let Tab = (() => {
|
|
|
152
151
|
params: entry.initialParams,
|
|
153
152
|
}));
|
|
154
153
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
// A <ng-template symbioteTabScreen> can appear or disappear after mount (a marker behind an
|
|
155
|
+
// @if, a data-driven screen list), so the route list follows the LIVE query instead of staying
|
|
156
|
+
// frozen at whatever ngAfterContentInit first saw: reconcileTabRoutes (core) keeps each
|
|
157
|
+
// surviving route's key and accumulated params and moves focus only when the focused route is
|
|
158
|
+
// the one that left. Runs from the query's own `changes` subscription - an ordinary imperative
|
|
159
|
+
// callback, deliberately not a computed, since this writes the state signal.
|
|
160
|
+
syncRegistry() {
|
|
161
|
+
this.rebuildRegistry();
|
|
158
162
|
const routes = this.routesFromRegistry();
|
|
159
163
|
if (routes.length === 0)
|
|
160
164
|
dlog('Tab: no <ng-template symbioteTabScreen> children registered');
|
|
165
|
+
const current = this.stateSignal();
|
|
166
|
+
if (current !== undefined) {
|
|
167
|
+
this.stateSignal.set(reconcileTabRoutes(current, routes));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Seeding an empty list would resolve initialRouteName against nothing and lose it once the
|
|
171
|
+
// markers do arrive, so the state stays unseeded until at least one has registered.
|
|
172
|
+
if (routes.length === 0)
|
|
173
|
+
return;
|
|
161
174
|
this.stateSignal.set(createInitialTabState(routes, this.initialRouteName));
|
|
162
175
|
}
|
|
163
176
|
dispatch(action) {
|
package/build/core/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './constants';
|
|
2
2
|
export { createInitialNavigatorState, navigatorReducer, computeActivityState, isTopRoute, } from './navigator-state';
|
|
3
3
|
export type { IRoute, INavigatorState, INavigatorAction } from './navigator-state';
|
|
4
|
+
export { reconcileStackRoutes } from './stack-router-state';
|
|
4
5
|
export type { IStackAnimation, IStackPresentation, IHeaderBackButtonDisplayMode, IHeaderUserInterfaceStyle, IOptionalBooleanNativeProp, ISheetAllowedDetents, ISheetLargestUndimmedDetent, ISheetInitialDetent, IHeaderBarButtonIcon, IHeaderBarButtonItemBadge, IHeaderBarButtonItemAction, IHeaderBarButtonMenuAction, IHeaderBarButtonSubmenu, IHeaderBarButtonItemMenu, IHeaderBarButtonItemSpacing, IHeaderBarButtonItem, ISearchBarAutoCapitalize, ISearchBarPlacement, ISearchBarOptions, IScreenOrientation, IScreenOptions, INavigatorPlatform, IScreenViewProps, IHeaderConfigViewProps, IStackViewProps, ISearchBarViewProps, } from './navigator-props';
|
|
5
6
|
export { resolveHeaderConfigView, resolveScreenView, resolveSearchBarView } from './screen-options';
|
|
6
7
|
export { buildSearchBarHandle } from './search-bar-commands';
|
|
@@ -11,7 +12,7 @@ export { serializeNavigatorState, deserializeNavigatorState } from './state-pers
|
|
|
11
12
|
export { createNavigationEmitter, diffFocusedRoute, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_STATE, NAVIGATION_EVENT_BEFORE_REMOVE, } from './navigation-events';
|
|
12
13
|
export type { INavigationEmitter, INavigationEventListener, INavigationEventName, IFocusTransition, } from './navigation-events';
|
|
13
14
|
export { isRecord } from './guards';
|
|
14
|
-
export { createInitialTabState, tabRouterReducer, isFocusedRoute } from './tab-router-state';
|
|
15
|
+
export { createInitialTabState, tabRouterReducer, reconcileTabRoutes, isFocusedRoute, } from './tab-router-state';
|
|
15
16
|
export type { ITabRouterState, ITabRouterAction } from './tab-router-state';
|
|
16
17
|
export type { ITabBarIcon, ITabOptions } from './tab-options';
|
|
17
18
|
export { renderTabBarItem, renderTabBar } from './render-tabs';
|
package/build/core/index.js
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
// the descriptor bridge.
|
|
8
8
|
export * from './constants.js';
|
|
9
9
|
export { createInitialNavigatorState, navigatorReducer, computeActivityState, isTopRoute, } from './navigator-state/index.js';
|
|
10
|
+
// Drops history entries whose `<Stack.Screen>` marker has unregistered - see
|
|
11
|
+
// stack-router-state.ts's header for why the Stack's history needs this and the Tab's projection
|
|
12
|
+
// does not.
|
|
13
|
+
export { reconcileStackRoutes } from './stack-router-state/index.js';
|
|
10
14
|
export { resolveHeaderConfigView, resolveScreenView, resolveSearchBarView } from './screen-options.js';
|
|
11
15
|
export { buildSearchBarHandle } from './search-bar-commands.js';
|
|
12
16
|
export { resolveScreenProps, resolveStackProps, renderHeaderConfig, resolveSearchBarProps, resolveScreenContentWrapperStyle, resolveScreenViewName, isHeaderInModal, resolveHeaderInModalStackStyle, resolveHeaderInModalScreenStyle, buildSearchBarPassthrough, resolveScreenRenderPlan, } from './render-stack.js';
|
|
@@ -18,7 +22,7 @@ export { createNavigationEmitter, diffFocusedRoute, NAVIGATION_EVENT_FOCUS, NAVI
|
|
|
18
22
|
export { isRecord } from './guards.js';
|
|
19
23
|
// Bottom-tabs navigator: a FOCUSED-INDEX router (not a stack - see tab-router-state.ts) driving
|
|
20
24
|
// a pure-JS tab bar render, framework-agnostic and shared verbatim by every adapter.
|
|
21
|
-
export { createInitialTabState, tabRouterReducer, isFocusedRoute } from './tab-router-state/index.js';
|
|
25
|
+
export { createInitialTabState, tabRouterReducer, reconcileTabRoutes, isFocusedRoute, } from './tab-router-state/index.js';
|
|
22
26
|
export { renderTabBarItem, renderTabBar } from './render-tabs.js';
|
|
23
27
|
// Linking config: resolves a URL to a route (and back), the piece @react-navigation's
|
|
24
28
|
// NavigationContainer `linking` prop provides on top of the framework-agnostic Linking module.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// StackRouter: the registry-reconciliation half (framework-agnostic, zero render). A Stack's route
|
|
2
|
+
// list is navigation HISTORY, not a projection of the registered `<Stack.Screen>` markers the way a
|
|
3
|
+
// Tab's is (tab-router-state) - it seeds exactly one route and grows only through push/replace. So
|
|
4
|
+
// a marker that unregisters while its route still sits in the history (a marker behind an `{#if}`,
|
|
5
|
+
// a data-driven screen list) leaves behind an entry nothing can render: the route stays, the
|
|
6
|
+
// adapter's componentFor() finds no screen, and the user is parked on a blank RNSScreen with no
|
|
7
|
+
// way to tell what happened. Mirrors @react-navigation/routers' StackRouter.getStateForRouteNames-
|
|
8
|
+
// Change, which filters those routes out of the history and pulls the focused index back into
|
|
9
|
+
// range.
|
|
10
|
+
//
|
|
11
|
+
// Deliberately NOT a navigatorReducer action: this is not something a caller dispatches, it is a
|
|
12
|
+
// reaction to the registry changing underneath an unchanged history, and each adapter observes
|
|
13
|
+
// that change through its own lifecycle. Keeping it a separate pure function lets every adapter
|
|
14
|
+
// wire it its own way without teaching the reducer about a registry it has never seen.
|
|
15
|
+
// The contract, and the two places it deliberately parts from upstream:
|
|
16
|
+
//
|
|
17
|
+
// - SURVIVORS KEEP THEIR IDENTITY. A stack route's key is COUNTER-derived (every adapter's
|
|
18
|
+
// createRoute increments a per-navigator sequence), not name-derived like a tab route's, so
|
|
19
|
+
// re-deriving the list from the registry would hand every survivor a brand-new key and remount
|
|
20
|
+
// live screens. Only the phantom entries go; every other route object passes through by
|
|
21
|
+
// reference, params included.
|
|
22
|
+
//
|
|
23
|
+
// - THE FOCUSED ROUTE IS THE TOP ONE. This state carries no `index` field: the focused route is
|
|
24
|
+
// always the last one (see computeActivityState's comment for why no route can outrank it), so
|
|
25
|
+
// upstream's `index: Math.min(state.index, routes.length - 1)` degenerates here to "the last
|
|
26
|
+
// survivor". Dropping the FOCUSED route therefore lands the user on the nearest still-registered
|
|
27
|
+
// route beneath it - where a pop() would have left them - rather than on nothing.
|
|
28
|
+
//
|
|
29
|
+
// - THE ROUTE LIST IS NEVER EMPTIED. `routes.length - 1` IS the focused index, so an empty list
|
|
30
|
+
// means no focused route at all - the blank screen this function exists to prevent. When nothing
|
|
31
|
+
// in the history survives (every marker gone, or the whole screen set swapped for new names) the
|
|
32
|
+
// state is returned UNCHANGED. Upstream instead pushes a freshly nanoid()-keyed route for
|
|
33
|
+
// initialRouteName; we cannot, because minting a route key belongs to the caller here
|
|
34
|
+
// (navigator-state.ts's header) and a derivation that mints one would allocate a new key on every
|
|
35
|
+
// re-run. Unchanged is also the right answer for the usual cause of a momentarily empty registry
|
|
36
|
+
// - markers unregistering and re-registering across a re-render - since the next run reconciles
|
|
37
|
+
// for real.
|
|
38
|
+
export function reconcileStackRoutes(state, registeredNames) {
|
|
39
|
+
const routes = state.routes.filter(route => registeredNames.includes(route.name));
|
|
40
|
+
// filter preserves order, so an unchanged length means nothing was dropped.
|
|
41
|
+
if (routes.length === state.routes.length || routes.length === 0)
|
|
42
|
+
return state;
|
|
43
|
+
return { routes };
|
|
44
|
+
}
|
|
@@ -15,5 +15,6 @@ export type ITabRouterAction = {
|
|
|
15
15
|
params: unknown;
|
|
16
16
|
};
|
|
17
17
|
export declare function createInitialTabState(routes: readonly IRouteEntry[], initialRouteName?: string): ITabRouterState;
|
|
18
|
+
export declare function reconcileTabRoutes(state: ITabRouterState, nextRoutes: readonly IRouteEntry[]): ITabRouterState;
|
|
18
19
|
export declare function tabRouterReducer(state: ITabRouterState, action: ITabRouterAction): ITabRouterState;
|
|
19
20
|
export declare function isFocusedRoute(index: number, focusedIndex: number): boolean;
|
|
@@ -14,6 +14,25 @@ export function createInitialTabState(routes, initialRouteName) {
|
|
|
14
14
|
const index = routes.findIndex(route => route.name === initialRouteName);
|
|
15
15
|
return { routes, index: index === -1 ? INITIAL_FOCUSED_INDEX : index };
|
|
16
16
|
}
|
|
17
|
+
// A screen marker can appear or disappear after mount (a marker behind an `{#if}`, a data-driven
|
|
18
|
+
// screen list), so a navigator's route list has to follow its registry instead of staying frozen
|
|
19
|
+
// at whatever createInitialTabState saw first. Mirrors TabRouter.getStateForRouteNamesChange:
|
|
20
|
+
// membership and order come from `nextRoutes`, but a route that survives keeps its IDENTITY (its
|
|
21
|
+
// key) and everything jumpTo/setParams accumulated onto its params - only genuinely new names
|
|
22
|
+
// take their entry from `nextRoutes`. Focus follows the previously focused route's NAME, so an
|
|
23
|
+
// unrelated screen disappearing never moves the user; when the focused route is the one that
|
|
24
|
+
// disappeared, focus falls back to the first route, the same landing spot every unresolvable
|
|
25
|
+
// focus gets here (createInitialTabState's own fallback).
|
|
26
|
+
export function reconcileTabRoutes(state, nextRoutes) {
|
|
27
|
+
const routes = nextRoutes.map(next => state.routes.find(route => route.name === next.name) ?? next);
|
|
28
|
+
const isUnchanged = routes.length === state.routes.length &&
|
|
29
|
+
routes.every((route, at) => route === state.routes[at]);
|
|
30
|
+
if (isUnchanged)
|
|
31
|
+
return state;
|
|
32
|
+
const focusedName = state.routes[state.index]?.name;
|
|
33
|
+
const index = routes.findIndex(route => route.name === focusedName);
|
|
34
|
+
return { routes, index: index === -1 ? INITIAL_FOCUSED_INDEX : index };
|
|
35
|
+
}
|
|
17
36
|
// jumpTo moves the focused index to an existing route by name and optionally merges params
|
|
18
37
|
// (navigation.jumpTo(name, params) - the tab-actions doc's own signature); it never adds,
|
|
19
38
|
// removes, or reorders routes (mirrors TabRouter: JUMP_TO is a no-op when the target name isn't
|
|
@@ -71,8 +71,10 @@ const DrawerImpl = forwardRef((props, forwardedRef) => {
|
|
|
71
71
|
optionsRef.current = options;
|
|
72
72
|
const screenWidthRef = useRef(screenWidth);
|
|
73
73
|
screenWidthRef.current = screenWidth;
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
// The whole router state, not just its isOpen bit: jumpTo below has to re-run the reducer to
|
|
75
|
+
// learn what the dispatch actually produced, and that needs the routes too.
|
|
76
|
+
const stateRef = useRef(state);
|
|
77
|
+
stateRef.current = state;
|
|
76
78
|
const animateProgressTo = useCallback((open) => {
|
|
77
79
|
// Investigation instrumentation (Drawer openDrawer-no-op / toggleDrawer-no-animation bug):
|
|
78
80
|
// every imperative caller (openDrawer/closeDrawer/toggleDrawer/jumpTo) funnels through here,
|
|
@@ -92,40 +94,48 @@ const DrawerImpl = forwardRef((props, forwardedRef) => {
|
|
|
92
94
|
}, [progress]);
|
|
93
95
|
const handle = useMemo(() => ({
|
|
94
96
|
openDrawer: () => {
|
|
95
|
-
dlog(`Drawer: openDrawer() called, isOpen=${
|
|
97
|
+
dlog(`Drawer: openDrawer() called, isOpen=${stateRef.current.isOpen} at t=${Date.now()}`);
|
|
96
98
|
animateProgressTo(true);
|
|
97
99
|
dispatch({ type: 'openDrawer' });
|
|
98
100
|
},
|
|
99
101
|
closeDrawer: () => {
|
|
100
|
-
dlog(`Drawer: closeDrawer() called, isOpen=${
|
|
102
|
+
dlog(`Drawer: closeDrawer() called, isOpen=${stateRef.current.isOpen} at t=${Date.now()}`);
|
|
101
103
|
animateProgressTo(false);
|
|
102
104
|
dispatch({ type: 'closeDrawer' });
|
|
103
105
|
},
|
|
104
106
|
toggleDrawer: () => {
|
|
105
|
-
dlog(`Drawer: toggleDrawer() called, isOpen=${
|
|
106
|
-
animateProgressTo(!
|
|
107
|
+
dlog(`Drawer: toggleDrawer() called, isOpen=${stateRef.current.isOpen} at t=${Date.now()}`);
|
|
108
|
+
animateProgressTo(!stateRef.current.isOpen);
|
|
107
109
|
dispatch({ type: 'toggleDrawer' });
|
|
108
110
|
},
|
|
109
111
|
jumpTo: (name) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
// An unregistered name is a documented reducer no-op that hands the SAME state back, so
|
|
113
|
+
// animating off `isOpen` alone would slide the panel shut while the router still says
|
|
114
|
+
// open. useReducer's dispatch is async - stateRef only catches up on the next render - so
|
|
115
|
+
// the reducer (a pure function) is re-run here to learn what the dispatch produces, and
|
|
116
|
+
// the animation runs only on a genuine open -> closed transition.
|
|
117
|
+
const action = { type: 'jumpTo', name };
|
|
118
|
+
const current = stateRef.current;
|
|
119
|
+
const next = drawerRouterReducer(current, action);
|
|
120
|
+
dispatch(action);
|
|
121
|
+
if (current.isOpen && !next.isOpen)
|
|
112
122
|
animateProgressTo(false);
|
|
113
123
|
},
|
|
114
124
|
}), [animateProgressTo]);
|
|
115
125
|
useImperativeHandle(forwardedRef, () => handle, [handle]);
|
|
116
126
|
const panResponder = useRef(PanResponder.create({
|
|
117
|
-
onStartShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, screenWidthRef.current,
|
|
118
|
-
onMoveShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, screenWidthRef.current,
|
|
127
|
+
onStartShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, screenWidthRef.current, stateRef.current.isOpen, optionsRef.current, 'start'),
|
|
128
|
+
onMoveShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, screenWidthRef.current, stateRef.current.isOpen, optionsRef.current, 'move'),
|
|
119
129
|
onPanResponderGrant: () => {
|
|
120
130
|
dlog('Drawer: gesture grant');
|
|
121
|
-
dragStartProgress.current =
|
|
131
|
+
dragStartProgress.current = stateRef.current.isOpen ? 1 : 0;
|
|
122
132
|
},
|
|
123
133
|
onPanResponderMove: (_event, gestureState) => {
|
|
124
134
|
const nextProgress = resolveDragProgress(gestureState, dragStartProgress.current, optionsRef.current);
|
|
125
135
|
progress.setValue(nextProgress);
|
|
126
136
|
},
|
|
127
137
|
onPanResponderRelease: (_event, gestureState) => {
|
|
128
|
-
const intent = resolveSwipeIntent(gestureState,
|
|
138
|
+
const intent = resolveSwipeIntent(gestureState, stateRef.current.isOpen, optionsRef.current);
|
|
129
139
|
const open = intent === 'open';
|
|
130
140
|
dlog(`Drawer: gesture release -> ${open ? 'open' : 'close'}`);
|
|
131
141
|
animateProgressTo(open);
|
|
@@ -133,7 +143,7 @@ const DrawerImpl = forwardRef((props, forwardedRef) => {
|
|
|
133
143
|
},
|
|
134
144
|
onPanResponderTerminate: () => {
|
|
135
145
|
dlog('Drawer: gesture terminated, snapping back');
|
|
136
|
-
animateProgressTo(
|
|
146
|
+
animateProgressTo(stateRef.current.isOpen);
|
|
137
147
|
},
|
|
138
148
|
})).current;
|
|
139
149
|
const animated = isDrawerAnimated(options);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { createElement, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useReducer, useRef, } from 'react';
|
|
12
12
|
import { descriptorToReact } from '@symbiote-native/react';
|
|
13
13
|
import { Platform, debugNodeId, dlog } from '@symbiote-native/engine';
|
|
14
|
-
import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_STATE, RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, RNS_SCREEN_STACK_VIEW_NAME, RNS_SCREEN_VIEW_NAME, SCREEN_ON_APPEAR, SCREEN_ON_DISAPPEAR, SCREEN_ON_DISMISSED, SCREEN_ON_HEADER_BACK_BUTTON_CLICKED, SCREEN_ON_WILL_APPEAR, SCREEN_ON_WILL_DISAPPEAR, STACK_ON_FINISH_TRANSITIONING, buildSearchBarHandle, buildSearchBarPassthrough, computeActivityState, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
|
|
14
|
+
import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_STATE, RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, RNS_SCREEN_STACK_VIEW_NAME, RNS_SCREEN_VIEW_NAME, SCREEN_ON_APPEAR, SCREEN_ON_DISAPPEAR, SCREEN_ON_DISMISSED, SCREEN_ON_HEADER_BACK_BUTTON_CLICKED, SCREEN_ON_WILL_APPEAR, SCREEN_ON_WILL_DISAPPEAR, STACK_ON_FINISH_TRANSITIONING, buildSearchBarHandle, buildSearchBarPassthrough, computeActivityState, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, reconcileStackRoutes, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
|
|
15
15
|
import { collectRegistry } from '../collect-registry.js';
|
|
16
16
|
import { NavigationContext } from '../navigation-context.js';
|
|
17
17
|
import { Screen } from '../screen.js';
|
|
@@ -53,7 +53,8 @@ const StackImpl = forwardRef((props, forwardedRef) => {
|
|
|
53
53
|
routeSequence.current += 1;
|
|
54
54
|
return { key: `${routeIdPrefix}-${name}-${routeSequence.current}`, name, params };
|
|
55
55
|
}, [routeIdPrefix]);
|
|
56
|
-
const
|
|
56
|
+
const registeredNames = useMemo(() => [...registry.keys()], [registry]);
|
|
57
|
+
const [dispatchedState, dispatch] = useReducer(navigatorReducer, undefined, () => {
|
|
57
58
|
const initialRouteName = props.initialRouteName ?? registry.keys().next().value;
|
|
58
59
|
if (initialRouteName === undefined) {
|
|
59
60
|
dlog('Stack: no <Stack.Screen> children registered');
|
|
@@ -61,6 +62,16 @@ const StackImpl = forwardRef((props, forwardedRef) => {
|
|
|
61
62
|
}
|
|
62
63
|
return createInitialNavigatorState(createRoute(initialRouteName, registry.get(initialRouteName)?.initialParams));
|
|
63
64
|
});
|
|
65
|
+
// A <Stack.Screen> marker can unregister while its route is still in the pushed history, which
|
|
66
|
+
// would leave that entry with nothing to render (reconcileStackRoutes' header). Reconciling here
|
|
67
|
+
// rather than in an effect repairs the CURRENT paint - the call is pure and hands back the same
|
|
68
|
+
// reference when nothing changed, so it is safe during render; the effect below only PERSISTS
|
|
69
|
+
// the repair, so the next push builds on the pruned history instead of the phantom one.
|
|
70
|
+
const state = reconcileStackRoutes(dispatchedState, registeredNames);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (state !== dispatchedState)
|
|
73
|
+
dispatch({ type: 'reset', state });
|
|
74
|
+
}, [state, dispatchedState]);
|
|
64
75
|
const handle = useMemo(() => ({
|
|
65
76
|
push: (name, params) => dispatch({ type: 'push', route: createRoute(name, params) }),
|
|
66
77
|
pop: count => dispatch({ type: 'pop', count }),
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// Tab, the React lifecycle half. The focused-index router (tab-router-state) and the tab-bar
|
|
2
2
|
// Descriptor builder (render-tabs) live in @symbiote-native/navigation core, shared verbatim
|
|
3
|
-
// with the Vue/Angular adapters; here React supplies the lifecycle -
|
|
4
|
-
//
|
|
5
|
-
// plus the descriptor bridge for the tab-bar leaf, exactly like Stack bridges its header
|
|
6
|
-
// (react/stack.ts). Unlike Stack, a bottom-tabs bar is a PURE-JS UI: it paints ordinary
|
|
3
|
+
// with the Vue/Angular adapters; here React supplies the lifecycle - useState for the dispatched
|
|
4
|
+
// router state, useId for route-key generation, useImperativeHandle for the jumpTo/setParams
|
|
5
|
+
// handle - plus the descriptor bridge for the tab-bar leaf, exactly like Stack bridges its header
|
|
6
|
+
// config (react/stack.ts). Unlike Stack, a bottom-tabs bar is a PURE-JS UI: it paints ordinary
|
|
7
7
|
// `symbiote-view`/`symbiote-text` primitives via the shared render fn, so there is no
|
|
8
8
|
// react-native-screens ViewConfig to register here - Tab needs no `../register` import.
|
|
9
|
-
import { createElement, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo,
|
|
9
|
+
import { createElement, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useState, } from 'react';
|
|
10
10
|
import { descriptorToReact } from '@symbiote-native/react';
|
|
11
11
|
import { dlog } from '@symbiote-native/engine';
|
|
12
|
-
import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, isFocusedRoute, renderTabBar, tabRouterReducer, } from '../../core/index.js';
|
|
12
|
+
import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, isFocusedRoute, reconcileTabRoutes, renderTabBar, tabRouterReducer, } from '../../core/index.js';
|
|
13
13
|
import { collectRegistry } from '../collect-registry.js';
|
|
14
14
|
import { NavigationContext } from '../navigation-context.js';
|
|
15
15
|
import { TabScreen } from '../tab-screen.js';
|
|
@@ -34,11 +34,25 @@ const TabImpl = forwardRef((props, forwardedRef) => {
|
|
|
34
34
|
name,
|
|
35
35
|
params: entry.initialParams,
|
|
36
36
|
})), [registry, routeIdPrefix]);
|
|
37
|
-
|
|
37
|
+
// A <Tab.Screen> can appear or disappear after mount (a marker behind a conditional, a
|
|
38
|
+
// data-driven screen list), so the route list is re-derived from `children` on every render and
|
|
39
|
+
// the STORED state is reconciled against it (reconcileTabRoutes, core - it preserves each
|
|
40
|
+
// surviving route's key and accumulated params and keeps focus on the same route NAME) rather
|
|
41
|
+
// than staying frozen at whatever the first render saw. `null` means no marker had registered
|
|
42
|
+
// yet: seeding an empty list would resolve initialRouteName against nothing and lose it, so the
|
|
43
|
+
// state stays derived until either a marker arrives or something dispatches.
|
|
44
|
+
const [storedState, setStoredState] = useState(() => routes.length === 0 ? null : createInitialTabState(routes, props.initialRouteName));
|
|
45
|
+
const resolveState = useCallback((stored) => stored === null
|
|
46
|
+
? createInitialTabState(routes, props.initialRouteName)
|
|
47
|
+
: reconcileTabRoutes(stored, routes), [routes, props.initialRouteName]);
|
|
48
|
+
const state = resolveState(storedState);
|
|
38
49
|
if (routes.length === 0)
|
|
39
50
|
dlog('Tab: no <Tab.Screen> children registered');
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
// The updater re-resolves rather than closing over `state` so that two dispatches batched into
|
|
52
|
+
// one render still compose: the second reduces over the first's result, not a stale snapshot.
|
|
53
|
+
const dispatch = useCallback((action) => setStoredState(current => tabRouterReducer(resolveState(current), action)), [resolveState]);
|
|
54
|
+
const jumpTo = useCallback((name, params) => dispatch({ type: 'jumpTo', name, params }), [dispatch]);
|
|
55
|
+
const setParams = useCallback((params, key) => dispatch({ type: 'setParams', key, params }), [dispatch]);
|
|
42
56
|
const handle = useMemo(() => ({ jumpTo, setParams }), [jumpTo, setParams]);
|
|
43
57
|
useImperativeHandle(forwardedRef, () => handle, [handle]);
|
|
44
58
|
const focusedRoute = state.routes[state.index];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ISearchBarCommands } from '../core';
|
|
2
|
+
export declare function hostProps(props: Record<string, unknown>): (node: unknown) => void;
|
|
3
|
+
export declare function searchBarRef(ref: {
|
|
4
|
+
current: ISearchBarCommands | null;
|
|
5
|
+
} | undefined): (node: unknown) => (() => void) | void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Svelte attachments (`{@attach ...}`) for the react-native-screens native leaves.
|
|
2
|
+
//
|
|
3
|
+
// WHY THESE EXIST AT ALL: every react-native-screens Fabric view name is capitalized and
|
|
4
|
+
// un-hyphenated (RNSScreen, RNSScreenStack, RNSScreenStackHeaderConfig, RNSSearchBar, ...), so a
|
|
5
|
+
// literal `<RNSScreen>` in a Svelte template parses as a COMPONENT reference, not an element -
|
|
6
|
+
// the same wall @symbiote-native/slider's 'RNCSlider' leaf hit. Slider sidestepped it by mounting
|
|
7
|
+
// the whole leaf through the Descriptor bridge, which works because that leaf carries no live
|
|
8
|
+
// framework children. A stack screen DOES (the app's own screen component lives inside
|
|
9
|
+
// RNSScreenContentWrapper), so the bridge is not an option here.
|
|
10
|
+
//
|
|
11
|
+
// The way through is `<svelte:element this={'RNSScreen'}>`: it creates the element via
|
|
12
|
+
// `document.createElement(tag)` with a plain runtime string the compiler never inspects, and it
|
|
13
|
+
// accepts ordinary framework children. Its one catch is documented in svelte-adapter-dom-shim
|
|
14
|
+
// skill §15 - a dynamic tag compiles through Svelte's generic setAttribute/property-diffing
|
|
15
|
+
// codegen, NOT the custom-element `p=` property-SET path, so writing `p={bag}` as an ATTRIBUTE on
|
|
16
|
+
// it silently fails. An attachment bypasses that entirely: it is handed the raw element and
|
|
17
|
+
// assigns the property from plain JS, then re-runs whenever the props it read change. Verified
|
|
18
|
+
// against the real compiler + the real shim before this was built on.
|
|
19
|
+
import { dlog, isSymbioteNode } from '@symbiote-native/engine';
|
|
20
|
+
import { buildSearchBarHandle } from '../core/index.js';
|
|
21
|
+
// `unknown` rather than `Element`: the value Svelte hands an attachment is one of the DOM shim's
|
|
22
|
+
// own element classes, not a real DOM Element, and a function accepting `unknown` is still
|
|
23
|
+
// assignable everywhere Svelte expects `(element: Element) => ...`.
|
|
24
|
+
function isShimElement(value) {
|
|
25
|
+
return typeof value === 'object' && value !== null && 'engineNode' in value;
|
|
26
|
+
}
|
|
27
|
+
// The object-bag prop set, the same entry point every adapter's props go through (routeProp).
|
|
28
|
+
// Re-runs on every change of `props`, and the shim's own `p` setter diffs per key from there.
|
|
29
|
+
export function hostProps(props) {
|
|
30
|
+
return node => {
|
|
31
|
+
if (!isShimElement(node)) {
|
|
32
|
+
dlog('navigation: hostProps attached to a non-shim element, ignored');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
node.p = props;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// The Svelte twin of Angular's SearchBarRefDirective / React's and Vue's callback `ref` on the
|
|
39
|
+
// RNSSearchBar element: fills the app-supplied cell once the native node exists and clears it on
|
|
40
|
+
// teardown. buildSearchBarHandle's own getter is LAZY, so handing it the node here is safe even
|
|
41
|
+
// before the first commit.
|
|
42
|
+
export function searchBarRef(ref) {
|
|
43
|
+
return node => {
|
|
44
|
+
if (ref === undefined)
|
|
45
|
+
return;
|
|
46
|
+
if (!isShimElement(node)) {
|
|
47
|
+
ref.current = null;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const engineNode = node.engineNode;
|
|
51
|
+
if (!isSymbioteNode(engineNode)) {
|
|
52
|
+
dlog('navigation: search bar attachment ran before the shim went live');
|
|
53
|
+
ref.current = null;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
ref.current = buildSearchBarHandle(() => engineNode);
|
|
57
|
+
return () => {
|
|
58
|
+
ref.current = null;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// A shape-CHANGE-tolerant wrapper over @symbiote-native/svelte's Descriptor bridge.
|
|
2
|
+
//
|
|
3
|
+
// `mountDescriptorChildren` (svelte-adapter-dom-shim skill §19) is deliberately shape-stable: it
|
|
4
|
+
// builds each shim node once and only re-sets props thereafter, throwing if the tree shape ever
|
|
5
|
+
// changes, because every `core/components` render-*.ts produces a constant shape. The navigation
|
|
6
|
+
// package's tab bar is the exception - `renderTabBar` emits one child per registered route, and
|
|
7
|
+
// that count genuinely varies (it is 0 until the `<Tab.Screen>` markers have registered, and
|
|
8
|
+
// changes again if a screen is added or removed). Same situation @symbiote-native/slider's own
|
|
9
|
+
// steps-indicator overlay is in, and the same resolution: keep the fast path for the common
|
|
10
|
+
// "same shape, new prop values" case and rebuild only when the shape actually changed.
|
|
11
|
+
//
|
|
12
|
+
// The shape signature covers types and nesting, not just the top-level count, because a tab bar
|
|
13
|
+
// item's own children vary too (an item gains an icon wrapper only when it has an icon or badge).
|
|
14
|
+
import { mountDescriptorChildren } from '@symbiote-native/svelte/native-view-bridge';
|
|
15
|
+
const TEXT_CHILD_MARK = '#';
|
|
16
|
+
function shapeOf(children) {
|
|
17
|
+
return children
|
|
18
|
+
.map(child => typeof child === 'string' ? TEXT_CHILD_MARK : `${child.type}(${shapeOf(child.children)})`)
|
|
19
|
+
.join(',');
|
|
20
|
+
}
|
|
21
|
+
export function createDescriptorSubtreeSync() {
|
|
22
|
+
let mounted;
|
|
23
|
+
let shape;
|
|
24
|
+
return (host, children) => {
|
|
25
|
+
if (host === null)
|
|
26
|
+
return;
|
|
27
|
+
const nextShape = shapeOf(children);
|
|
28
|
+
if (mounted === undefined || nextShape !== shape) {
|
|
29
|
+
for (const child of host.children.slice())
|
|
30
|
+
host.removeChild(child);
|
|
31
|
+
mounted = mountDescriptorChildren(host, children);
|
|
32
|
+
shape = nextShape;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
mounted.update(children);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { IStyleProp, IViewStyle } from '@symbiote-native/engine';
|
|
3
|
+
import type { IDrawerDescriptorMap, IDrawerNavigatorHandle, IDrawerOptions, IDrawerRouterState } from '../../core';
|
|
4
|
+
export type IDrawerContentSlotProps = {
|
|
5
|
+
state: IDrawerRouterState;
|
|
6
|
+
descriptors: IDrawerDescriptorMap;
|
|
7
|
+
navigation: IDrawerNavigatorHandle;
|
|
8
|
+
};
|
|
9
|
+
export type IDrawerProps = IDrawerOptions & {
|
|
10
|
+
initialRouteName?: string;
|
|
11
|
+
drawerStyle?: IStyleProp<IViewStyle>;
|
|
12
|
+
children?: Snippet;
|
|
13
|
+
drawerContent?: Snippet<[IDrawerContentSlotProps]>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const Drawer: import("svelte").Component<import("./drawer-props").IDrawerProps, {
|
|
2
|
+
openDrawer: () => void;
|
|
3
|
+
closeDrawer: () => void;
|
|
4
|
+
toggleDrawer: () => void;
|
|
5
|
+
jumpTo: (name: string) => void;
|
|
6
|
+
}, ""> & {
|
|
7
|
+
Screen: import("svelte").Component<import("..").IDrawerScreenProps, {}, "">;
|
|
8
|
+
};
|
|
9
|
+
export type { IDrawerNavigatorHandle, IDrawerDescriptorMap } from '../../core';
|
|
10
|
+
export type { IDrawerContentSlotProps, IDrawerProps } from './drawer-props';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// `Drawer.Screen` alongside the standalone `DrawerScreen` export, matching React's and Vue's
|
|
2
|
+
// barrels - see ../stack/index.ts for why Object.assign is the right mechanism on a compiled
|
|
3
|
+
// Svelte component.
|
|
4
|
+
import DrawerImpl from './index.svelte';
|
|
5
|
+
import DrawerScreen from '../drawer-screen.svelte';
|
|
6
|
+
export const Drawer = Object.assign(DrawerImpl, { Screen: DrawerScreen });
|