@sigmela/router 0.2.4 → 0.2.5
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/lib/module/NavigationStack.js +3 -5
- package/lib/module/ScreenStack/ScreenStack.web.js +27 -5
- package/lib/module/ScreenStack/animationHelpers.js +6 -1
- package/lib/module/SplitView/RenderSplitView.web.js +1 -1
- package/lib/module/StackRenderer.js +1 -2
- package/lib/module/TabBar/RenderTabBar.native.js +1 -1
- package/lib/module/TabBar/RenderTabBar.web.js +1 -1
- package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +1 -1
- package/lib/typescript/src/StackRenderer.d.ts +1 -2
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { nanoid } from 'nanoid/non-secure';
|
|
|
4
4
|
import { match as pathMatchFactory } from 'path-to-regexp';
|
|
5
5
|
import qs from 'query-string';
|
|
6
6
|
import React from 'react';
|
|
7
|
+
import { StackRenderer } from "./StackRenderer.js";
|
|
7
8
|
export class NavigationStack {
|
|
8
9
|
routes = [];
|
|
9
10
|
children = [];
|
|
@@ -133,13 +134,10 @@ export class NavigationStack {
|
|
|
133
134
|
getRenderer() {
|
|
134
135
|
// eslint-disable-next-line consistent-this
|
|
135
136
|
const stackInstance = this;
|
|
137
|
+
const stackId = stackInstance.getId();
|
|
136
138
|
return function NavigationStackRenderer(props) {
|
|
137
|
-
// Lazy require to avoid circular dependency (StackRenderer imports NavigationStack)
|
|
138
|
-
const {
|
|
139
|
-
StackRenderer
|
|
140
|
-
} = require('./StackRenderer');
|
|
141
139
|
return /*#__PURE__*/React.createElement(StackRenderer, {
|
|
142
|
-
|
|
140
|
+
stackId: stackId,
|
|
143
141
|
appearance: props.appearance
|
|
144
142
|
});
|
|
145
143
|
};
|
|
@@ -69,6 +69,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
69
69
|
const isInitialMountRef = useRef(true);
|
|
70
70
|
const suppressEnterAfterEmptyRef = useRef(false);
|
|
71
71
|
const suppressedEnterKeyRef = useRef(null);
|
|
72
|
+
const isBulkRemovalRef = useRef(false);
|
|
72
73
|
const prevKeysRef = useRef([]);
|
|
73
74
|
const lastDirectionRef = useRef('forward');
|
|
74
75
|
const childMapRef = useRef(new Map());
|
|
@@ -143,12 +144,12 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
143
144
|
isResolved: state.isResolved
|
|
144
145
|
})));
|
|
145
146
|
const stateMapEntries = Array.from(stateMap.entries());
|
|
147
|
+
const prevKeysForDirection = prevKeysRef.current;
|
|
146
148
|
const direction = useMemo(() => {
|
|
147
|
-
const
|
|
148
|
-
const computed = computeDirection(prevKeys, routeKeys);
|
|
149
|
+
const computed = computeDirection(prevKeysForDirection, routeKeys);
|
|
149
150
|
prevKeysRef.current = routeKeys;
|
|
150
151
|
return computed;
|
|
151
|
-
}, [routeKeys]);
|
|
152
|
+
}, [routeKeys, prevKeysForDirection]);
|
|
152
153
|
devLog('[ScreenStack] Computed direction', {
|
|
153
154
|
prevKeys: prevKeysRef.current,
|
|
154
155
|
routeKeys,
|
|
@@ -176,6 +177,19 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
176
177
|
const containerClassName = useMemo(() => {
|
|
177
178
|
return 'screen-stack';
|
|
178
179
|
}, []);
|
|
180
|
+
|
|
181
|
+
// CRITICAL: Calculate bulk removal BEFORE useMemo for itemsContextValue
|
|
182
|
+
// so the flag is available when computing animation types
|
|
183
|
+
const removedKeysForBulkDetection = useMemo(() => {
|
|
184
|
+
const routeKeySet = new Set(routeKeys);
|
|
185
|
+
const existingKeySet = new Set();
|
|
186
|
+
for (const [key] of stateMapEntries) {
|
|
187
|
+
existingKeySet.add(key);
|
|
188
|
+
}
|
|
189
|
+
return [...existingKeySet].filter(key => !routeKeySet.has(key));
|
|
190
|
+
}, [routeKeys, stateMapEntries]);
|
|
191
|
+
const isBulkRemoval = removedKeysForBulkDetection.length > 1 || routeKeys.length === 0 && prevKeysForDirection.length > 1;
|
|
192
|
+
isBulkRemovalRef.current = isBulkRemoval;
|
|
179
193
|
useLayoutEffect(() => {
|
|
180
194
|
devLog('[ScreenStack] === LIFECYCLE EFFECT START ===', {
|
|
181
195
|
prevKeys: prevKeysRef.current,
|
|
@@ -203,6 +217,14 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
203
217
|
removedKeys
|
|
204
218
|
});
|
|
205
219
|
|
|
220
|
+
// Bulk removal was already computed before useMemo (see above)
|
|
221
|
+
devLog('[ScreenStack] Bulk removal detected', {
|
|
222
|
+
isBulkRemoval: isBulkRemovalRef.current,
|
|
223
|
+
removedCount: removedKeys.length,
|
|
224
|
+
prevLength: prevKeysForDirection.length,
|
|
225
|
+
currentLength: routeKeys.length
|
|
226
|
+
});
|
|
227
|
+
|
|
206
228
|
// If this is the first pushed key after the stack was empty, remember its key so we can
|
|
207
229
|
// suppress only its enter animation (without affecting exit animations).
|
|
208
230
|
if (!animateFirstScreenAfterEmpty && suppressEnterAfterEmptyRef.current && routeKeys.length > 0 && newKeys.length > 0) {
|
|
@@ -332,7 +354,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
332
354
|
const routeIndex = routeKeys.indexOf(key);
|
|
333
355
|
const zIndex = routeIndex >= 0 ? routeIndex + 1 : keysToRender.length + index + 1;
|
|
334
356
|
const presentationType = getPresentationTypeClass(presentation);
|
|
335
|
-
let animationType = computeAnimationType(key, isInStack, isTop, direction, presentation, isInitialPhase, animated);
|
|
357
|
+
let animationType = computeAnimationType(key, isInStack, isTop, direction, presentation, isInitialPhase, animated, isBulkRemovalRef.current);
|
|
336
358
|
|
|
337
359
|
// SplitView-secondary-only: suppress enter animation for the first screen after empty.
|
|
338
360
|
if (!animateFirstScreenAfterEmpty && isTop && direction === 'forward' && suppressedEnterKeyRef.current === key) {
|
|
@@ -364,7 +386,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
364
386
|
phase = 'inactive';
|
|
365
387
|
}
|
|
366
388
|
const presentationType = getPresentationTypeClass(presentation);
|
|
367
|
-
let animationType = isInitialPhase ? 'none' : computeAnimationType(key, isInStack, isTop, direction, presentation, isInitialPhase, animated);
|
|
389
|
+
let animationType = isInitialPhase ? 'none' : computeAnimationType(key, isInStack, isTop, direction, presentation, isInitialPhase, animated, isBulkRemovalRef.current);
|
|
368
390
|
if (!animateFirstScreenAfterEmpty && isTop && direction === 'forward' && suppressedEnterKeyRef.current === key) {
|
|
369
391
|
animationType = 'none';
|
|
370
392
|
}
|
|
@@ -35,13 +35,18 @@ export function getAnimationTypeForPresentation(presentation, isEntering, direct
|
|
|
35
35
|
}
|
|
36
36
|
return `${presentationClass}-${suffix}`;
|
|
37
37
|
}
|
|
38
|
-
export function computeAnimationType(_key, isInStack, isTop, direction, presentation, isInitialPhase, animated = true) {
|
|
38
|
+
export function computeAnimationType(_key, isInStack, isTop, direction, presentation, isInitialPhase, animated = true, isBulkRemoval = false) {
|
|
39
39
|
if (!animated) {
|
|
40
40
|
return 'no-animate';
|
|
41
41
|
}
|
|
42
42
|
if (isInitialPhase) {
|
|
43
43
|
return 'none';
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
// When multiple screens are removed at once (bulk removal), don't animate them
|
|
47
|
+
if (isBulkRemoval && !isInStack) {
|
|
48
|
+
return 'no-animate';
|
|
49
|
+
}
|
|
45
50
|
const isEntering = isInStack && isTop;
|
|
46
51
|
const isModalLike = isModalLikePresentation(presentation);
|
|
47
52
|
if (isModalLike) {
|
|
@@ -7,12 +7,11 @@ import { useRouter } from "./RouterContext.js";
|
|
|
7
7
|
import { StyleSheet } from 'react-native';
|
|
8
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
9
|
export const StackRenderer = /*#__PURE__*/memo(({
|
|
10
|
-
|
|
10
|
+
stackId,
|
|
11
11
|
appearance,
|
|
12
12
|
history
|
|
13
13
|
}) => {
|
|
14
14
|
const router = useRouter();
|
|
15
|
-
const stackId = stack.getId();
|
|
16
15
|
const subscribe = useCallback(cb => router.subscribeStack(stackId, cb), [router, stackId]);
|
|
17
16
|
const get = useCallback(() => router.getStackHistory(stackId), [router, stackId]);
|
|
18
17
|
const historyFromStore = useSyncExternalStore(subscribe, get, get);
|
|
@@ -141,7 +141,7 @@ const TabStackRenderer = /*#__PURE__*/memo(({
|
|
|
141
141
|
const history = useSyncExternalStore(subscribe, get, get);
|
|
142
142
|
return /*#__PURE__*/_jsx(StackRenderer, {
|
|
143
143
|
appearance: appearance,
|
|
144
|
-
|
|
144
|
+
stackId: stackId,
|
|
145
145
|
history: history
|
|
146
146
|
});
|
|
147
147
|
});
|
|
@@ -2,5 +2,5 @@ import type { StackPresentationTypes } from '../types';
|
|
|
2
2
|
import type { PresentationTypeClass, AnimationType } from './ScreenStackContext';
|
|
3
3
|
export declare function getPresentationTypeClass(presentation: StackPresentationTypes): PresentationTypeClass;
|
|
4
4
|
export declare function getAnimationTypeForPresentation(presentation: StackPresentationTypes, isEntering: boolean, direction: 'forward' | 'back'): string;
|
|
5
|
-
export declare function computeAnimationType(_key: string, isInStack: boolean, isTop: boolean, direction: 'forward' | 'back', presentation: StackPresentationTypes, isInitialPhase: boolean, animated?: boolean): AnimationType;
|
|
5
|
+
export declare function computeAnimationType(_key: string, isInStack: boolean, isTop: boolean, direction: 'forward' | 'back', presentation: StackPresentationTypes, isInitialPhase: boolean, animated?: boolean, isBulkRemoval?: boolean): AnimationType;
|
|
6
6
|
//# sourceMappingURL=animationHelpers.d.ts.map
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { HistoryItem, NavigationAppearance } from './types';
|
|
2
|
-
import { NavigationStack } from './NavigationStack';
|
|
3
2
|
export interface StackRendererProps {
|
|
4
|
-
|
|
3
|
+
stackId: string;
|
|
5
4
|
appearance?: NavigationAppearance;
|
|
6
5
|
history?: HistoryItem[];
|
|
7
6
|
}
|