@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.
@@ -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
- stack: stackInstance,
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 prevKeys = prevKeysRef.current;
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) {
@@ -38,7 +38,7 @@ const StackSliceRenderer = /*#__PURE__*/memo(({
38
38
  }
39
39
  return /*#__PURE__*/_jsx(StackRenderer, {
40
40
  appearance: appearance,
41
- stack: stack,
41
+ stackId: stackId,
42
42
  history: historyToRender
43
43
  });
44
44
  });
@@ -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
- stack,
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
- stack: stack,
144
+ stackId: stackId,
145
145
  history: history
146
146
  });
147
147
  });
@@ -29,7 +29,7 @@ const TabStackRenderer = /*#__PURE__*/memo(({
29
29
  const history = useSyncExternalStore(subscribe, get, get);
30
30
  return /*#__PURE__*/_jsx(StackRenderer, {
31
31
  appearance: appearance,
32
- stack: stack,
32
+ stackId: stackId,
33
33
  history: history
34
34
  });
35
35
  });
@@ -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
- stack: NavigationStack;
3
+ stackId: string;
5
4
  appearance?: NavigationAppearance;
6
5
  history?: HistoryItem[];
7
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigmela/router",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "React Native Router",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",