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