@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
|
@@ -11,19 +11,23 @@
|
|
|
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';
|
|
18
18
|
// backTitleVisible defaults to `true` on both platforms per the codegen spec's own default
|
|
19
19
|
// (CT.WithDefault<boolean, 'true'>) - no ios/android divergence in v1 scope, so a single constant
|
|
20
20
|
// stands in for the per-platform injection point ISliderPlatform-style adapters use elsewhere.
|
|
21
|
-
const NAVIGATOR_PLATFORM = {
|
|
21
|
+
const NAVIGATOR_PLATFORM = {
|
|
22
|
+
defaultHeaderBackTitleVisible: true,
|
|
23
|
+
};
|
|
22
24
|
function isScreenElement(child) {
|
|
23
25
|
return isValidElement(child) && child.type === Screen;
|
|
24
26
|
}
|
|
25
27
|
function resolveScreenOptions(entry, optionsArgs, screenOptions) {
|
|
26
|
-
const own = typeof entry.options === 'function'
|
|
28
|
+
const own = typeof entry.options === 'function'
|
|
29
|
+
? entry.options(optionsArgs)
|
|
30
|
+
: entry.options;
|
|
27
31
|
return { ...screenOptions, ...own };
|
|
28
32
|
}
|
|
29
33
|
const StackImpl = forwardRef((props, forwardedRef) => {
|
|
@@ -51,16 +55,35 @@ const StackImpl = forwardRef((props, forwardedRef) => {
|
|
|
51
55
|
}, [emitters]);
|
|
52
56
|
const createRoute = useCallback((name, params) => {
|
|
53
57
|
routeSequence.current += 1;
|
|
54
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
key: `${routeIdPrefix}-${name}-${routeSequence.current}`,
|
|
60
|
+
name,
|
|
61
|
+
params,
|
|
62
|
+
};
|
|
55
63
|
}, [routeIdPrefix]);
|
|
56
|
-
const
|
|
64
|
+
const registeredNames = useMemo(() => [...registry.keys()], [registry]);
|
|
65
|
+
const [dispatchedState, dispatch] = useReducer(navigatorReducer, undefined, () => {
|
|
57
66
|
const initialRouteName = props.initialRouteName ?? registry.keys().next().value;
|
|
58
67
|
if (initialRouteName === undefined) {
|
|
59
68
|
dlog('Stack: no <Stack.Screen> children registered');
|
|
60
|
-
return createInitialNavigatorState({
|
|
69
|
+
return createInitialNavigatorState({
|
|
70
|
+
key: routeIdPrefix,
|
|
71
|
+
name: '',
|
|
72
|
+
params: undefined,
|
|
73
|
+
});
|
|
61
74
|
}
|
|
62
75
|
return createInitialNavigatorState(createRoute(initialRouteName, registry.get(initialRouteName)?.initialParams));
|
|
63
76
|
});
|
|
77
|
+
// A <Stack.Screen> marker can unregister while its route is still in the pushed history, which
|
|
78
|
+
// would leave that entry with nothing to render (reconcileStackRoutes' header). Reconciling here
|
|
79
|
+
// rather than in an effect repairs the CURRENT paint - the call is pure and hands back the same
|
|
80
|
+
// reference when nothing changed, so it is safe during render; the effect below only PERSISTS
|
|
81
|
+
// the repair, so the next push builds on the pruned history instead of the phantom one.
|
|
82
|
+
const state = reconcileStackRoutes(dispatchedState, registeredNames);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (state !== dispatchedState)
|
|
85
|
+
dispatch({ type: 'reset', state });
|
|
86
|
+
}, [state, dispatchedState]);
|
|
64
87
|
const handle = useMemo(() => ({
|
|
65
88
|
push: (name, params) => dispatch({ type: 'push', route: createRoute(name, params) }),
|
|
66
89
|
pop: count => dispatch({ type: 'pop', count }),
|
|
@@ -101,7 +124,10 @@ const StackImpl = forwardRef((props, forwardedRef) => {
|
|
|
101
124
|
dlog(`Stack: no screen registered for route name "${route.name}"`);
|
|
102
125
|
return null;
|
|
103
126
|
}
|
|
104
|
-
const screenOptionsArgs = {
|
|
127
|
+
const screenOptionsArgs = {
|
|
128
|
+
route,
|
|
129
|
+
navigation: handle,
|
|
130
|
+
};
|
|
105
131
|
const mergedOptions = resolveScreenOptions(entry, screenOptionsArgs, props.screenOptions);
|
|
106
132
|
const activityState = computeActivityState(index, state.routes.length);
|
|
107
133
|
// Investigation instrumentation (flicker-on-focus bug): fires on EVERY Stack render, not just
|
|
@@ -155,7 +181,8 @@ const StackImpl = forwardRef((props, forwardedRef) => {
|
|
|
155
181
|
const appRef = searchBarOptions.ref;
|
|
156
182
|
if (!appRef)
|
|
157
183
|
return;
|
|
158
|
-
appRef.current =
|
|
184
|
+
appRef.current =
|
|
185
|
+
node === null ? null : buildSearchBarHandle(() => node);
|
|
159
186
|
},
|
|
160
187
|
}
|
|
161
188
|
: undefined,
|
|
@@ -177,7 +204,14 @@ const StackImpl = forwardRef((props, forwardedRef) => {
|
|
|
177
204
|
// sizing (see RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME's comment in core/constants.ts). A
|
|
178
205
|
// `push` screen doesn't need it, but a `formSheet` one is otherwise left with no content
|
|
179
206
|
// ever attached natively.
|
|
180
|
-
const content = createElement(RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, plan.contentWrapperProps, createElement(NavigationContext.Provider, {
|
|
207
|
+
const content = createElement(RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, plan.contentWrapperProps, createElement(NavigationContext.Provider, {
|
|
208
|
+
value: {
|
|
209
|
+
route,
|
|
210
|
+
navigation: handle,
|
|
211
|
+
emitter: routeEmitter,
|
|
212
|
+
parent: ambientContext,
|
|
213
|
+
},
|
|
214
|
+
}, createElement(entry.component)));
|
|
181
215
|
// react-native-screens' own Screen.tsx swaps in a DIFFERENT Fabric component
|
|
182
216
|
// ('RNSModalScreen') for a modally-presented screen - its native updateLayoutMetrics: relies on
|
|
183
217
|
// this exact class to know it must NOT apply Yoga's computed frame (see resolveScreenViewName's
|
|
@@ -194,7 +228,10 @@ const StackImpl = forwardRef((props, forwardedRef) => {
|
|
|
194
228
|
// RNSScreen.mm treats an unset/inactive nested screen as not yet pushed, leaving it
|
|
195
229
|
// parked at its pre-push transition position (off past the bottom edge) instead of
|
|
196
230
|
// its real, presented frame.
|
|
197
|
-
{
|
|
231
|
+
{
|
|
232
|
+
style: plan.innerScreenStyle,
|
|
233
|
+
activityState: plan.activityState,
|
|
234
|
+
}, descriptorToReact(plan.headerConfig), content)))
|
|
198
235
|
: createElement(plan.screenViewName, { key: route.key, ...plan.screenProps }, descriptorToReact(plan.headerConfig), content);
|
|
199
236
|
});
|
|
200
237
|
return createElement(RNS_SCREEN_STACK_VIEW_NAME, stackProps, ...children);
|
|
@@ -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';
|
|
@@ -17,7 +17,9 @@ function isTabScreenElement(child) {
|
|
|
17
17
|
return isValidElement(child) && child.type === TabScreen;
|
|
18
18
|
}
|
|
19
19
|
function resolveTabOptions(entry, optionsArgs, screenOptions) {
|
|
20
|
-
const own = typeof entry.options === 'function'
|
|
20
|
+
const own = typeof entry.options === 'function'
|
|
21
|
+
? entry.options(optionsArgs)
|
|
22
|
+
: entry.options;
|
|
21
23
|
return { ...screenOptions, ...own };
|
|
22
24
|
}
|
|
23
25
|
const TAB_CONTENT_STYLE = { flex: 1 };
|
|
@@ -34,11 +36,27 @@ const TabImpl = forwardRef((props, forwardedRef) => {
|
|
|
34
36
|
name,
|
|
35
37
|
params: entry.initialParams,
|
|
36
38
|
})), [registry, routeIdPrefix]);
|
|
37
|
-
|
|
39
|
+
// A <Tab.Screen> can appear or disappear after mount (a marker behind a conditional, a
|
|
40
|
+
// data-driven screen list), so the route list is re-derived from `children` on every render and
|
|
41
|
+
// the STORED state is reconciled against it (reconcileTabRoutes, core - it preserves each
|
|
42
|
+
// surviving route's key and accumulated params and keeps focus on the same route NAME) rather
|
|
43
|
+
// than staying frozen at whatever the first render saw. `null` means no marker had registered
|
|
44
|
+
// yet: seeding an empty list would resolve initialRouteName against nothing and lose it, so the
|
|
45
|
+
// state stays derived until either a marker arrives or something dispatches.
|
|
46
|
+
const [storedState, setStoredState] = useState(() => routes.length === 0
|
|
47
|
+
? null
|
|
48
|
+
: createInitialTabState(routes, props.initialRouteName));
|
|
49
|
+
const resolveState = useCallback((stored) => stored === null
|
|
50
|
+
? createInitialTabState(routes, props.initialRouteName)
|
|
51
|
+
: reconcileTabRoutes(stored, routes), [routes, props.initialRouteName]);
|
|
52
|
+
const state = resolveState(storedState);
|
|
38
53
|
if (routes.length === 0)
|
|
39
54
|
dlog('Tab: no <Tab.Screen> children registered');
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
// The updater re-resolves rather than closing over `state` so that two dispatches batched into
|
|
56
|
+
// one render still compose: the second reduces over the first's result, not a stale snapshot.
|
|
57
|
+
const dispatch = useCallback((action) => setStoredState(current => tabRouterReducer(resolveState(current), action)), [resolveState]);
|
|
58
|
+
const jumpTo = useCallback((name, params) => dispatch({ type: 'jumpTo', name, params }), [dispatch]);
|
|
59
|
+
const setParams = useCallback((params, key) => dispatch({ type: 'setParams', key, params }), [dispatch]);
|
|
42
60
|
const handle = useMemo(() => ({ jumpTo, setParams }), [jumpTo, setParams]);
|
|
43
61
|
useImperativeHandle(forwardedRef, () => handle, [handle]);
|
|
44
62
|
const focusedRoute = state.routes[state.index];
|
|
@@ -70,11 +88,17 @@ const TabImpl = forwardRef((props, forwardedRef) => {
|
|
|
70
88
|
},
|
|
71
89
|
};
|
|
72
90
|
});
|
|
73
|
-
const focusedEntry = focusedRoute
|
|
91
|
+
const focusedEntry = focusedRoute
|
|
92
|
+
? registry.get(focusedRoute.name)
|
|
93
|
+
: undefined;
|
|
74
94
|
const focusedOptions = focusedEntry && focusedRoute
|
|
75
95
|
? resolveTabOptions(focusedEntry, { route: focusedRoute, navigation: handle }, props.screenOptions)
|
|
76
96
|
: props.screenOptions;
|
|
77
|
-
const tabBar = descriptorToReact(renderTabBar({
|
|
97
|
+
const tabBar = descriptorToReact(renderTabBar({
|
|
98
|
+
items,
|
|
99
|
+
style: focusedOptions?.tabBarStyle,
|
|
100
|
+
passthrough: {},
|
|
101
|
+
}));
|
|
78
102
|
// Only the focused route's screen is ever mounted (unlike Stack, which keeps every pushed route
|
|
79
103
|
// alive), so a fresh emitter per focus change is sufficient - no per-route emitter map to prune,
|
|
80
104
|
// because the previous screen's whole subtree (and any listeners it registered) is torn down by
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Accessor } from 'solid-js';
|
|
2
|
+
import type { JSX } from '@symbiote-native/solid/jsx-runtime';
|
|
3
|
+
import type { IStyleProp, IViewStyle } from '@symbiote-native/engine';
|
|
4
|
+
import type { IDrawerDescriptorMap, IDrawerNavigatorHandle, IDrawerOptions, IDrawerRouterState } from '../../core';
|
|
5
|
+
import { DrawerScreen } from '../screen';
|
|
6
|
+
export type { IDrawerNavigatorHandle, IDrawerDescriptorMap } from '../../core';
|
|
7
|
+
export type IDrawerContentSlotProps = {
|
|
8
|
+
state: IDrawerRouterState;
|
|
9
|
+
descriptors: IDrawerDescriptorMap;
|
|
10
|
+
navigation: IDrawerNavigatorHandle;
|
|
11
|
+
};
|
|
12
|
+
export type IDrawerProps = IDrawerOptions & {
|
|
13
|
+
initialRouteName?: string;
|
|
14
|
+
drawerStyle?: IStyleProp<IViewStyle>;
|
|
15
|
+
drawerContent?: (props: Accessor<IDrawerContentSlotProps>) => JSX.Element;
|
|
16
|
+
ref?: (handle: IDrawerNavigatorHandle) => void;
|
|
17
|
+
children?: JSX.Element;
|
|
18
|
+
};
|
|
19
|
+
declare function DrawerImpl(props: IDrawerProps): JSX.Element;
|
|
20
|
+
export declare const Drawer: typeof DrawerImpl & {
|
|
21
|
+
Screen: typeof DrawerScreen;
|
|
22
|
+
};
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// Drawer, the Solid lifecycle half. The open/closed + focused-route router (drawer-router-state)
|
|
2
|
+
// and the pure swipe/geometry math (drawer-options) live in @symbiote-native/navigation core,
|
|
3
|
+
// shared verbatim with every other adapter; here Solid supplies the lifecycle - a signal for the
|
|
4
|
+
// router, a PanResponder built ONCE whose callbacks read options/window-width/isOpen LIVE (Solid
|
|
5
|
+
// props are getters, so a gesture callback always sees the current value with no ref-mirroring
|
|
6
|
+
// dance), an Animated.Value driving the slide/opacity transforms, and a callback `ref` for the
|
|
7
|
+
// open/close/toggle/jumpTo handle. Tab is the closer sibling (both are fixed-route-list,
|
|
8
|
+
// no-react-native-screens navigators); Stack's push/pop and native-screen bridging don't apply.
|
|
9
|
+
//
|
|
10
|
+
// FEASIBILITY NOTE (mirrored verbatim from the React/Vue twins - the gaps are architectural, not
|
|
11
|
+
// adapter-specific): the REAL @react-navigation/drawer is built on react-native-gesture-handler +
|
|
12
|
+
// react-native-reanimated, neither of which this codebase depends on. What's built here reaches the
|
|
13
|
+
// same swipe-to-open/close + front/back/slide/permanent behavior using only PanResponder +
|
|
14
|
+
// Animated (both already in @symbiote-native/engine). See the explicit gap list at the bottom.
|
|
15
|
+
//
|
|
16
|
+
// THE SOLID-ONLY PART: renderDrawer's Descriptor SHAPE follows `drawerType` - the slot ORDER
|
|
17
|
+
// changes, and 'permanent' drops the animation entirely. descriptorToSolid builds once and updates
|
|
18
|
+
// props in place, so a type/position change is a genuine rebuild boundary and is asked for by name
|
|
19
|
+
// (a createMemo over those discriminators alone, build untracked -
|
|
20
|
+
// .claude/rules/solid-descriptor-bridge.md §5). Everything else - width, overlay colour, the live
|
|
21
|
+
// interpolations, the focused screen's params - flows through prop accessors on the existing nodes.
|
|
22
|
+
//
|
|
23
|
+
// `drawerContent` is a render prop taking an ACCESSOR, called once and untracked, not a plain
|
|
24
|
+
// value: §4 of the same rules file. A value snapshot would freeze the drawer's own content at the
|
|
25
|
+
// state it was built with, and re-invoking it from inside a tracked scope would replace the whole
|
|
26
|
+
// panel subtree mid-gesture.
|
|
27
|
+
import { createEffect, createMemo, createSignal, mergeProps, onCleanup, untrack, } from 'solid-js';
|
|
28
|
+
import { Animated, createWindowDimensions } from '@symbiote-native/solid';
|
|
29
|
+
// createComponent from solid-js, insert from the renderer - see stack/index.ts's note.
|
|
30
|
+
import { createComponent } from 'solid-js';
|
|
31
|
+
import { insert } from '@symbiote-native/solid/renderer';
|
|
32
|
+
import { PanResponder, dlog } from '@symbiote-native/engine';
|
|
33
|
+
import { DRAWER_DEFAULT_OVERLAY_COLOR, NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialDrawerRouterState, createNavigationEmitter, diffFocusedRoute, drawerChildOrder, drawerRouterReducer, isDrawerAnimated, renderDrawer, resolveDragProgress, resolveDrawerGeometry, resolveDrawerPosition, resolveDrawerSlotInterpolation, resolveDrawerType, resolveSwipeIntent, shouldClaimDrawerSwipe, } from '../../core/index.js';
|
|
34
|
+
import { hostElement } from '../host.js';
|
|
35
|
+
import { NavigationScopeProvider, useNavigationScope, } from '../navigation-context.js';
|
|
36
|
+
import { ScreenCollectorProvider, createScreenSignal, toRegistry, } from '../screen-registry.js';
|
|
37
|
+
import { DrawerScreen } from '../screen.js';
|
|
38
|
+
const DRAWER_SNAP_DURATION = 250;
|
|
39
|
+
let navigatorSequence = 0;
|
|
40
|
+
function resolveDrawerScreenOptions(entry, args) {
|
|
41
|
+
if (typeof entry.options === 'function')
|
|
42
|
+
return entry.options(args);
|
|
43
|
+
return entry.options ?? {};
|
|
44
|
+
}
|
|
45
|
+
function sameDrawerShape(a, b) {
|
|
46
|
+
return (a.type === b.type && a.position === b.position && a.animated === b.animated);
|
|
47
|
+
}
|
|
48
|
+
function DrawerImpl(props) {
|
|
49
|
+
const parentScope = useNavigationScope();
|
|
50
|
+
const { screens, collector } = createScreenSignal('drawer');
|
|
51
|
+
const registry = createMemo(() => toRegistry(screens()));
|
|
52
|
+
navigatorSequence += 1;
|
|
53
|
+
const routeIdPrefix = `drawer-${navigatorSequence}`;
|
|
54
|
+
// Every field read live off `props`, so a gesture callback (built once, below) always negotiates
|
|
55
|
+
// against the CURRENT options - Solid props are getters, which is what removes React's
|
|
56
|
+
// useRef(value) mirroring here.
|
|
57
|
+
function currentOptions() {
|
|
58
|
+
return {
|
|
59
|
+
drawerType: props.drawerType,
|
|
60
|
+
drawerPosition: props.drawerPosition,
|
|
61
|
+
drawerWidth: props.drawerWidth,
|
|
62
|
+
overlayColor: props.overlayColor,
|
|
63
|
+
swipeEnabled: props.swipeEnabled,
|
|
64
|
+
swipeEdgeWidth: props.swipeEdgeWidth,
|
|
65
|
+
swipeMinDistance: props.swipeMinDistance,
|
|
66
|
+
swipeMinVelocity: props.swipeMinVelocity,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Route keys are derived from the screen NAME, so the derivation is pure and re-runnable.
|
|
70
|
+
const routes = createMemo(() => [...registry().entries()].map(([name, entry]) => ({
|
|
71
|
+
key: `${routeIdPrefix}-${name}`,
|
|
72
|
+
name,
|
|
73
|
+
params: entry.initialParams,
|
|
74
|
+
})));
|
|
75
|
+
// `null` means nothing has dispatched yet - and until then the initial state is RE-DERIVED from
|
|
76
|
+
// the registry on every read rather than cached. That is not an optimisation detail, it is what
|
|
77
|
+
// makes the drawer correct: each marker registers with its OWN signal write, and Solid can
|
|
78
|
+
// recompute a downstream memo between two of them, so a one-shot seed reliably captured only the
|
|
79
|
+
// FIRST screen and every later jumpTo was a no-op against a one-route router. Once the app
|
|
80
|
+
// dispatches, the route list freezes - matching React's and Vue's, which build it once in their
|
|
81
|
+
// setup pass and never reconcile (a marker added after that is ignored on every adapter).
|
|
82
|
+
const [dispatched, setDispatched] = createSignal(null);
|
|
83
|
+
const state = createMemo(() => {
|
|
84
|
+
const current = dispatched();
|
|
85
|
+
if (current !== null)
|
|
86
|
+
return current;
|
|
87
|
+
const initial = routes();
|
|
88
|
+
if (initial.length === 0) {
|
|
89
|
+
dlog('Drawer: no <Drawer.Screen> children registered');
|
|
90
|
+
}
|
|
91
|
+
return createInitialDrawerRouterState(initial, props.initialRouteName);
|
|
92
|
+
});
|
|
93
|
+
function dispatch(action) {
|
|
94
|
+
setDispatched(drawerRouterReducer(state(), action));
|
|
95
|
+
}
|
|
96
|
+
const windowDimensions = createWindowDimensions();
|
|
97
|
+
// progress: 0 closed -> 1 open, the single Animated.Value every slide/opacity transform below
|
|
98
|
+
// interpolates from. A plain const: a Solid body runs once, so this needs no re-creation guard
|
|
99
|
+
// the way React's useRef(new Animated.Value(...)).current does.
|
|
100
|
+
const progress = new Animated.Value(state().isOpen ? 1 : 0);
|
|
101
|
+
// Where a drag STARTS from, in progress units. Always exactly 0 or 1: a gesture only begins at
|
|
102
|
+
// rest, since terminate/release always snap back before another grant can fire.
|
|
103
|
+
let dragStartProgress = 0;
|
|
104
|
+
function animateProgressTo(open) {
|
|
105
|
+
dlog(`Drawer: animateProgressTo(open=${open}) at t=${Date.now()}`);
|
|
106
|
+
Animated.timing(progress, {
|
|
107
|
+
toValue: open ? 1 : 0,
|
|
108
|
+
duration: DRAWER_SNAP_DURATION,
|
|
109
|
+
// Native-driver wiring is deferred for v1 - see the header feasibility note.
|
|
110
|
+
useNativeDriver: false,
|
|
111
|
+
}).start();
|
|
112
|
+
}
|
|
113
|
+
const handle = {
|
|
114
|
+
openDrawer: () => {
|
|
115
|
+
animateProgressTo(true);
|
|
116
|
+
dispatch({ type: 'openDrawer' });
|
|
117
|
+
},
|
|
118
|
+
closeDrawer: () => {
|
|
119
|
+
animateProgressTo(false);
|
|
120
|
+
dispatch({ type: 'closeDrawer' });
|
|
121
|
+
},
|
|
122
|
+
toggleDrawer: () => {
|
|
123
|
+
animateProgressTo(!state().isOpen);
|
|
124
|
+
dispatch({ type: 'toggleDrawer' });
|
|
125
|
+
},
|
|
126
|
+
jumpTo: (name) => {
|
|
127
|
+
// Both sides of the dispatch, because an unregistered name is a documented reducer no-op that
|
|
128
|
+
// hands the SAME state back: animating off the pre-dispatch snapshot alone would slide the
|
|
129
|
+
// panel shut while the router still says isOpen. The signal is written synchronously inside
|
|
130
|
+
// dispatch, so the second read is already the reducer's own answer.
|
|
131
|
+
const wasOpen = state().isOpen;
|
|
132
|
+
dispatch({ type: 'jumpTo', name });
|
|
133
|
+
if (wasOpen && !state().isOpen)
|
|
134
|
+
animateProgressTo(false);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
props.ref?.(handle);
|
|
138
|
+
const panResponder = PanResponder.create({
|
|
139
|
+
onStartShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, windowDimensions().width, state().isOpen, currentOptions(), 'start'),
|
|
140
|
+
onMoveShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, windowDimensions().width, state().isOpen, currentOptions(), 'move'),
|
|
141
|
+
onPanResponderGrant: () => {
|
|
142
|
+
dragStartProgress = state().isOpen ? 1 : 0;
|
|
143
|
+
},
|
|
144
|
+
onPanResponderMove: (_event, gestureState) => {
|
|
145
|
+
progress.setValue(resolveDragProgress(gestureState, dragStartProgress, currentOptions()));
|
|
146
|
+
},
|
|
147
|
+
onPanResponderRelease: (_event, gestureState) => {
|
|
148
|
+
const intent = resolveSwipeIntent(gestureState, state().isOpen, currentOptions());
|
|
149
|
+
const open = intent === 'open';
|
|
150
|
+
dlog(`Drawer: gesture release -> ${open ? 'open' : 'close'}`);
|
|
151
|
+
animateProgressTo(open);
|
|
152
|
+
dispatch(open ? { type: 'openDrawer' } : { type: 'closeDrawer' });
|
|
153
|
+
},
|
|
154
|
+
onPanResponderTerminate: () => {
|
|
155
|
+
animateProgressTo(state().isOpen);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const emitters = new Map();
|
|
159
|
+
function emitterFor(routeKey) {
|
|
160
|
+
let emitter = emitters.get(routeKey);
|
|
161
|
+
if (emitter === undefined) {
|
|
162
|
+
emitter = createNavigationEmitter();
|
|
163
|
+
emitters.set(routeKey, emitter);
|
|
164
|
+
}
|
|
165
|
+
return emitter;
|
|
166
|
+
}
|
|
167
|
+
const focusedKey = createMemo(() => {
|
|
168
|
+
const current = state();
|
|
169
|
+
return current.routes[current.index]?.key;
|
|
170
|
+
});
|
|
171
|
+
// Deferred by a microtask for the same reason Tab's is: the newly focused screen's subtree, and
|
|
172
|
+
// the subscriptions inside it, are built by the effect flush this callback would otherwise run
|
|
173
|
+
// inside.
|
|
174
|
+
let lastFocusedKey;
|
|
175
|
+
createEffect(() => {
|
|
176
|
+
const nextKey = focusedKey();
|
|
177
|
+
const { blurKey, focusKey } = diffFocusedRoute(lastFocusedKey, nextKey);
|
|
178
|
+
if (blurKey === undefined && focusKey === undefined)
|
|
179
|
+
return;
|
|
180
|
+
lastFocusedKey = nextKey;
|
|
181
|
+
queueMicrotask(() => {
|
|
182
|
+
if (blurKey !== undefined) {
|
|
183
|
+
dlog(`Drawer: route "${blurKey}" blurred at t=${Date.now()}`);
|
|
184
|
+
emitterFor(blurKey).emit(NAVIGATION_EVENT_BLUR);
|
|
185
|
+
}
|
|
186
|
+
if (focusKey !== undefined) {
|
|
187
|
+
dlog(`Drawer: route "${focusKey}" focused at t=${Date.now()}`);
|
|
188
|
+
emitterFor(focusKey).emit(NAVIGATION_EVENT_FOCUS);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
onCleanup(() => {
|
|
193
|
+
if (lastFocusedKey !== undefined) {
|
|
194
|
+
emitterFor(lastFocusedKey).emit(NAVIGATION_EVENT_BLUR);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
function routeFor(routeKey) {
|
|
198
|
+
const found = state().routes.find(route => route.key === routeKey);
|
|
199
|
+
return found ?? { key: routeKey, name: '', params: undefined };
|
|
200
|
+
}
|
|
201
|
+
const focusedComponent = createMemo(() => {
|
|
202
|
+
const current = state();
|
|
203
|
+
const route = current.routes[current.index];
|
|
204
|
+
return route === undefined
|
|
205
|
+
? undefined
|
|
206
|
+
: registry().get(route.name)?.component;
|
|
207
|
+
});
|
|
208
|
+
const content = createMemo(() => {
|
|
209
|
+
const routeKey = focusedKey();
|
|
210
|
+
const Component = focusedComponent();
|
|
211
|
+
return untrack(() => {
|
|
212
|
+
if (routeKey === undefined || Component === undefined)
|
|
213
|
+
return undefined;
|
|
214
|
+
const scope = () => ({
|
|
215
|
+
route: routeFor(routeKey),
|
|
216
|
+
navigation: handle,
|
|
217
|
+
emitter: emitterFor(routeKey),
|
|
218
|
+
parent: parentScope?.(),
|
|
219
|
+
});
|
|
220
|
+
return createComponent(NavigationScopeProvider, {
|
|
221
|
+
value: scope,
|
|
222
|
+
get children() {
|
|
223
|
+
return createComponent(Component, {});
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
const descriptors = createMemo(() => {
|
|
229
|
+
const map = {};
|
|
230
|
+
const entries = registry();
|
|
231
|
+
for (const route of state().routes) {
|
|
232
|
+
const entry = entries.get(route.name);
|
|
233
|
+
if (entry === undefined)
|
|
234
|
+
continue;
|
|
235
|
+
map[route.key] = {
|
|
236
|
+
options: resolveDrawerScreenOptions(entry, {
|
|
237
|
+
route,
|
|
238
|
+
navigation: handle,
|
|
239
|
+
}),
|
|
240
|
+
navigation: handle,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return map;
|
|
244
|
+
});
|
|
245
|
+
// Called ONCE, untracked, with an accessor - trap §4. Calling it per change (or handing it a
|
|
246
|
+
// value) would rebuild the whole panel subtree on every drawer state change, which mid-gesture is
|
|
247
|
+
// exactly how a responder grant gets lost.
|
|
248
|
+
//
|
|
249
|
+
// LAZY, though, and inside a memo of its OWN - two separate reasons, both load-bearing.
|
|
250
|
+
// (a) Not called from this body directly: the markers have not registered yet at that point, so a
|
|
251
|
+
// render prop reading `state`/`descriptors` at its own top level (which trap §4 documents as
|
|
252
|
+
// a frozen read) would snapshot an EMPTY navigator.
|
|
253
|
+
// (b) Not a plain `let` cache either. Whatever computation first CALLS the render prop OWNS every
|
|
254
|
+
// memo the returned subtree created, and disposes them when it re-runs - so a cached node
|
|
255
|
+
// handed back to a re-running slot effect is a node whose own reactivity has been torn down,
|
|
256
|
+
// silently frozen at its first paint. A dependency-free memo has a stable owner (this
|
|
257
|
+
// component) and runs exactly once, which gives both properties at the same time.
|
|
258
|
+
const drawerContent = createMemo(() => untrack(() => props.drawerContent?.(() => ({
|
|
259
|
+
state: state(),
|
|
260
|
+
descriptors: descriptors(),
|
|
261
|
+
navigation: handle,
|
|
262
|
+
}))));
|
|
263
|
+
const shape = createMemo(() => {
|
|
264
|
+
const options = currentOptions();
|
|
265
|
+
return {
|
|
266
|
+
type: resolveDrawerType(options),
|
|
267
|
+
position: resolveDrawerPosition(options),
|
|
268
|
+
animated: isDrawerAnimated(options),
|
|
269
|
+
};
|
|
270
|
+
}, { type: '', position: '', animated: false }, { equals: sameDrawerShape });
|
|
271
|
+
function buildDrawer(current) {
|
|
272
|
+
const order = drawerChildOrder(currentOptions());
|
|
273
|
+
const rootDescriptor = createMemo(() => {
|
|
274
|
+
const options = currentOptions();
|
|
275
|
+
return renderDrawer({
|
|
276
|
+
overlayColor: props.overlayColor ?? DRAWER_DEFAULT_OVERLAY_COLOR,
|
|
277
|
+
drawerStyle: props.drawerStyle,
|
|
278
|
+
contentPassthrough: {},
|
|
279
|
+
overlayPassthrough: current.animated
|
|
280
|
+
? {
|
|
281
|
+
pointerEvents: state().isOpen ? 'auto' : 'none',
|
|
282
|
+
onStartShouldSetResponder: () => true,
|
|
283
|
+
onResponderRelease: () => {
|
|
284
|
+
animateProgressTo(false);
|
|
285
|
+
dispatch({ type: 'closeDrawer' });
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
: {},
|
|
289
|
+
panelPassthrough: {},
|
|
290
|
+
}, options);
|
|
291
|
+
});
|
|
292
|
+
function slotDescriptor(slot) {
|
|
293
|
+
const child = rootDescriptor().children[order.indexOf(slot)];
|
|
294
|
+
if (child === undefined || typeof child === 'string') {
|
|
295
|
+
throw new Error(`Drawer: renderDrawer produced no "${slot}" slot`);
|
|
296
|
+
}
|
|
297
|
+
return child;
|
|
298
|
+
}
|
|
299
|
+
// Each animated style holds an AnimatedInterpolation node, not a plain number/colour - it feeds
|
|
300
|
+
// only Animated.View's deliberately permissive `style?: unknown`, never the plain-IViewStyle
|
|
301
|
+
// branch. Recomputed reactively (geometry follows drawerWidth), which reaches the live node as
|
|
302
|
+
// an ordinary prop update rather than a rebuild.
|
|
303
|
+
function animatedStyle(slot) {
|
|
304
|
+
if (!current.animated)
|
|
305
|
+
return undefined;
|
|
306
|
+
const geometry = resolveDrawerGeometry(currentOptions());
|
|
307
|
+
// Three branches, not one call on a `slot` union: resolveDrawerSlotInterpolation is OVERLOADED
|
|
308
|
+
// per slot (only the overlay's result carries `opacity`), and a union argument matches no
|
|
309
|
+
// overload. The branch is also what makes the opacity read type-safe without a cast.
|
|
310
|
+
if (slot === 'overlay') {
|
|
311
|
+
// The overlay is a full-screen absolutely-positioned sibling BELOW content in paint order;
|
|
312
|
+
// for 'slide' the content itself translates away, so the overlay's translateX shares
|
|
313
|
+
// content's range rather than staying pinned full-screen.
|
|
314
|
+
const overlay = resolveDrawerSlotInterpolation(geometry, 'overlay');
|
|
315
|
+
return {
|
|
316
|
+
opacity: progress.interpolate(overlay.opacity),
|
|
317
|
+
transform: [{ translateX: progress.interpolate(overlay.translateX) }],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const interpolation = slot === 'content'
|
|
321
|
+
? resolveDrawerSlotInterpolation(geometry, 'content')
|
|
322
|
+
: resolveDrawerSlotInterpolation(geometry, 'panel');
|
|
323
|
+
return {
|
|
324
|
+
transform: [
|
|
325
|
+
{ translateX: progress.interpolate(interpolation.translateX) },
|
|
326
|
+
],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function slotChildren(slot) {
|
|
330
|
+
if (slot === 'content')
|
|
331
|
+
return content();
|
|
332
|
+
if (slot === 'panel')
|
|
333
|
+
return drawerContent();
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
function buildSlot(slot) {
|
|
337
|
+
const style = animatedStyle(slot);
|
|
338
|
+
if (style === undefined) {
|
|
339
|
+
const node = hostElement(slotDescriptor(slot).type, () => slotDescriptor(slot).props);
|
|
340
|
+
const children = slotChildren(slot);
|
|
341
|
+
if (children !== undefined)
|
|
342
|
+
insert(node, children);
|
|
343
|
+
return node;
|
|
344
|
+
}
|
|
345
|
+
// ONE source, never a spread followed by an explicit prop: mergeProps takes the first
|
|
346
|
+
// NON-undefined value scanning back-to-front, so a later `undefined` silently loses to an
|
|
347
|
+
// earlier value (.claude/rules/solid-descriptor-bridge.md §6). Collapsing to a single bag
|
|
348
|
+
// removes the question. mergeProps still earns its place - it wraps the function source in a
|
|
349
|
+
// memo and hands Animated.View a reactive proxy, which is what keeps the slot's props live
|
|
350
|
+
// without rebuilding the node.
|
|
351
|
+
return createComponent(Animated.View, mergeProps(() => ({
|
|
352
|
+
...slotDescriptor(slot).props,
|
|
353
|
+
style: [slotDescriptor(slot).props.style, animatedStyle(slot)],
|
|
354
|
+
get children() {
|
|
355
|
+
return slotChildren(slot);
|
|
356
|
+
},
|
|
357
|
+
})));
|
|
358
|
+
}
|
|
359
|
+
const root = hostElement('symbiote-view', () => ({
|
|
360
|
+
...rootDescriptor().props,
|
|
361
|
+
...panResponder.panHandlers,
|
|
362
|
+
}));
|
|
363
|
+
insert(root, order.map(buildSlot));
|
|
364
|
+
return root;
|
|
365
|
+
}
|
|
366
|
+
const drawer = createMemo(() => {
|
|
367
|
+
const current = shape();
|
|
368
|
+
return untrack(() => buildDrawer(current));
|
|
369
|
+
});
|
|
370
|
+
const host = hostElement('symbiote-view', () => ({ style: { flex: 1 } }));
|
|
371
|
+
insert(host, drawer);
|
|
372
|
+
return createComponent(ScreenCollectorProvider, {
|
|
373
|
+
value: collector,
|
|
374
|
+
get children() {
|
|
375
|
+
return [props.children, host];
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
export const Drawer = Object.assign(DrawerImpl, { Screen: DrawerScreen });
|
|
380
|
+
// --- Explicit gap list vs the real react-native-gesture-handler + react-native-reanimated
|
|
381
|
+
// @react-navigation/drawer (mirrored verbatim from the React twin - the gaps are architectural,
|
|
382
|
+
// not adapter-specific) ---
|
|
383
|
+
// 1. `configureGestureHandler` - a raw react-native-gesture-handler `Gesture` object escape hatch.
|
|
384
|
+
// No PanResponder equivalent exists; not ported.
|
|
385
|
+
// 2. Simultaneous/failure gesture RELATIONSHIPS (gesture-handler's declarative composition vs a
|
|
386
|
+
// nested ScrollView, another PanResponder, etc.) - PanResponder only offers negotiation via the
|
|
387
|
+
// should-set boolean gates used here (edge-start + dominant-axis), which is more prone to an
|
|
388
|
+
// accidental hijack of a nested horizontal ScrollView/Swiper.
|
|
389
|
+
// 3. `useDrawerProgress` - a Reanimated SharedValue read on the UI thread. `progress` here is a
|
|
390
|
+
// JS-thread AnimatedValue; interpolating it works, but without native-driver wiring it does not
|
|
391
|
+
// carry the same synchronous UI-thread guarantee under JS-thread load.
|
|
392
|
+
// 4. `hideStatusBarOnOpen` / `keyboardDismissMode` / `statusBarAnimation` / `overlayStyle` - not
|
|
393
|
+
// wired in this pass; straightforward additions once StatusBar/Keyboard module wiring is needed
|
|
394
|
+
// here (not a PanResponder/Animated limitation, just unscoped for v1).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Component } from 'solid-js';
|
|
2
|
+
import type { IDrawerNavigatorHandle, IDrawerScreenOptions, IRoute } from '../core';
|
|
3
|
+
export type IDrawerScreenOptionsArgs = {
|
|
4
|
+
route: IRoute<unknown>;
|
|
5
|
+
navigation: IDrawerNavigatorHandle;
|
|
6
|
+
};
|
|
7
|
+
export type IDrawerScreenOptionsResolver = (args: IDrawerScreenOptionsArgs) => IDrawerScreenOptions;
|
|
8
|
+
export type IDrawerScreenProps = {
|
|
9
|
+
name: string;
|
|
10
|
+
component: Component;
|
|
11
|
+
options?: IDrawerScreenOptions | IDrawerScreenOptionsResolver;
|
|
12
|
+
initialParams?: unknown;
|
|
13
|
+
};
|