@sigmela/router 0.2.5 → 0.2.6
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/Router.js
CHANGED
|
@@ -20,6 +20,7 @@ export class Router {
|
|
|
20
20
|
};
|
|
21
21
|
debugEnabled = false;
|
|
22
22
|
sheetDismissers = new Map();
|
|
23
|
+
dismissedStackIds = new Set();
|
|
23
24
|
stackListeners = new Map();
|
|
24
25
|
stackById = new Map();
|
|
25
26
|
routeById = new Map();
|
|
@@ -60,6 +61,17 @@ export class Router {
|
|
|
60
61
|
isDebugEnabled() {
|
|
61
62
|
return this.debugEnabled;
|
|
62
63
|
}
|
|
64
|
+
isStackBeingDismissed(stackId) {
|
|
65
|
+
if (!stackId) return false;
|
|
66
|
+
return this.dismissedStackIds.has(stackId);
|
|
67
|
+
}
|
|
68
|
+
clearStackDismissed(stackId) {
|
|
69
|
+
if (!stackId) return;
|
|
70
|
+
this.dismissedStackIds.delete(stackId);
|
|
71
|
+
}
|
|
72
|
+
markStackDismissed(stackId) {
|
|
73
|
+
this.dismissedStackIds.add(stackId);
|
|
74
|
+
}
|
|
63
75
|
log(message, data) {
|
|
64
76
|
if (this.debugEnabled) {
|
|
65
77
|
if (data !== undefined) {
|
|
@@ -187,6 +199,7 @@ export class Router {
|
|
|
187
199
|
childStackId,
|
|
188
200
|
modalKey: modalItem.key
|
|
189
201
|
});
|
|
202
|
+
this.markStackDismissed(childStackId);
|
|
190
203
|
const newHistory = this.state.history.filter(item => item.stackId !== childStackId && item.key !== modalItem.key);
|
|
191
204
|
this.setState({
|
|
192
205
|
history: newHistory
|
|
@@ -5,6 +5,7 @@ import { useTransitionMap } from 'react-transition-state';
|
|
|
5
5
|
import { ScreenStackItemsContext, ScreenStackAnimatingContext, useScreenStackConfig } from "./ScreenStackContext.js";
|
|
6
6
|
import { getPresentationTypeClass, computeAnimationType } from "./animationHelpers.js";
|
|
7
7
|
import { RouterContext } from "../RouterContext.js";
|
|
8
|
+
import { isModalLikePresentation } from "../types.js";
|
|
8
9
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
10
|
const isScreenStackItemElement = child => {
|
|
10
11
|
if (! /*#__PURE__*/isValidElement(child)) return false;
|
|
@@ -71,6 +72,8 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
71
72
|
const suppressedEnterKeyRef = useRef(null);
|
|
72
73
|
const isBulkRemovalRef = useRef(false);
|
|
73
74
|
const prevKeysRef = useRef([]);
|
|
75
|
+
const lastStackIdRef = useRef(null);
|
|
76
|
+
const dismissInProgressRef = useRef(false);
|
|
74
77
|
const lastDirectionRef = useRef('forward');
|
|
75
78
|
const childMapRef = useRef(new Map());
|
|
76
79
|
const stackChildren = useMemo(() => {
|
|
@@ -89,6 +92,20 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
89
92
|
});
|
|
90
93
|
return stackItems;
|
|
91
94
|
}, [children, devLog]);
|
|
95
|
+
const currentStackId = useMemo(() => {
|
|
96
|
+
let found;
|
|
97
|
+
for (const child of stackChildren) {
|
|
98
|
+
const childStackId = child.props.stackId ?? child.props.item?.stackId ?? undefined;
|
|
99
|
+
if (childStackId) {
|
|
100
|
+
found = childStackId;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (found) {
|
|
105
|
+
lastStackIdRef.current = found;
|
|
106
|
+
}
|
|
107
|
+
return found ?? lastStackIdRef.current;
|
|
108
|
+
}, [stackChildren]);
|
|
92
109
|
const routeKeys = useMemo(() => {
|
|
93
110
|
const keys = stackChildren.map(child => {
|
|
94
111
|
const item = child.props.item;
|
|
@@ -147,7 +164,6 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
147
164
|
const prevKeysForDirection = prevKeysRef.current;
|
|
148
165
|
const direction = useMemo(() => {
|
|
149
166
|
const computed = computeDirection(prevKeysForDirection, routeKeys);
|
|
150
|
-
prevKeysRef.current = routeKeys;
|
|
151
167
|
return computed;
|
|
152
168
|
}, [routeKeys, prevKeysForDirection]);
|
|
153
169
|
devLog('[ScreenStack] Computed direction', {
|
|
@@ -180,15 +196,9 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
180
196
|
|
|
181
197
|
// CRITICAL: Calculate bulk removal BEFORE useMemo for itemsContextValue
|
|
182
198
|
// so the flag is available when computing animation types
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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;
|
|
199
|
+
// Use prevKeysForDirection (captured before direction useMemo) to detect removals
|
|
200
|
+
const removedCount = prevKeysForDirection.filter(key => !routeKeys.includes(key)).length;
|
|
201
|
+
const isBulkRemoval = removedCount > 1 || routeKeys.length === 0 && prevKeysForDirection.length > 1;
|
|
192
202
|
isBulkRemovalRef.current = isBulkRemoval;
|
|
193
203
|
useLayoutEffect(() => {
|
|
194
204
|
devLog('[ScreenStack] === LIFECYCLE EFFECT START ===', {
|
|
@@ -255,7 +265,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
255
265
|
}
|
|
256
266
|
lastDirectionRef.current = direction;
|
|
257
267
|
devLog('[ScreenStack] === LIFECYCLE EFFECT END ===');
|
|
258
|
-
}, [routeKeys, direction, setItem, toggle, stateMapEntries, stateMap, animateFirstScreenAfterEmpty, devLog]);
|
|
268
|
+
}, [routeKeys, direction, prevKeysForDirection.length, setItem, toggle, stateMapEntries, stateMap, animateFirstScreenAfterEmpty, devLog]);
|
|
259
269
|
useLayoutEffect(() => {
|
|
260
270
|
devLog('[ScreenStack] === CLEANUP EFFECT START ===');
|
|
261
271
|
const routeKeySet = new Set(routeKeys);
|
|
@@ -282,6 +292,12 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
282
292
|
}
|
|
283
293
|
devLog('[ScreenStack] === CLEANUP EFFECT END ===');
|
|
284
294
|
}, [routeKeys, stateMapEntries, deleteItem, devLog]);
|
|
295
|
+
|
|
296
|
+
// Update the previous keys after all layout effects so direction calculations
|
|
297
|
+
// always compare against the last committed stack.
|
|
298
|
+
useLayoutEffect(() => {
|
|
299
|
+
prevKeysRef.current = routeKeys;
|
|
300
|
+
}, [routeKeys]);
|
|
285
301
|
useEffect(() => {
|
|
286
302
|
if (!isInitialMountRef.current) return;
|
|
287
303
|
const hasMountedItem = stateMapEntries.some(([, st]) => st.isMounted);
|
|
@@ -327,6 +343,32 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
327
343
|
});
|
|
328
344
|
const topKey = routeKeys[routeKeys.length - 1] ?? null;
|
|
329
345
|
const routeKeySet = useMemo(() => new Set(routeKeys), [routeKeys]);
|
|
346
|
+
const hasExitingItems = useMemo(() => {
|
|
347
|
+
return stateMapEntries.some(([key, state]) => state.isMounted && !routeKeySet.has(key));
|
|
348
|
+
}, [stateMapEntries, routeKeySet]);
|
|
349
|
+
const hasExitingModalLike = useMemo(() => {
|
|
350
|
+
return stateMapEntries.some(([key, state]) => {
|
|
351
|
+
if (!state.isMounted || routeKeySet.has(key)) return false;
|
|
352
|
+
const item = childMap.get(key)?.props.item;
|
|
353
|
+
return isModalLikePresentation(item?.options?.stackPresentation);
|
|
354
|
+
});
|
|
355
|
+
}, [childMap, routeKeySet, stateMapEntries]);
|
|
356
|
+
const animationDirection = hasExitingItems ? lastDirectionRef.current : direction;
|
|
357
|
+
const stackDismissedByRouter = router?.isStackBeingDismissed?.(currentStackId ?? undefined) ?? false;
|
|
358
|
+
if (stackDismissedByRouter) {
|
|
359
|
+
dismissInProgressRef.current = true;
|
|
360
|
+
}
|
|
361
|
+
const isStackBeingDismissed = dismissInProgressRef.current;
|
|
362
|
+
devLog('[ScreenStack] Dismiss state', {
|
|
363
|
+
stackId: currentStackId,
|
|
364
|
+
isStackBeingDismissed
|
|
365
|
+
});
|
|
366
|
+
useLayoutEffect(() => {
|
|
367
|
+
if (!isStackBeingDismissed) return;
|
|
368
|
+
if (hasExitingItems) return;
|
|
369
|
+
dismissInProgressRef.current = false;
|
|
370
|
+
router?.clearStackDismissed?.(currentStackId ?? undefined);
|
|
371
|
+
}, [hasExitingItems, isStackBeingDismissed, currentStackId, router]);
|
|
330
372
|
const itemsContextValue = useMemo(() => {
|
|
331
373
|
const items = {};
|
|
332
374
|
for (let index = 0; index < keysToRender.length; index++) {
|
|
@@ -354,7 +396,13 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
354
396
|
const routeIndex = routeKeys.indexOf(key);
|
|
355
397
|
const zIndex = routeIndex >= 0 ? routeIndex + 1 : keysToRender.length + index + 1;
|
|
356
398
|
const presentationType = getPresentationTypeClass(presentation);
|
|
357
|
-
let animationType = computeAnimationType(key, isInStack, isTop,
|
|
399
|
+
let animationType = computeAnimationType(key, isInStack, isTop, animationDirection, presentation, isInitialPhase, animated, isBulkRemovalRef.current);
|
|
400
|
+
if (hasExitingModalLike && isTop && isInStack) {
|
|
401
|
+
animationType = 'none';
|
|
402
|
+
}
|
|
403
|
+
if (isStackBeingDismissed && !isInStack) {
|
|
404
|
+
animationType = 'no-animate';
|
|
405
|
+
}
|
|
358
406
|
|
|
359
407
|
// SplitView-secondary-only: suppress enter animation for the first screen after empty.
|
|
360
408
|
if (!animateFirstScreenAfterEmpty && isTop && direction === 'forward' && suppressedEnterKeyRef.current === key) {
|
|
@@ -386,7 +434,13 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
386
434
|
phase = 'inactive';
|
|
387
435
|
}
|
|
388
436
|
const presentationType = getPresentationTypeClass(presentation);
|
|
389
|
-
let animationType = isInitialPhase ? 'none' : computeAnimationType(key, isInStack, isTop,
|
|
437
|
+
let animationType = isInitialPhase ? 'none' : computeAnimationType(key, isInStack, isTop, animationDirection, presentation, isInitialPhase, animated, isBulkRemovalRef.current);
|
|
438
|
+
if (hasExitingModalLike && isTop && isInStack) {
|
|
439
|
+
animationType = 'none';
|
|
440
|
+
}
|
|
441
|
+
if (isStackBeingDismissed && !isInStack) {
|
|
442
|
+
animationType = 'no-animate';
|
|
443
|
+
}
|
|
390
444
|
if (!animateFirstScreenAfterEmpty && isTop && direction === 'forward' && suppressedEnterKeyRef.current === key) {
|
|
391
445
|
animationType = 'none';
|
|
392
446
|
}
|
|
@@ -401,7 +455,7 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
401
455
|
return {
|
|
402
456
|
items
|
|
403
457
|
};
|
|
404
|
-
}, [keysToRender, stateMap, childMap, routeKeySet, topKey, isInitialPhase, routeKeys, direction, animateFirstScreenAfterEmpty]);
|
|
458
|
+
}, [keysToRender, stateMap, childMap, routeKeySet, topKey, isInitialPhase, routeKeys, animationDirection, direction, animateFirstScreenAfterEmpty, isStackBeingDismissed, hasExitingModalLike]);
|
|
405
459
|
const animating = useMemo(() => {
|
|
406
460
|
return stateMapEntries.some(([, state]) => state.isMounted && (state.status === 'entering' || state.status === 'exiting' || state.status === 'preEnter' || state.status === 'preExit'));
|
|
407
461
|
}, [stateMapEntries]);
|
|
@@ -21,6 +21,7 @@ export declare class Router {
|
|
|
21
21
|
private readonly routerScreenOptions;
|
|
22
22
|
private readonly debugEnabled;
|
|
23
23
|
private sheetDismissers;
|
|
24
|
+
private dismissedStackIds;
|
|
24
25
|
private stackListeners;
|
|
25
26
|
private stackById;
|
|
26
27
|
private routeById;
|
|
@@ -35,6 +36,9 @@ export declare class Router {
|
|
|
35
36
|
private navigationToken;
|
|
36
37
|
constructor(config: RouterConfig);
|
|
37
38
|
isDebugEnabled(): boolean;
|
|
39
|
+
isStackBeingDismissed(stackId?: string): boolean;
|
|
40
|
+
clearStackDismissed(stackId?: string): void;
|
|
41
|
+
private markStackDismissed;
|
|
38
42
|
private log;
|
|
39
43
|
navigate: (path: string) => void;
|
|
40
44
|
replace: (path: string, dedupe?: boolean) => void;
|