@sigmela/router 0.2.5 → 0.2.7
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]);
|
|
@@ -412,6 +466,9 @@ export const ScreenStack = /*#__PURE__*/memo(props => {
|
|
|
412
466
|
children: /*#__PURE__*/_jsx("div", {
|
|
413
467
|
ref: containerRef,
|
|
414
468
|
className: containerClassName + (animating ? ' animating' : ''),
|
|
469
|
+
style: {
|
|
470
|
+
'--stack-transition-time': `${transitionTime}ms`
|
|
471
|
+
},
|
|
415
472
|
children: keysToRender.map(key => {
|
|
416
473
|
const transitionState = stateMap.get(key);
|
|
417
474
|
if (!transitionState || !transitionState.isMounted) {
|
package/lib/module/styles.css
CHANGED
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
|
|
18
18
|
/* Base transition for opening screen */
|
|
19
19
|
transition:
|
|
20
|
-
transform
|
|
21
|
-
|
|
20
|
+
transform var(--stack-transition-time, 300ms)
|
|
21
|
+
cubic-bezier(0.22, 0.61, 0.36, 1),
|
|
22
|
+
filter var(--stack-transition-time, 300ms)
|
|
23
|
+
cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/* Inner container for regular screen */
|
|
@@ -43,6 +45,8 @@
|
|
|
43
45
|
pointer-events: none;
|
|
44
46
|
z-index: 1;
|
|
45
47
|
will-change: opacity;
|
|
48
|
+
transition: opacity var(--stack-transition-time, 300ms)
|
|
49
|
+
cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/* Show overlay only for modal-like presentations.
|
|
@@ -53,26 +57,6 @@
|
|
|
53
57
|
display: none;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
/* Keyframes for overlay appearance */
|
|
57
|
-
@keyframes modal-overlay-enter {
|
|
58
|
-
from {
|
|
59
|
-
opacity: 0;
|
|
60
|
-
}
|
|
61
|
-
to {
|
|
62
|
-
opacity: 0.5;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/* Keyframes for overlay disappearance */
|
|
67
|
-
@keyframes modal-overlay-exit {
|
|
68
|
-
from {
|
|
69
|
-
opacity: 0.5;
|
|
70
|
-
}
|
|
71
|
-
to {
|
|
72
|
-
opacity: 0;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
60
|
/* Overlay in initial state — transparent */
|
|
77
61
|
.screen-stack-item.modal.transition-preEnter > .stack-modal-overlay,
|
|
78
62
|
.screen-stack-item.modal-right.transition-preEnter > .stack-modal-overlay,
|
|
@@ -86,7 +70,7 @@
|
|
|
86
70
|
pointer-events: none;
|
|
87
71
|
}
|
|
88
72
|
|
|
89
|
-
/* Overlay on enter —
|
|
73
|
+
/* Overlay on enter — fade in via transition */
|
|
90
74
|
.screen-stack-item.modal.transition-entering > .stack-modal-overlay,
|
|
91
75
|
.screen-stack-item.modal.phase-active.transition-preEnter > .stack-modal-overlay,
|
|
92
76
|
.screen-stack-item.modal-right.transition-entering > .stack-modal-overlay,
|
|
@@ -103,7 +87,7 @@
|
|
|
103
87
|
.screen-stack-item.sheet.phase-active.transition-preEnter > .stack-modal-overlay {
|
|
104
88
|
background: rgba(0, 0, 0, 0.5);
|
|
105
89
|
pointer-events: none;
|
|
106
|
-
|
|
90
|
+
opacity: 0.5;
|
|
107
91
|
}
|
|
108
92
|
|
|
109
93
|
/* For modal-like in active / entered states — fully visible */
|
|
@@ -126,7 +110,7 @@
|
|
|
126
110
|
pointer-events: auto;
|
|
127
111
|
}
|
|
128
112
|
|
|
129
|
-
/* Overlay on modal-like close —
|
|
113
|
+
/* Overlay on modal-like close — fade out via transition */
|
|
130
114
|
.screen-stack-item.modal.phase-exiting > .stack-modal-overlay,
|
|
131
115
|
.screen-stack-item.modal.transition-exiting > .stack-modal-overlay,
|
|
132
116
|
.screen-stack-item.modal-right.phase-exiting > .stack-modal-overlay,
|
|
@@ -143,7 +127,7 @@
|
|
|
143
127
|
.screen-stack-item.sheet.transition-exiting > .stack-modal-overlay {
|
|
144
128
|
background: rgba(0, 0, 0, 0.5);
|
|
145
129
|
pointer-events: none;
|
|
146
|
-
|
|
130
|
+
opacity: 0;
|
|
147
131
|
}
|
|
148
132
|
|
|
149
133
|
/* Overlay for transparent-modal — transparent */
|
|
@@ -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;
|