@symbiote-native/navigation 2.0.3 → 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.
- package/README.md +42 -12
- package/build/angular/drawer/index.js +29 -13
- package/build/angular/index.d.ts +4 -4
- package/build/angular/injectors/inject-navigation.js +1 -1
- package/build/angular/navigation-scope.directive.js +1 -1
- package/build/angular/search-bar-ref.directive.js +1 -1
- package/build/angular/stack/index.d.ts +2 -0
- package/build/angular/stack/index.js +43 -9
- package/build/angular/tabs/index.d.ts +1 -1
- package/build/angular/tabs/index.js +40 -13
- package/build/core/drawer-options/index.js +19 -5
- package/build/core/drawer-router-state/index.js +8 -2
- package/build/core/guards.js +3 -1
- package/build/core/header-bar-buttons.js +14 -4
- package/build/core/index.d.ts +6 -5
- package/build/core/index.js +7 -3
- package/build/core/navigator-state/index.js +12 -4
- package/build/core/render-drawer.js +4 -1
- package/build/core/render-stack.js +13 -4
- package/build/core/screen-options.js +1 -1
- package/build/core/search-bar-commands.js +1 -1
- package/build/core/stack-router-state/index.d.ts +2 -0
- package/build/core/stack-router-state/index.js +44 -0
- package/build/core/state-persistence/index.js +6 -2
- package/build/core/tab-router-state/index.d.ts +1 -0
- package/build/core/tab-router-state/index.js +27 -2
- package/build/react/drawer/index.js +59 -26
- package/build/react/index.d.ts +7 -7
- package/build/react/index.js +2 -2
- package/build/react/stack/index.js +47 -10
- package/build/react/tabs/index.js +36 -12
- package/build/solid/drawer/index.d.ts +22 -0
- package/build/solid/drawer/index.js +394 -0
- package/build/solid/drawer-screen-props.d.ts +13 -0
- package/build/solid/drawer-screen-props.js +2 -0
- package/build/solid/host.d.ts +2 -0
- package/build/solid/host.js +23 -0
- package/build/solid/index.d.ts +19 -0
- package/build/solid/index.js +24 -0
- package/build/solid/linking.d.ts +3 -0
- package/build/solid/linking.js +55 -0
- package/build/solid/navigation-context.d.ts +13 -0
- package/build/solid/navigation-context.js +37 -0
- package/build/solid/primitives/create-focus-effect.d.ts +1 -0
- package/build/solid/primitives/create-focus-effect.js +30 -0
- package/build/solid/primitives/create-is-focused.d.ts +2 -0
- package/build/solid/primitives/create-is-focused.js +31 -0
- package/build/solid/primitives/create-navigation-state.d.ts +3 -0
- package/build/solid/primitives/create-navigation-state.js +30 -0
- package/build/solid/primitives/index.d.ts +12 -0
- package/build/solid/primitives/index.js +8 -0
- package/build/solid/primitives/use-drawer-navigation.d.ts +5 -0
- package/build/solid/primitives/use-drawer-navigation.js +15 -0
- package/build/solid/primitives/use-navigation.d.ts +8 -0
- package/build/solid/primitives/use-navigation.js +23 -0
- package/build/solid/primitives/use-route.d.ts +3 -0
- package/build/solid/primitives/use-route.js +14 -0
- package/build/solid/primitives/use-stack-navigation.d.ts +5 -0
- package/build/solid/primitives/use-stack-navigation.js +19 -0
- package/build/solid/primitives/use-tab-navigation.d.ts +5 -0
- package/build/solid/primitives/use-tab-navigation.js +15 -0
- package/build/solid/screen-props.d.ts +19 -0
- package/build/solid/screen-props.js +9 -0
- package/build/solid/screen-registry.d.ts +31 -0
- package/build/solid/screen-registry.js +86 -0
- package/build/solid/screen.d.ts +6 -0
- package/build/solid/screen.js +69 -0
- package/build/solid/stack/index.d.ts +15 -0
- package/build/solid/stack/index.js +348 -0
- package/build/solid/tab-screen-props.d.ts +13 -0
- package/build/solid/tab-screen-props.js +3 -0
- package/build/solid/tabs/index.d.ts +14 -0
- package/build/solid/tabs/index.js +228 -0
- package/build/svelte/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 +39 -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 +559 -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 +25 -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 +27 -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 +26 -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 +261 -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 +205 -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 +269 -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/composables/use-navigation.js +5 -1
- package/build/vue/drawer/index.js +36 -13
- package/build/vue/index.d.ts +3 -3
- package/build/vue/navigation-context.js +1 -1
- package/build/vue/stack/index.js +43 -18
- package/build/vue/tabs/index.js +65 -39
- package/build-ngc/angular/drawer/index.js +40 -18
- package/build-ngc/angular/drawer/index.js.map +1 -1
- package/build-ngc/angular/drawer-screen.directive.js +3 -3
- package/build-ngc/angular/index.d.ts +4 -4
- package/build-ngc/angular/index.js.map +1 -1
- package/build-ngc/angular/injectors/inject-focus-effect.js.map +1 -1
- package/build-ngc/angular/injectors/inject-is-focused.js.map +1 -1
- package/build-ngc/angular/injectors/inject-navigation-state.js.map +1 -1
- package/build-ngc/angular/injectors/inject-navigation.js +1 -1
- package/build-ngc/angular/injectors/inject-navigation.js.map +1 -1
- package/build-ngc/angular/linking.js.map +1 -1
- package/build-ngc/angular/navigation-context.service.js +3 -3
- package/build-ngc/angular/navigation-context.service.js.map +1 -1
- package/build-ngc/angular/navigation-scope.directive.js +4 -4
- package/build-ngc/angular/navigation-scope.directive.js.map +1 -1
- package/build-ngc/angular/screen.directive.js +3 -3
- package/build-ngc/angular/screen.directive.js.map +1 -1
- package/build-ngc/angular/search-bar-ref.directive.js +4 -4
- package/build-ngc/angular/search-bar-ref.directive.js.map +1 -1
- package/build-ngc/angular/stack/index.d.ts +2 -0
- package/build-ngc/angular/stack/index.js +52 -14
- package/build-ngc/angular/stack/index.js.map +1 -1
- package/build-ngc/angular/tab-screen.directive.js +3 -3
- package/build-ngc/angular/tab-screen.directive.js.map +1 -1
- package/build-ngc/angular/tabs/index.d.ts +1 -1
- package/build-ngc/angular/tabs/index.js +43 -16
- package/build-ngc/angular/tabs/index.js.map +1 -1
- package/build-ngc/core/constants.js.map +1 -1
- package/build-ngc/core/drawer-options/index.js +19 -5
- package/build-ngc/core/drawer-options/index.js.map +1 -1
- package/build-ngc/core/drawer-router-state/index.js +8 -2
- package/build-ngc/core/drawer-router-state/index.js.map +1 -1
- package/build-ngc/core/guards.js +3 -1
- package/build-ngc/core/guards.js.map +1 -1
- package/build-ngc/core/header-bar-buttons.js +14 -4
- package/build-ngc/core/header-bar-buttons.js.map +1 -1
- package/build-ngc/core/index.d.ts +6 -5
- package/build-ngc/core/index.js +7 -3
- package/build-ngc/core/index.js.map +1 -1
- package/build-ngc/core/linking-config/index.js.map +1 -1
- package/build-ngc/core/navigation-events/index.js.map +1 -1
- package/build-ngc/core/navigator-handles/index.js.map +1 -1
- package/build-ngc/core/navigator-state/index.js +12 -4
- package/build-ngc/core/navigator-state/index.js.map +1 -1
- package/build-ngc/core/render-drawer.js +4 -1
- package/build-ngc/core/render-drawer.js.map +1 -1
- package/build-ngc/core/render-stack.js +13 -4
- package/build-ngc/core/render-stack.js.map +1 -1
- package/build-ngc/core/render-tabs.js.map +1 -1
- package/build-ngc/core/screen-options.js +1 -1
- package/build-ngc/core/screen-options.js.map +1 -1
- package/build-ngc/core/search-bar-commands.js +1 -1
- package/build-ngc/core/search-bar-commands.js.map +1 -1
- package/build-ngc/core/stack-router-state/index.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/state-persistence/index.js +6 -2
- package/build-ngc/core/state-persistence/index.js.map +1 -1
- package/build-ngc/core/tab-router-state/index.d.ts +1 -0
- package/build-ngc/core/tab-router-state/index.js +27 -2
- package/build-ngc/core/tab-router-state/index.js.map +1 -1
- package/package.json +44 -16
- package/src/angular/drawer/index.ts +75 -29
- package/src/angular/index.ts +20 -4
- package/src/angular/injectors/inject-focus-effect.ts +8 -2
- package/src/angular/injectors/inject-is-focused.ts +6 -4
- package/src/angular/injectors/inject-navigation-state.ts +7 -4
- package/src/angular/injectors/inject-navigation.ts +12 -3
- package/src/angular/linking.ts +4 -1
- package/src/angular/navigation-context.service.ts +5 -2
- package/src/angular/navigation-scope.directive.ts +6 -2
- package/src/angular/screen.directive.ts +13 -3
- package/src/angular/search-bar-ref.directive.ts +10 -2
- package/src/angular/stack/index.ts +91 -24
- package/src/angular/tab-screen.directive.ts +3 -1
- package/src/angular/tabs/index.ts +54 -16
- package/src/core/constants.ts +6 -3
- package/src/core/drawer-options/index.ts +40 -13
- package/src/core/drawer-router-state/index.ts +11 -3
- package/src/core/guards.ts +3 -1
- package/src/core/header-bar-buttons.ts +41 -12
- package/src/core/index.ts +29 -5
- package/src/core/linking-config/index.ts +31 -10
- package/src/core/navigation-events/index.ts +8 -2
- package/src/core/navigator-handles/index.ts +8 -3
- package/src/core/navigator-props.ts +13 -4
- package/src/core/navigator-state/index.ts +24 -7
- package/src/core/render-drawer.ts +24 -9
- package/src/core/render-stack.ts +52 -13
- package/src/core/render-tabs.ts +24 -6
- package/src/core/screen-options.ts +15 -5
- package/src/core/search-bar-commands.ts +12 -3
- package/src/core/stack-router-state/index.ts +52 -0
- package/src/core/state-persistence/index.ts +13 -3
- package/src/core/tab-router-state/index.ts +35 -3
- package/src/react/drawer/index.ts +409 -330
- package/src/react/drawer-screen.ts +8 -2
- package/src/react/hooks/use-focus-effect.ts +8 -2
- package/src/react/hooks/use-is-focused.ts +6 -2
- package/src/react/hooks/use-navigation.ts +8 -2
- package/src/react/index.ts +37 -7
- package/src/react/linking.ts +4 -1
- package/src/react/navigation-context.ts +6 -2
- package/src/react/screen.ts +12 -3
- package/src/react/stack/index.ts +297 -220
- package/src/react/tab-screen.ts +3 -1
- package/src/react/tabs/index.ts +202 -139
- package/src/solid/drawer/index.ts +570 -0
- package/src/solid/drawer-screen-props.ts +24 -0
- package/src/solid/host.ts +29 -0
- package/src/solid/index.ts +87 -0
- package/src/solid/linking.ts +74 -0
- package/src/solid/navigation-context.ts +56 -0
- package/src/solid/primitives/create-focus-effect.ts +41 -0
- package/src/solid/primitives/create-is-focused.ts +41 -0
- package/src/solid/primitives/create-navigation-state.ts +44 -0
- package/src/solid/primitives/index.ts +12 -0
- package/src/solid/primitives/use-drawer-navigation.ts +25 -0
- package/src/solid/primitives/use-navigation.ts +44 -0
- package/src/solid/primitives/use-route.ts +18 -0
- package/src/solid/primitives/use-stack-navigation.ts +29 -0
- package/src/solid/primitives/use-tab-navigation.ts +25 -0
- package/src/solid/screen-props.ts +51 -0
- package/src/solid/screen-registry.ts +159 -0
- package/src/solid/screen.ts +80 -0
- package/src/solid/stack/index.ts +528 -0
- package/src/solid/tab-screen-props.ts +21 -0
- package/src/solid/tabs/index.ts +327 -0
- package/src/svelte/attachments.ts +70 -0
- package/src/svelte/descriptor-subtree.ts +51 -0
- package/src/svelte/drawer/drawer-props.ts +28 -0
- package/src/svelte/drawer/index.svelte +559 -0
- package/src/svelte/drawer/index.ts +10 -0
- package/src/svelte/drawer-screen-props.ts +26 -0
- package/src/svelte/drawer-screen.svelte +25 -0
- package/src/svelte/fabric-tree.test-helper.ts +95 -0
- package/src/svelte/index.ts +83 -0
- package/src/svelte/linking.svelte.ts +72 -0
- package/src/svelte/navigation-context.ts +62 -0
- package/src/svelte/navigation-scope.svelte +27 -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 +31 -0
- package/src/svelte/runes/use-focus-effect.svelte.ts +41 -0
- package/src/svelte/runes/use-is-focused.svelte.ts +38 -0
- package/src/svelte/runes/use-navigation-state.svelte.ts +48 -0
- package/src/svelte/runes/use-navigation.svelte.ts +49 -0
- package/src/svelte/runes/use-route.svelte.ts +16 -0
- package/src/svelte/runes/use-stack-navigation.svelte.ts +34 -0
- package/src/svelte/runes/use-tab-navigation.svelte.ts +29 -0
- package/src/svelte/screen-props.ts +50 -0
- package/src/svelte/screen-registry.ts +98 -0
- package/src/svelte/screen.svelte +28 -0
- package/src/svelte/stack/index.svelte +261 -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 +205 -0
- package/src/svelte/svelte-ambient.d.ts +6 -0
- package/src/svelte/svelte-compile.test-helper.ts +147 -0
- package/src/svelte/tab-screen-props.ts +22 -0
- package/src/svelte/tab-screen.svelte +26 -0
- package/src/svelte/tabs/index.svelte +269 -0
- package/src/svelte/tabs/index.ts +10 -0
- package/src/svelte/tabs/tab-props.ts +13 -0
- package/src/vue/composables/use-navigation-state.ts +7 -4
- package/src/vue/composables/use-navigation.ts +13 -3
- package/src/vue/drawer/index.ts +128 -41
- package/src/vue/drawer-screen.ts +12 -5
- package/src/vue/index.ts +15 -3
- package/src/vue/linking.ts +7 -2
- package/src/vue/navigation-context.ts +17 -5
- package/src/vue/screen.ts +7 -2
- package/src/vue/stack/index.ts +108 -43
- package/src/vue/tab-screen.ts +3 -1
- package/src/vue/tabs/index.ts +109 -68
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { INavigatorPlatform } from '../../core';
|
|
3
|
+
|
|
4
|
+
// backTitleVisible defaults to `true` on both platforms per the codegen spec's own default
|
|
5
|
+
// (CT.WithDefault<boolean, 'true'>) - no ios/android divergence in v1 scope, so a single
|
|
6
|
+
// constant stands in for the per-platform injection point ISliderPlatform-style adapters use
|
|
7
|
+
// elsewhere.
|
|
8
|
+
const NAVIGATOR_PLATFORM: INavigatorPlatform = {
|
|
9
|
+
defaultHeaderBackTitleVisible: true,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// react-native-screens' RNSScreenStackHeaderConfig.mm requires every header child to be an
|
|
13
|
+
// RNSScreenStackHeaderSubview; `type: 'searchBar'` is how it knows which slot this one fills.
|
|
14
|
+
const HEADER_SUBVIEW_PROPS: Record<string, unknown> = { type: 'searchBar' };
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<script lang="ts">
|
|
18
|
+
// One mounted route's native chrome. Split out of index.svelte so each route owns its own plan
|
|
19
|
+
// derivation and its own attachments, torn down by an ordinary component unmount when the route
|
|
20
|
+
// is popped - the Svelte equivalent of Vue's per-route render-loop closure.
|
|
21
|
+
//
|
|
22
|
+
// Every react-native-screens view here goes through `<svelte:element this={'RNSScreen'}>`
|
|
23
|
+
// rather than a literal tag: their Fabric names are capitalized and un-hyphenated, so a literal
|
|
24
|
+
// tag would parse as a COMPONENT reference in a Svelte template. Their props ride an
|
|
25
|
+
// `{@attach hostProps(...)}` attachment rather than an attribute, because a dynamic tag
|
|
26
|
+
// compiles through Svelte's generic setAttribute path and never the custom-element property-SET
|
|
27
|
+
// path the object bag depends on - see ../attachments.ts for the full reasoning.
|
|
28
|
+
//
|
|
29
|
+
// Whitespace between sibling tags in the per-route tree is inert: the shim drops a
|
|
30
|
+
// whitespace-only text node whose parent takes no raw text, so it never becomes an RCTRawText
|
|
31
|
+
// child of a react-native-screens view (dom-shim/text.ts, svelte-adapter-dom-shim skill §16b).
|
|
32
|
+
import { Platform, dlog } from '@symbiote-native/engine';
|
|
33
|
+
import {
|
|
34
|
+
NAVIGATION_EVENT_BLUR,
|
|
35
|
+
NAVIGATION_EVENT_FOCUS,
|
|
36
|
+
RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME,
|
|
37
|
+
RNS_SCREEN_STACK_HEADER_CONFIG_VIEW_NAME,
|
|
38
|
+
RNS_SCREEN_STACK_HEADER_SUBVIEW_VIEW_NAME,
|
|
39
|
+
RNS_SCREEN_STACK_VIEW_NAME,
|
|
40
|
+
RNS_SCREEN_VIEW_NAME,
|
|
41
|
+
RNS_SEARCH_BAR_VIEW_NAME,
|
|
42
|
+
SCREEN_ON_APPEAR,
|
|
43
|
+
SCREEN_ON_DISAPPEAR,
|
|
44
|
+
SCREEN_ON_DISMISSED,
|
|
45
|
+
SCREEN_ON_HEADER_BACK_BUTTON_CLICKED,
|
|
46
|
+
SCREEN_ON_WILL_APPEAR,
|
|
47
|
+
SCREEN_ON_WILL_DISAPPEAR,
|
|
48
|
+
buildSearchBarPassthrough,
|
|
49
|
+
resolveScreenRenderPlan,
|
|
50
|
+
} from '../../core';
|
|
51
|
+
import type { IScreenRenderPlan } from '../../core';
|
|
52
|
+
import { hostProps, searchBarRef } from '../attachments';
|
|
53
|
+
import NavigationScope from '../navigation-scope.svelte';
|
|
54
|
+
import type { INavigationScopeValue } from '../navigation-context';
|
|
55
|
+
import type { IStackScreenProps } from './stack-props';
|
|
56
|
+
|
|
57
|
+
let {
|
|
58
|
+
route,
|
|
59
|
+
index,
|
|
60
|
+
routeCount,
|
|
61
|
+
options,
|
|
62
|
+
navigation,
|
|
63
|
+
emitter,
|
|
64
|
+
parentScope,
|
|
65
|
+
// Destructured under a capitalized name because a Svelte template resolves a component tag
|
|
66
|
+
// only from a capitalized identifier.
|
|
67
|
+
component: ScreenComponent,
|
|
68
|
+
onPopRequested,
|
|
69
|
+
}: IStackScreenProps = $props();
|
|
70
|
+
|
|
71
|
+
const searchBarOptions = $derived(options.headerSearchBarOptions);
|
|
72
|
+
|
|
73
|
+
const plan = $derived.by<IScreenRenderPlan>(() =>
|
|
74
|
+
resolveScreenRenderPlan({
|
|
75
|
+
screenId: route.key,
|
|
76
|
+
index,
|
|
77
|
+
routeCount,
|
|
78
|
+
options,
|
|
79
|
+
platform: NAVIGATOR_PLATFORM,
|
|
80
|
+
isAndroid: Platform.OS === 'android',
|
|
81
|
+
screenPassthrough: {
|
|
82
|
+
[SCREEN_ON_DISMISSED]: onPopRequested,
|
|
83
|
+
[SCREEN_ON_HEADER_BACK_BUTTON_CLICKED]: onPopRequested,
|
|
84
|
+
// onAppear/onDisappear are the definitive visibility boundary (post-transition-
|
|
85
|
+
// animation), so 'focus'/'blur' fire exactly once per transition; onWillAppear/
|
|
86
|
+
// onWillDisappear fire BEFORE the animation runs, so wiring them to emit() too would
|
|
87
|
+
// double-invoke useFocusEffect per transition - they only get a debug log here.
|
|
88
|
+
[SCREEN_ON_WILL_APPEAR]: () =>
|
|
89
|
+
dlog(`Stack: route "${route.name}" will appear at t=${Date.now()}`),
|
|
90
|
+
[SCREEN_ON_APPEAR]: () => {
|
|
91
|
+
dlog(
|
|
92
|
+
`Stack: route "${route.name}" appeared (focus) at t=${Date.now()}`,
|
|
93
|
+
);
|
|
94
|
+
emitter.emit(NAVIGATION_EVENT_FOCUS);
|
|
95
|
+
},
|
|
96
|
+
[SCREEN_ON_WILL_DISAPPEAR]: () =>
|
|
97
|
+
dlog(
|
|
98
|
+
`Stack: route "${route.name}" will disappear at t=${Date.now()}`,
|
|
99
|
+
),
|
|
100
|
+
[SCREEN_ON_DISAPPEAR]: () => {
|
|
101
|
+
dlog(
|
|
102
|
+
`Stack: route "${route.name}" disappeared (blur) at t=${Date.now()}`,
|
|
103
|
+
);
|
|
104
|
+
emitter.emit(NAVIGATION_EVENT_BLUR);
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
// The imperative SearchBarCommands ref rides its OWN attachment on the RNSSearchBar leaf
|
|
108
|
+
// below (../attachments.ts), never this passthrough map - same split Angular uses, so no
|
|
109
|
+
// `ref` key can leak through to Fabric as a real prop.
|
|
110
|
+
searchBarPassthrough: searchBarOptions
|
|
111
|
+
? buildSearchBarPassthrough(searchBarOptions, message =>
|
|
112
|
+
dlog(`Stack: route "${route.name}" ${message}`),
|
|
113
|
+
)
|
|
114
|
+
: undefined,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Investigation instrumentation (flicker-on-focus bug): the actual timing/z-order-relevant
|
|
119
|
+
// values resolved onto the native RNSScreen, once per mounted route - rules a stackAnimation/
|
|
120
|
+
// transitionDuration mismatch against react-native-screens' own native default in or out. Kept
|
|
121
|
+
// behind DEBUG, never removed.
|
|
122
|
+
let hasLoggedScreenProps = false;
|
|
123
|
+
$effect(() => {
|
|
124
|
+
const screenProps = plan.screenProps;
|
|
125
|
+
if (hasLoggedScreenProps) return;
|
|
126
|
+
hasLoggedScreenProps = true;
|
|
127
|
+
dlog(
|
|
128
|
+
`Stack: route "${route.name}" resolved screen props ` +
|
|
129
|
+
`stackAnimation=${String(screenProps.stackAnimation)} ` +
|
|
130
|
+
`stackPresentation=${String(screenProps.stackPresentation)} ` +
|
|
131
|
+
`transitionDuration=${String(screenProps.transitionDuration)} ` +
|
|
132
|
+
`gestureEnabled=${String(screenProps.gestureEnabled)} at t=${Date.now()}`,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// A modal/formSheet screen has no UINavigationController of its own on iOS - nest an inner
|
|
137
|
+
// RNSScreenStack/RNSScreen purely to host the native header bar (see isHeaderInModal's comment
|
|
138
|
+
// in core/render-stack.ts). Skipping this leaves RNSScreenStackHeaderConfig with no navigation
|
|
139
|
+
// controller to attach to, so the header silently never renders. activityState mirrors the
|
|
140
|
+
// outer screen's own value - RNSScreen.mm treats an unset/inactive nested screen as not yet
|
|
141
|
+
// pushed, leaving it parked at its pre-push transition position.
|
|
142
|
+
const innerStackProps = $derived<Record<string, unknown>>({
|
|
143
|
+
style: plan.innerStackStyle,
|
|
144
|
+
});
|
|
145
|
+
const innerScreenProps = $derived<Record<string, unknown>>({
|
|
146
|
+
style: plan.innerScreenStyle,
|
|
147
|
+
activityState: plan.activityState,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const scopeValue = $derived<INavigationScopeValue>({
|
|
151
|
+
route,
|
|
152
|
+
navigation,
|
|
153
|
+
emitter,
|
|
154
|
+
parent: parentScope,
|
|
155
|
+
});
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
{#snippet chrome()}
|
|
159
|
+
<svelte:element
|
|
160
|
+
this={RNS_SCREEN_STACK_HEADER_CONFIG_VIEW_NAME}
|
|
161
|
+
{@attach hostProps(plan.headerConfig.props)}
|
|
162
|
+
>
|
|
163
|
+
{#if plan.searchBarProps !== undefined}
|
|
164
|
+
<svelte:element
|
|
165
|
+
this={RNS_SCREEN_STACK_HEADER_SUBVIEW_VIEW_NAME}
|
|
166
|
+
{@attach hostProps(HEADER_SUBVIEW_PROPS)}
|
|
167
|
+
>
|
|
168
|
+
<svelte:element
|
|
169
|
+
this={RNS_SEARCH_BAR_VIEW_NAME}
|
|
170
|
+
{@attach hostProps(plan.searchBarProps)}
|
|
171
|
+
{@attach searchBarRef(searchBarOptions?.ref)}
|
|
172
|
+
/>
|
|
173
|
+
</svelte:element>
|
|
174
|
+
{/if}
|
|
175
|
+
</svelte:element>
|
|
176
|
+
<svelte:element
|
|
177
|
+
this={RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME}
|
|
178
|
+
{@attach hostProps(plan.contentWrapperProps)}
|
|
179
|
+
>
|
|
180
|
+
<NavigationScope value={scopeValue}>
|
|
181
|
+
<ScreenComponent />
|
|
182
|
+
</NavigationScope>
|
|
183
|
+
</svelte:element>
|
|
184
|
+
{/snippet}
|
|
185
|
+
|
|
186
|
+
<svelte:element
|
|
187
|
+
this={plan.screenViewName}
|
|
188
|
+
{@attach hostProps(plan.screenProps)}
|
|
189
|
+
>
|
|
190
|
+
{#if plan.inModal}
|
|
191
|
+
<svelte:element
|
|
192
|
+
this={RNS_SCREEN_STACK_VIEW_NAME}
|
|
193
|
+
{@attach hostProps(innerStackProps)}
|
|
194
|
+
>
|
|
195
|
+
<svelte:element
|
|
196
|
+
this={RNS_SCREEN_VIEW_NAME}
|
|
197
|
+
{@attach hostProps(innerScreenProps)}
|
|
198
|
+
>
|
|
199
|
+
{@render chrome()}
|
|
200
|
+
</svelte:element>
|
|
201
|
+
</svelte:element>
|
|
202
|
+
{:else}
|
|
203
|
+
{@render chrome()}
|
|
204
|
+
{/if}
|
|
205
|
+
</svelte:element>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Pulls in svelte's ambient global declarations ($state/$effect/...) for this package's tsc
|
|
2
|
+
// program. adapters/svelte gets these for free because render.ts does a real value import from
|
|
3
|
+
// 'svelte' (any file in a program importing a module loads its ambient globals for the whole
|
|
4
|
+
// program); this package has no such import, so a triple-slash reference does the same job
|
|
5
|
+
// explicitly rather than relying on an incidental side-effect.
|
|
6
|
+
/// <reference types="svelte" />
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// The compile-and-load harness every `*.smoke.test.ts` in this folder uses.
|
|
2
|
+
//
|
|
3
|
+
// There is no `.svelte`-aware bundler wired into this repo's vitest (svelte-adapter-dom-shim
|
|
4
|
+
// skill §15, a deliberate choice), so a test that needs to RUN a component compiles the real
|
|
5
|
+
// source with `svelte/compiler` itself and dynamic-`import()`s the output. Three rules fall out
|
|
6
|
+
// of that, all of them learned the hard way in the adapter's own smokes:
|
|
7
|
+
//
|
|
8
|
+
// 1. The compiled file must sit NEXT TO the real source, because its own relative imports
|
|
9
|
+
// (`./stack-screen.svelte`, `../navigation-context`) resolve from wherever the compiled FILE
|
|
10
|
+
// lives, not from where the source did.
|
|
11
|
+
// 2. Every `.svelte` specifier inside the compiled output has to be rewritten to the compiled
|
|
12
|
+
// twin, recursively - a navigator pulls in stack-screen.svelte, which pulls in
|
|
13
|
+
// navigation-scope.svelte.
|
|
14
|
+
// 3. Node's `import()` caches by path, so re-writing new content to a path a previous test
|
|
15
|
+
// already imported hands back the STALE module. Every harness instance therefore stamps its
|
|
16
|
+
// own id into the filenames.
|
|
17
|
+
//
|
|
18
|
+
// `aliases` covers the one case relative rewriting cannot: `@symbiote-native/svelte`'s main
|
|
19
|
+
// barrel re-exports real `.svelte` sources, which Vite's plain (svelte-plugin-free) transform
|
|
20
|
+
// cannot parse at all - so a test that needs something from it points the specifier at a module
|
|
21
|
+
// it compiled itself instead.
|
|
22
|
+
|
|
23
|
+
import { compile } from 'svelte/compiler';
|
|
24
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
25
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { pathToFileURL } from 'node:url';
|
|
27
|
+
// The real Metro pipeline's own `.svelte.ts` compile step (TS-strip, then `compileModule` to
|
|
28
|
+
// desugar the runes). Reused rather than reimplemented, so the runes in ./runes are exercised
|
|
29
|
+
// through the ACTUAL shipped compile path - a bare, uncompiled `$state`/`$effect` call throws
|
|
30
|
+
// `rune_outside_svelte` at runtime. Default-imported because it is a `.cjs` module.
|
|
31
|
+
import metroSvelteTransformer from '@symbiote-native/svelte/metro-svelte-transformer';
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
compileSvelteModuleFile,
|
|
35
|
+
}: {
|
|
36
|
+
compileSvelteModuleFile: (source: string, filename: string) => string;
|
|
37
|
+
} = metroSvelteTransformer;
|
|
38
|
+
|
|
39
|
+
const COMPILE_OPTIONS = {
|
|
40
|
+
generate: 'client',
|
|
41
|
+
fragments: 'tree',
|
|
42
|
+
css: 'external',
|
|
43
|
+
} as const;
|
|
44
|
+
const SVELTE_SPECIFIER_PATTERN = /(['"])(\.{1,2}\/[^'"]*\.svelte)\1/g;
|
|
45
|
+
|
|
46
|
+
export type ISvelteHarness = {
|
|
47
|
+
// Compile a real `.svelte` file plus everything it imports; returns the compiled entry path.
|
|
48
|
+
compileFile(sveltePath: string): string;
|
|
49
|
+
// Compile an inline fixture written into `dir`, so its own relative imports resolve from there.
|
|
50
|
+
compileSource(dir: string, name: string, source: string): string;
|
|
51
|
+
cleanup(): void;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function createSvelteHarness(
|
|
55
|
+
id: string,
|
|
56
|
+
aliases: Readonly<Record<string, string>> = {},
|
|
57
|
+
): ISvelteHarness {
|
|
58
|
+
const written: string[] = [];
|
|
59
|
+
const compiledBySource = new Map<string, string>();
|
|
60
|
+
|
|
61
|
+
function outPathFor(sveltePath: string): string {
|
|
62
|
+
const stem = basename(sveltePath).replace(/\./g, '-');
|
|
63
|
+
return join(dirname(sveltePath), `.smoke-compiled-${id}-${stem}.mjs`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// `./` is prepended unless the path already walks up: every compiled file's basename STARTS
|
|
67
|
+
// with a dot (`.smoke-compiled-...`), so a bare "starts with '.'" check would leave it looking
|
|
68
|
+
// like a bare package specifier to the resolver.
|
|
69
|
+
function relativeSpecifier(fromFile: string, target: string): string {
|
|
70
|
+
const specifier = relative(dirname(fromFile), target);
|
|
71
|
+
return specifier.startsWith('..') ? specifier : `./${specifier}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rewrite(code: string, outPath: string): string {
|
|
75
|
+
let rewritten = code.replace(
|
|
76
|
+
SVELTE_SPECIFIER_PATTERN,
|
|
77
|
+
(_match, quote: string, spec: string) => {
|
|
78
|
+
const childSource = resolve(dirname(outPath), spec);
|
|
79
|
+
const childOut = compileFile(childSource);
|
|
80
|
+
return `${quote}${relativeSpecifier(outPath, childOut)}${quote}`;
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
for (const [specifier, target] of Object.entries(aliases)) {
|
|
84
|
+
rewritten = rewritten
|
|
85
|
+
.split(`'${specifier}'`)
|
|
86
|
+
.join(`'${relativeSpecifier(outPath, target)}'`);
|
|
87
|
+
rewritten = rewritten
|
|
88
|
+
.split(`"${specifier}"`)
|
|
89
|
+
.join(`"${relativeSpecifier(outPath, target)}"`);
|
|
90
|
+
}
|
|
91
|
+
return rewritten;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function emit(
|
|
95
|
+
sveltePath: string,
|
|
96
|
+
source: string,
|
|
97
|
+
isRuneModule: boolean,
|
|
98
|
+
): string {
|
|
99
|
+
const outPath = outPathFor(sveltePath);
|
|
100
|
+
// Registered BEFORE compiling so a cycle between two components terminates.
|
|
101
|
+
compiledBySource.set(sveltePath, outPath);
|
|
102
|
+
written.push(outPath);
|
|
103
|
+
const filename = basename(sveltePath);
|
|
104
|
+
const code = isRuneModule
|
|
105
|
+
? compileSvelteModuleFile(source, `${filename}.ts`)
|
|
106
|
+
: compile(source, { ...COMPILE_OPTIONS, filename }).js.code;
|
|
107
|
+
writeFileSync(outPath, rewrite(code, outPath));
|
|
108
|
+
return outPath;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// A `./x.svelte` specifier is a COMPONENT when `x.svelte` exists on disk and a rune MODULE
|
|
112
|
+
// when `x.svelte.ts` does - the same two-way split the Metro transformer makes on the real
|
|
113
|
+
// filename.
|
|
114
|
+
function compileFile(sveltePath: string): string {
|
|
115
|
+
const cached = compiledBySource.get(sveltePath);
|
|
116
|
+
if (cached !== undefined) return cached;
|
|
117
|
+
const runeModulePath = `${sveltePath}.ts`;
|
|
118
|
+
if (!existsSync(sveltePath) && existsSync(runeModulePath)) {
|
|
119
|
+
return emit(sveltePath, readFileSync(runeModulePath, 'utf8'), true);
|
|
120
|
+
}
|
|
121
|
+
return emit(sveltePath, readFileSync(sveltePath, 'utf8'), false);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
compileFile,
|
|
126
|
+
compileSource(dir, name, source) {
|
|
127
|
+
return emit(join(dir, `${name}.svelte`), source, false);
|
|
128
|
+
},
|
|
129
|
+
cleanup() {
|
|
130
|
+
for (const file of written) rmSync(file, { force: true });
|
|
131
|
+
written.length = 0;
|
|
132
|
+
compiledBySource.clear();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The compiled module's default export, narrowed without a cast so a test never silently mounts
|
|
138
|
+
// `undefined`.
|
|
139
|
+
export async function loadComponent(compiledPath: string): Promise<unknown> {
|
|
140
|
+
// pathToFileURL rather than a `file://${...}` template: the literal form makes Vite's import
|
|
141
|
+
// analyzer log an "Invalid file URL" warning for the un-substituted placeholder.
|
|
142
|
+
const mod: unknown = await import(pathToFileURL(compiledPath).href);
|
|
143
|
+
if (mod === null || typeof mod !== 'object' || !('default' in mod)) {
|
|
144
|
+
throw new Error(`${compiledPath} produced no default export`);
|
|
145
|
+
}
|
|
146
|
+
return mod.default;
|
|
147
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Tab.Screen's public prop surface - mirrors screen-props.ts minus the stack-only concepts (no
|
|
2
|
+
// push/pop lifecycle, no header search bar). Split out of `tab-screen.svelte` for the same
|
|
3
|
+
// tsc-cannot-see-a-`.svelte`-named-export reason screen-props.ts documents.
|
|
4
|
+
|
|
5
|
+
import type { Component } from 'svelte';
|
|
6
|
+
import type { IRoute, ITabNavigatorHandle, ITabOptions } from '../core';
|
|
7
|
+
|
|
8
|
+
export type ITabScreenOptionsArgs = {
|
|
9
|
+
route: IRoute<unknown>;
|
|
10
|
+
navigation: ITabNavigatorHandle;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ITabScreenOptionsResolver = (
|
|
14
|
+
args: ITabScreenOptionsArgs,
|
|
15
|
+
) => ITabOptions;
|
|
16
|
+
|
|
17
|
+
export type ITabScreenProps = {
|
|
18
|
+
name: string;
|
|
19
|
+
component: Component;
|
|
20
|
+
options?: ITabOptions | ITabScreenOptionsResolver;
|
|
21
|
+
initialParams?: unknown;
|
|
22
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Tab.Screen: a declarative marker that paints nothing, registered through the same
|
|
3
|
+
// context-based collector Stack.Screen uses (screen-registry.ts). The `'tab'` kind tag is what
|
|
4
|
+
// stops it from silently registering with an ancestor Stack/Drawer when it is written in the
|
|
5
|
+
// wrong place. See CLAUDE.md <third_party_rn_packages_are_react_only> - this and Tab import
|
|
6
|
+
// nothing from react-native.
|
|
7
|
+
import { collectScreen } from './screen-registry';
|
|
8
|
+
import type { ITabScreenProps } from './tab-screen-props';
|
|
9
|
+
|
|
10
|
+
let { name, component, options, initialParams }: ITabScreenProps = $props();
|
|
11
|
+
|
|
12
|
+
collectScreen('tab', {
|
|
13
|
+
get name(): ITabScreenProps['name'] {
|
|
14
|
+
return name;
|
|
15
|
+
},
|
|
16
|
+
get component(): ITabScreenProps['component'] {
|
|
17
|
+
return component;
|
|
18
|
+
},
|
|
19
|
+
get options(): ITabScreenProps['options'] {
|
|
20
|
+
return options;
|
|
21
|
+
},
|
|
22
|
+
get initialParams(): unknown {
|
|
23
|
+
return initialParams;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
</script>
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
// Tab, the Svelte lifecycle half. The focused-index router (tab-router-state) and the tab-bar
|
|
3
|
+
// Descriptor builder (render-tabs) live in @symbiote-native/navigation core, shared verbatim
|
|
4
|
+
// with the React/Vue/Angular entries; here Svelte supplies the lifecycle - `$state.raw` for the
|
|
5
|
+
// router (its twin of useReducer), a module counter for route-key generation, `export
|
|
6
|
+
// function`s for the jumpTo/setParams handle - plus the Descriptor bridge for the tab-bar leaf.
|
|
7
|
+
// Unlike Stack, a bottom-tabs bar is a PURE-JS UI: it paints ordinary `symbiote-view`/
|
|
8
|
+
// `symbiote-text` primitives via the shared render fn, so there is no react-native-screens
|
|
9
|
+
// ViewConfig to register here - Tab needs no `../../register` import.
|
|
10
|
+
//
|
|
11
|
+
// Screens are discovered through the context collector (../screen-registry.ts), not by reading
|
|
12
|
+
// `children` - see stack/index.svelte's header for why, and ../registry-host.ts for where the
|
|
13
|
+
// markers are rendered.
|
|
14
|
+
let tabInstanceCounter = 0;
|
|
15
|
+
|
|
16
|
+
const TAB_ROOT_PROPS: Record<string, unknown> = { style: { flex: 1 } };
|
|
17
|
+
const TAB_CONTENT_PROPS: Record<string, unknown> = { style: { flex: 1 } };
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import type { Component } from 'svelte';
|
|
22
|
+
import { onDestroy, tick } from 'svelte';
|
|
23
|
+
import { dlog } from '@symbiote-native/engine';
|
|
24
|
+
import type { ShimElement } from '@symbiote-native/svelte/native-view-bridge';
|
|
25
|
+
import {
|
|
26
|
+
NAVIGATION_EVENT_BLUR,
|
|
27
|
+
NAVIGATION_EVENT_FOCUS,
|
|
28
|
+
createInitialTabState,
|
|
29
|
+
createNavigationEmitter,
|
|
30
|
+
diffFocusedRoute,
|
|
31
|
+
isFocusedRoute,
|
|
32
|
+
reconcileTabRoutes,
|
|
33
|
+
renderTabBar,
|
|
34
|
+
tabRouterReducer,
|
|
35
|
+
} from '../../core';
|
|
36
|
+
import type {
|
|
37
|
+
IDescriptor,
|
|
38
|
+
INavigationEmitter,
|
|
39
|
+
IRoute,
|
|
40
|
+
ITabBarItemView,
|
|
41
|
+
ITabNavigatorHandle,
|
|
42
|
+
ITabOptions,
|
|
43
|
+
ITabRouterAction,
|
|
44
|
+
ITabRouterState,
|
|
45
|
+
} from '../../core';
|
|
46
|
+
import { createDescriptorSubtreeSync } from '../descriptor-subtree';
|
|
47
|
+
import { getNavigationScope } from '../navigation-context';
|
|
48
|
+
import type { INavigationScopeValue } from '../navigation-context';
|
|
49
|
+
import NavigationScope from '../navigation-scope.svelte';
|
|
50
|
+
import { SCREEN_REGISTRY_HOST_PROPS } from '../registry-host';
|
|
51
|
+
import {
|
|
52
|
+
setScreenCollector,
|
|
53
|
+
toRegistry,
|
|
54
|
+
withoutScreen,
|
|
55
|
+
} from '../screen-registry';
|
|
56
|
+
import type { IRegisteredScreen } from '../screen-registry';
|
|
57
|
+
import type { ITabScreenProps } from '../tab-screen-props';
|
|
58
|
+
import type { ITabProps } from './tab-props';
|
|
59
|
+
|
|
60
|
+
let { initialRouteName, screenOptions, children }: ITabProps = $props();
|
|
61
|
+
|
|
62
|
+
// Read BEFORE this Tab establishes its own per-screen NavigationScope - becomes the `parent`
|
|
63
|
+
// link a nested screen's useNavigation().getParent() walks. undefined at the nesting root.
|
|
64
|
+
const parentScope = getNavigationScope();
|
|
65
|
+
|
|
66
|
+
const routeIdPrefix = `tab-${(tabInstanceCounter += 1)}`;
|
|
67
|
+
|
|
68
|
+
let screens = $state.raw<IRegisteredScreen<ITabScreenProps['options']>[]>([]);
|
|
69
|
+
setScreenCollector<ITabScreenProps['options']>({
|
|
70
|
+
kind: 'tab',
|
|
71
|
+
register: screen => {
|
|
72
|
+
screens = [...screens, screen];
|
|
73
|
+
},
|
|
74
|
+
unregister: screen => {
|
|
75
|
+
screens = withoutScreen(screens, screen);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const registry = $derived(toRegistry(screens));
|
|
80
|
+
|
|
81
|
+
// A tab's route list is a PROJECTION of the registry, not navigation history the way Stack's
|
|
82
|
+
// is: a marker registering or unregistering must add or drop its tab. Unlike Stack, a route key
|
|
83
|
+
// here is derived from the route NAME rather than an incrementing sequence, so re-deriving this
|
|
84
|
+
// list on every registry change re-keys nothing.
|
|
85
|
+
const registeredRoutes = $derived.by<IRoute<unknown>[]>(() =>
|
|
86
|
+
[...registry.entries()].map(([name, entry]) => ({
|
|
87
|
+
key: `${routeIdPrefix}-${name}`,
|
|
88
|
+
name,
|
|
89
|
+
params: entry.initialParams,
|
|
90
|
+
})),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// The dispatched half is the only STATE here; the registry half is re-derived and reconciled
|
|
94
|
+
// against it on every change (reconcileTabRoutes, core - it is what preserves each surviving
|
|
95
|
+
// route's key/params and keeps the focus on the same route NAME). Nothing needs memoizing
|
|
96
|
+
// inside the derivation, so there is no state write during a derivation to avoid in the first
|
|
97
|
+
// place. Until something dispatches there is nothing to reconcile against, hence the seed.
|
|
98
|
+
let dispatchedState = $state.raw<ITabRouterState | null>(null);
|
|
99
|
+
|
|
100
|
+
const state = $derived.by<ITabRouterState>(() => {
|
|
101
|
+
if (registeredRoutes.length === 0)
|
|
102
|
+
dlog('Tab: no <Tab.Screen> children registered');
|
|
103
|
+
const dispatched = dispatchedState;
|
|
104
|
+
return dispatched === null
|
|
105
|
+
? createInitialTabState(registeredRoutes, initialRouteName)
|
|
106
|
+
: reconcileTabRoutes(dispatched, registeredRoutes);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function dispatch(action: ITabRouterAction): void {
|
|
110
|
+
dispatchedState = tabRouterReducer(state, action);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function jumpTo(name: string, params?: unknown): void {
|
|
114
|
+
dispatch({ type: 'jumpTo', name, params });
|
|
115
|
+
}
|
|
116
|
+
export function setParams(params: unknown, key: string): void {
|
|
117
|
+
dispatch({ type: 'setParams', key, params });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const handle: ITabNavigatorHandle = { jumpTo, setParams };
|
|
121
|
+
|
|
122
|
+
// One emitter per route.key, created lazily and cached for the navigator's whole lifetime -
|
|
123
|
+
// mirrors Stack's own `emitters` map. This decouples emitter IDENTITY (stable, looked up by
|
|
124
|
+
// key, read below when a route's NavigationScope is built) from emit TIMING (must wait until
|
|
125
|
+
// the focused screen has actually mounted and subscribed).
|
|
126
|
+
const emitters = new Map<string, INavigationEmitter>();
|
|
127
|
+
function emitterFor(routeKey: string): INavigationEmitter {
|
|
128
|
+
let emitter = emitters.get(routeKey);
|
|
129
|
+
if (emitter === undefined) {
|
|
130
|
+
emitter = createNavigationEmitter();
|
|
131
|
+
emitters.set(routeKey, emitter);
|
|
132
|
+
}
|
|
133
|
+
return emitter;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Tab paints its own bar in pure JS - there is no native onAppear/onDisappear to hook (unlike
|
|
137
|
+
// Stack's RNSScreen), so focus/blur is synthesized here. Keyed on the route KEY (not the route
|
|
138
|
+
// object) so a setParams-only change doesn't spuriously re-fire focus/blur.
|
|
139
|
+
//
|
|
140
|
+
// The bookkeeping updates immediately, but the actual emit is deferred to `tick()` - Svelte's
|
|
141
|
+
// twin of Vue's nextTick, and needed for the same reason: the newly-focused screen's own
|
|
142
|
+
// `$effect`s (its useIsFocused/useFocusEffect subscriptions) are created during THIS flush,
|
|
143
|
+
// after this effect was, so emitting synchronously here would reach zero subscribers.
|
|
144
|
+
let previousFocusedKey: string | undefined;
|
|
145
|
+
$effect(() => {
|
|
146
|
+
const nextKey = state.routes[state.index]?.key;
|
|
147
|
+
const { blurKey, focusKey } = diffFocusedRoute(previousFocusedKey, nextKey);
|
|
148
|
+
if (blurKey === undefined && focusKey === undefined) return;
|
|
149
|
+
previousFocusedKey = nextKey;
|
|
150
|
+
void tick().then(() => {
|
|
151
|
+
if (blurKey !== undefined) {
|
|
152
|
+
dlog(`Tab: route "${blurKey}" blurred at t=${Date.now()}`);
|
|
153
|
+
emitterFor(blurKey).emit(NAVIGATION_EVENT_BLUR);
|
|
154
|
+
}
|
|
155
|
+
if (focusKey !== undefined) {
|
|
156
|
+
dlog(`Tab: route "${focusKey}" focused at t=${Date.now()}`);
|
|
157
|
+
emitterFor(focusKey).emit(NAVIGATION_EVENT_FOCUS);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
onDestroy(() => {
|
|
163
|
+
if (previousFocusedKey !== undefined) {
|
|
164
|
+
emitterFor(previousFocusedKey).emit(NAVIGATION_EVENT_BLUR);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
function resolveTabOptions(
|
|
169
|
+
entry: IRegisteredScreen<ITabScreenProps['options']> | undefined,
|
|
170
|
+
route: IRoute<unknown>,
|
|
171
|
+
): ITabOptions {
|
|
172
|
+
const own =
|
|
173
|
+
entry === undefined
|
|
174
|
+
? undefined
|
|
175
|
+
: typeof entry.options === 'function'
|
|
176
|
+
? entry.options({ route, navigation: handle })
|
|
177
|
+
: entry.options;
|
|
178
|
+
return { ...screenOptions, ...own };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const items = $derived.by<ITabBarItemView[]>(() =>
|
|
182
|
+
state.routes.map((route, index) => {
|
|
183
|
+
const entry = registry.get(route.name);
|
|
184
|
+
const focused = isFocusedRoute(index, state.index);
|
|
185
|
+
if (entry === undefined) {
|
|
186
|
+
dlog(`Tab: no screen registered for route name "${route.name}"`);
|
|
187
|
+
return { key: route.key, focused, label: route.name, passthrough: {} };
|
|
188
|
+
}
|
|
189
|
+
const options = resolveTabOptions(entry, route);
|
|
190
|
+
return {
|
|
191
|
+
key: route.key,
|
|
192
|
+
focused,
|
|
193
|
+
label: options.tabBarLabel ?? options.title ?? route.name,
|
|
194
|
+
icon: options.tabBarIcon,
|
|
195
|
+
badge: options.tabBarBadge,
|
|
196
|
+
activeTintColor: options.tabBarActiveTintColor,
|
|
197
|
+
inactiveTintColor: options.tabBarInactiveTintColor,
|
|
198
|
+
passthrough: {
|
|
199
|
+
onPress: () => jumpTo(route.name),
|
|
200
|
+
accessibilityRole: 'tab',
|
|
201
|
+
accessibilityState: { selected: focused },
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const focusedRoute = $derived(state.routes[state.index]);
|
|
208
|
+
const focusedOptions = $derived(
|
|
209
|
+
focusedRoute === undefined
|
|
210
|
+
? screenOptions
|
|
211
|
+
: resolveTabOptions(registry.get(focusedRoute.name), focusedRoute),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const tabBar = $derived.by<IDescriptor>(() =>
|
|
215
|
+
renderTabBar({
|
|
216
|
+
items,
|
|
217
|
+
style: focusedOptions?.tabBarStyle,
|
|
218
|
+
passthrough: {},
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Only the focused route's screen is ever mounted (unlike Stack, which keeps every pushed route
|
|
223
|
+
// alive), so a fresh NavigationScope per focus change is sufficient - the previous screen's
|
|
224
|
+
// whole subtree (and any listeners it registered) is torn down by an ordinary unmount when
|
|
225
|
+
// focus moves on.
|
|
226
|
+
const focusedScreen = $derived.by<
|
|
227
|
+
{ scope: INavigationScopeValue; component: Component } | undefined
|
|
228
|
+
>(() => {
|
|
229
|
+
if (focusedRoute === undefined) return undefined;
|
|
230
|
+
const entry = registry.get(focusedRoute.name);
|
|
231
|
+
if (entry === undefined) return undefined;
|
|
232
|
+
return {
|
|
233
|
+
component: entry.component,
|
|
234
|
+
scope: {
|
|
235
|
+
route: focusedRoute,
|
|
236
|
+
navigation: handle,
|
|
237
|
+
emitter: emitterFor(focusedRoute.key),
|
|
238
|
+
parent: parentScope?.current,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// The bar's ROOT stays a literal template tag (so `bind:this` has a statically known tag) and
|
|
244
|
+
// only its children go through the Descriptor bridge - the uniform shape every Svelte component
|
|
245
|
+
// consuming a `render-*.ts` uses (svelte-adapter-dom-shim skill §19). The item count varies with
|
|
246
|
+
// the registry, hence the shape-change-tolerant wrapper rather than the raw bridge.
|
|
247
|
+
let tabBarHost = $state.raw<ShimElement | null>(null);
|
|
248
|
+
const syncTabBarChildren = createDescriptorSubtreeSync();
|
|
249
|
+
$effect(() => {
|
|
250
|
+
const host = tabBarHost;
|
|
251
|
+
const barChildren = tabBar.children;
|
|
252
|
+
syncTabBarChildren(host, barChildren);
|
|
253
|
+
});
|
|
254
|
+
</script>
|
|
255
|
+
|
|
256
|
+
<symbiote-view p={TAB_ROOT_PROPS}>
|
|
257
|
+
<symbiote-text p={SCREEN_REGISTRY_HOST_PROPS}>
|
|
258
|
+
{@render children?.()}
|
|
259
|
+
</symbiote-text>
|
|
260
|
+
<symbiote-view p={TAB_CONTENT_PROPS}>
|
|
261
|
+
{#if focusedScreen !== undefined}
|
|
262
|
+
{@const FocusedComponent = focusedScreen.component}
|
|
263
|
+
<NavigationScope value={focusedScreen.scope}>
|
|
264
|
+
<FocusedComponent />
|
|
265
|
+
</NavigationScope>
|
|
266
|
+
{/if}
|
|
267
|
+
</symbiote-view>
|
|
268
|
+
<symbiote-view p={tabBar.props} bind:this={tabBarHost} />
|
|
269
|
+
</symbiote-view>
|