react-panel-layout 0.6.0 → 0.6.1
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/dist/{FloatingPanelFrame-SgYLc6Ud.js → FloatingPanelFrame-3eU9AwPo.js} +2 -2
- package/dist/{FloatingPanelFrame-SgYLc6Ud.js.map → FloatingPanelFrame-3eU9AwPo.js.map} +1 -1
- package/dist/FloatingWindow-CUXnEtrb.js +827 -0
- package/dist/FloatingWindow-CUXnEtrb.js.map +1 -0
- package/dist/FloatingWindow-DMwyK0eK.cjs +2 -0
- package/dist/FloatingWindow-DMwyK0eK.cjs.map +1 -0
- package/dist/GridLayout-DKTg_N61.cjs +2 -0
- package/dist/{GridLayout-B4VRsC0r.cjs.map → GridLayout-DKTg_N61.cjs.map} +1 -1
- package/dist/{GridLayout-BltqeCPK.js → GridLayout-UWNxXw77.js} +34 -35
- package/dist/{GridLayout-BltqeCPK.js.map → GridLayout-UWNxXw77.js.map} +1 -1
- package/dist/{HorizontalDivider-WF1k_qND.js → HorizontalDivider-DdxzfV0l.js} +3 -3
- package/dist/{HorizontalDivider-WF1k_qND.js.map → HorizontalDivider-DdxzfV0l.js.map} +1 -1
- package/dist/{HorizontalDivider-B5Z-KZLk.cjs → HorizontalDivider-_pgV4Mcv.cjs} +2 -2
- package/dist/{HorizontalDivider-B5Z-KZLk.cjs.map → HorizontalDivider-_pgV4Mcv.cjs.map} +1 -1
- package/dist/{PanelSystem-Dr1TBhxM.js → PanelSystem-BqUzNtf2.js} +5 -5
- package/dist/{PanelSystem-Dr1TBhxM.js.map → PanelSystem-BqUzNtf2.js.map} +1 -1
- package/dist/{PanelSystem-Bs8bQwQF.cjs → PanelSystem-D603LKKv.cjs} +2 -2
- package/dist/{PanelSystem-Bs8bQwQF.cjs.map → PanelSystem-D603LKKv.cjs.map} +1 -1
- package/dist/ResizeHandle-CBcAS918.cjs +2 -0
- package/dist/{ResizeHandle-CScipO5l.cjs.map → ResizeHandle-CBcAS918.cjs.map} +1 -1
- package/dist/{ResizeHandle-CdA_JYfN.js → ResizeHandle-CXjc1meV.js} +28 -29
- package/dist/{ResizeHandle-CdA_JYfN.js.map → ResizeHandle-CXjc1meV.js.map} +1 -1
- package/dist/SwipePivotTabBar-DWrCuwEI.js +411 -0
- package/dist/SwipePivotTabBar-DWrCuwEI.js.map +1 -0
- package/dist/SwipePivotTabBar-fjjXkpj7.cjs +2 -0
- package/dist/SwipePivotTabBar-fjjXkpj7.cjs.map +1 -0
- package/dist/components/gesture/SwipeSafeZone.d.ts +40 -0
- package/dist/components/window/Drawer.d.ts +3 -1
- package/dist/components/window/DrawerLayers.d.ts +1 -1
- package/dist/components/window/drawerStyles.d.ts +69 -0
- package/dist/components/window/drawerSwipeConfig.d.ts +29 -0
- package/dist/components/window/useDrawerSwipeTransform.d.ts +23 -0
- package/dist/config.cjs +1 -1
- package/dist/config.js +3 -3
- package/dist/constants/styles.d.ts +17 -0
- package/dist/dialog/index.d.ts +69 -0
- package/dist/floating.js +1 -1
- package/dist/grid.cjs +1 -1
- package/dist/grid.js +2 -2
- package/dist/hooks/gesture/testing/createGestureSimulator.d.ts +7 -0
- package/dist/hooks/gesture/types.d.ts +48 -5
- package/dist/hooks/gesture/utils.d.ts +19 -0
- package/dist/hooks/useAnimationFrame.d.ts +2 -0
- package/dist/hooks/useOperationContinuity.d.ts +64 -0
- package/dist/hooks/useResizeObserver.d.ts +33 -1
- package/dist/hooks/useSharedElementTransition.d.ts +112 -0
- package/dist/hooks/useSwipeContentTransform.d.ts +9 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +7 -7
- package/dist/modules/dialog/AlertDialog.d.ts +9 -0
- package/dist/modules/dialog/DialogContainer.d.ts +37 -0
- package/dist/modules/dialog/Modal.d.ts +26 -0
- package/dist/modules/dialog/SwipeDialogContainer.d.ts +16 -0
- package/dist/modules/dialog/dialogAnimationUtils.d.ts +113 -0
- package/dist/modules/dialog/types.d.ts +183 -0
- package/dist/modules/dialog/useDialog.d.ts +39 -0
- package/dist/modules/dialog/useDialogContainer.d.ts +47 -0
- package/dist/modules/dialog/useDialogSwipeInput.d.ts +70 -0
- package/dist/modules/dialog/useDialogTransform.d.ts +82 -0
- package/dist/modules/drawer/types.d.ts +74 -0
- package/dist/modules/drawer/useDrawerSwipeInput.d.ts +24 -0
- package/dist/modules/pivot/SwipePivotTabBar.d.ts +3 -0
- package/dist/modules/stack/SwipeStackContent.d.ts +6 -3
- package/dist/modules/stack/SwipeStackOutlet.d.ts +4 -4
- package/dist/modules/stack/computeSwipeStackTransform.d.ts +1 -1
- package/dist/panels.cjs +1 -1
- package/dist/panels.js +1 -1
- package/dist/pivot.cjs +1 -1
- package/dist/pivot.js +1 -1
- package/dist/resizer.cjs +1 -1
- package/dist/resizer.js +2 -2
- package/dist/stack.cjs +1 -1
- package/dist/stack.cjs.map +1 -1
- package/dist/stack.js +503 -762
- package/dist/stack.js.map +1 -1
- package/dist/sticky-header/calculateStickyMetrics.d.ts +28 -0
- package/dist/sticky-header.cjs +1 -1
- package/dist/sticky-header.cjs.map +1 -1
- package/dist/sticky-header.js +59 -51
- package/dist/sticky-header.js.map +1 -1
- package/dist/{styles-DPPuJ0sf.js → styles-NkjuMOVS.js} +13 -13
- package/dist/{styles-DPPuJ0sf.js.map → styles-NkjuMOVS.js.map} +1 -1
- package/dist/styles-qf6ptVLD.cjs.map +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/useDocumentPointerEvents-DXxw3qWj.js +54 -0
- package/dist/useDocumentPointerEvents-DXxw3qWj.js.map +1 -0
- package/dist/useDocumentPointerEvents-DxDSOtip.cjs +2 -0
- package/dist/useDocumentPointerEvents-DxDSOtip.cjs.map +1 -0
- package/dist/useNativeGestureGuard-C7TSqEkr.cjs +2 -0
- package/dist/useNativeGestureGuard-C7TSqEkr.cjs.map +1 -0
- package/dist/useNativeGestureGuard-CGYo6O0r.js +347 -0
- package/dist/useNativeGestureGuard-CGYo6O0r.js.map +1 -0
- package/dist/window/index.d.ts +2 -0
- package/dist/window.cjs +1 -1
- package/dist/window.cjs.map +1 -1
- package/dist/window.js +114 -103
- package/dist/window.js.map +1 -1
- package/package.json +6 -1
- package/src/components/gesture/SwipeSafeZone.tsx +69 -0
- package/src/components/window/Drawer.tsx +249 -162
- package/src/components/window/DrawerLayers.tsx +13 -3
- package/src/components/window/drawerStyles.spec.ts +263 -0
- package/src/components/window/drawerStyles.ts +228 -0
- package/src/components/window/drawerSwipeConfig.spec.ts +131 -0
- package/src/components/window/drawerSwipeConfig.ts +112 -0
- package/src/components/window/useDrawerSwipeTransform.spec.ts +234 -0
- package/src/components/window/useDrawerSwipeTransform.ts +129 -0
- package/src/constants/styles.ts +19 -0
- package/src/demo/pages/Dialog/alerts/index.tsx +22 -0
- package/src/demo/pages/Dialog/card/index.tsx +22 -0
- package/src/demo/pages/Dialog/components/AlertDialogDemo.tsx +124 -0
- package/src/demo/pages/Dialog/components/CardExpandDemo.module.css +243 -0
- package/src/demo/pages/Dialog/components/CardExpandDemo.tsx +204 -0
- package/src/demo/pages/Dialog/components/CustomAlertDialogDemo.tsx +219 -0
- package/src/demo/pages/Dialog/components/DialogDemos.module.css +77 -0
- package/src/demo/pages/Dialog/components/ModalBasics.tsx +45 -0
- package/src/demo/pages/Dialog/components/SwipeDialogDemo.module.css +77 -0
- package/src/demo/pages/Dialog/components/SwipeDialogDemo.tsx +181 -0
- package/src/demo/pages/Dialog/custom-alert/index.tsx +22 -0
- package/src/demo/pages/Dialog/modal/index.tsx +17 -0
- package/src/demo/pages/Dialog/swipe/index.tsx +22 -0
- package/src/demo/pages/Drawer/components/DrawerSwipe.module.css +316 -0
- package/src/demo/pages/Drawer/components/DrawerSwipe.tsx +178 -0
- package/src/demo/pages/Drawer/swipe/index.tsx +17 -0
- package/src/demo/pages/Pivot/components/SwipeTabsPivot.tsx +54 -23
- package/src/demo/pages/Pivot/swipe-debug/index.tsx +1 -1
- package/src/demo/pages/Stack/components/StackBasics.spec.tsx +152 -0
- package/src/demo/pages/Stack/components/StackBasics.tsx +179 -95
- package/src/demo/pages/Stack/components/StackTablet.spec.tsx +120 -0
- package/src/demo/pages/Stack/components/StackTablet.tsx +42 -21
- package/src/demo/routes.tsx +22 -1
- package/src/dialog/index.ts +85 -0
- package/src/hooks/gesture/testing/createGestureSimulator.spec.ts +68 -64
- package/src/hooks/gesture/testing/createGestureSimulator.ts +112 -37
- package/src/hooks/gesture/types.ts +83 -6
- package/src/hooks/gesture/useEdgeSwipeInput.spec.ts +22 -14
- package/src/hooks/gesture/useNativeGestureGuard.spec.ts +91 -31
- package/src/hooks/gesture/useNativeGestureGuard.ts +3 -1
- package/src/hooks/gesture/utils.ts +91 -0
- package/src/hooks/useAnimatedVisibility.spec.ts +44 -24
- package/src/hooks/useAnimatedVisibility.ts +28 -2
- package/src/hooks/useAnimationFrame.ts +8 -0
- package/src/hooks/useOperationContinuity.spec.ts +387 -0
- package/src/hooks/useOperationContinuity.ts +135 -0
- package/src/hooks/useResizeObserver.spec.tsx +277 -0
- package/src/hooks/useResizeObserver.tsx +108 -39
- package/src/hooks/useScrollContainer.ts +4 -10
- package/src/hooks/useSharedElementTransition.ts +333 -0
- package/src/hooks/useSwipeContentTransform.spec.ts +18 -18
- package/src/hooks/useSwipeContentTransform.ts +166 -28
- package/src/modules/dialog/AlertDialog.spec.tsx +387 -0
- package/src/modules/dialog/AlertDialog.tsx +221 -0
- package/src/modules/dialog/DialogContainer.spec.tsx +228 -0
- package/src/modules/dialog/DialogContainer.tsx +188 -0
- package/src/modules/dialog/Modal.spec.tsx +220 -0
- package/src/modules/dialog/Modal.tsx +182 -0
- package/src/modules/dialog/SwipeDialogContainer.tsx +208 -0
- package/src/modules/dialog/dialogAnimationUtils.spec.ts +253 -0
- package/src/modules/dialog/dialogAnimationUtils.ts +297 -0
- package/src/modules/dialog/types.ts +186 -0
- package/src/modules/dialog/useDialog.spec.tsx +447 -0
- package/src/modules/dialog/useDialog.ts +214 -0
- package/src/modules/dialog/useDialogContainer.spec.ts +331 -0
- package/src/modules/dialog/useDialogContainer.ts +150 -0
- package/src/modules/dialog/useDialogSwipeInput.spec.ts +157 -0
- package/src/modules/dialog/useDialogSwipeInput.ts +319 -0
- package/src/modules/dialog/useDialogTransform.spec.ts +370 -0
- package/src/modules/dialog/useDialogTransform.ts +407 -0
- package/src/modules/drawer/types.ts +102 -0
- package/src/modules/drawer/useDrawerSwipeInput.spec.ts +566 -0
- package/src/modules/drawer/useDrawerSwipeInput.ts +399 -0
- package/src/modules/panels/rendering/ContentRegistry.spec.tsx +21 -14
- package/src/modules/pivot/SwipePivotContent.position.spec.tsx +12 -8
- package/src/modules/pivot/SwipePivotContent.spec.tsx +55 -25
- package/src/modules/pivot/SwipePivotContent.tsx +2 -2
- package/src/modules/pivot/SwipePivotTabBar.spec.tsx +85 -68
- package/src/modules/pivot/SwipePivotTabBar.tsx +75 -15
- package/src/modules/pivot/scaleInputState.spec.ts +11 -2
- package/src/modules/pivot/usePivot.spec.ts +17 -3
- package/src/modules/pivot/usePivotSwipeInput.spec.ts +182 -123
- package/src/modules/stack/SwipeStackContent.spec.tsx +387 -100
- package/src/modules/stack/SwipeStackContent.tsx +43 -33
- package/src/modules/stack/SwipeStackOutlet.spec.tsx +14 -16
- package/src/modules/stack/SwipeStackOutlet.tsx +6 -6
- package/src/modules/stack/computeSwipeStackTransform.spec.ts +5 -5
- package/src/modules/stack/computeSwipeStackTransform.ts +3 -3
- package/src/modules/stack/swipeTransitionContinuity.spec.tsx +1133 -0
- package/src/modules/stack/useStackAnimationState.spec.ts +3 -1
- package/src/modules/stack/useStackAnimationState.ts +18 -13
- package/src/modules/stack/useStackNavigation.spec.ts +198 -3
- package/src/modules/stack/useStackNavigation.tsx +113 -56
- package/src/modules/stack/useStackSwipeInput.spec.ts +65 -32
- package/src/modules/stack/useStackSwipeInput.ts +1 -1
- package/src/sticky-header/StickyArea.tsx +29 -57
- package/src/sticky-header/calculateStickyMetrics.spec.ts +105 -0
- package/src/sticky-header/calculateStickyMetrics.ts +50 -0
- package/src/types.ts +18 -0
- package/src/window/index.ts +2 -0
- package/dist/FloatingWindow-BpdOpg_L.js +0 -400
- package/dist/FloatingWindow-BpdOpg_L.js.map +0 -1
- package/dist/FloatingWindow-TCDNY5gE.cjs +0 -2
- package/dist/FloatingWindow-TCDNY5gE.cjs.map +0 -1
- package/dist/GridLayout-B4VRsC0r.cjs +0 -2
- package/dist/ResizeHandle-CScipO5l.cjs +0 -2
- package/dist/SwipePivotTabBar-BGO9X94m.js +0 -407
- package/dist/SwipePivotTabBar-BGO9X94m.js.map +0 -1
- package/dist/SwipePivotTabBar-BrQismcZ.cjs +0 -2
- package/dist/SwipePivotTabBar-BrQismcZ.cjs.map +0 -1
- package/dist/useDocumentPointerEvents-CKdhGXd0.js +0 -46
- package/dist/useDocumentPointerEvents-CKdhGXd0.js.map +0 -1
- package/dist/useDocumentPointerEvents-ChqrKXDk.cjs +0 -2
- package/dist/useDocumentPointerEvents-ChqrKXDk.cjs.map +0 -1
- package/dist/useEffectEvent-Dp7HLCf0.js +0 -13
- package/dist/useEffectEvent-Dp7HLCf0.js.map +0 -1
- package/dist/useEffectEvent-huSsGUnl.cjs +0 -2
- package/dist/useEffectEvent-huSsGUnl.cjs.map +0 -1
|
@@ -39,6 +39,22 @@ export function computeAnimatedPanels(
|
|
|
39
39
|
prevStack: ReadonlyArray<string>,
|
|
40
40
|
currentStack: ReadonlyArray<string>,
|
|
41
41
|
): ReadonlyArray<AnimatedPanel> {
|
|
42
|
+
const getPanelPhase = (
|
|
43
|
+
wasInPrevStack: boolean,
|
|
44
|
+
prevStackLength: number,
|
|
45
|
+
prevPhase: PanelAnimationPhase | undefined,
|
|
46
|
+
): PanelAnimationPhase => {
|
|
47
|
+
if (!wasInPrevStack && prevStackLength > 0) {
|
|
48
|
+
// New panel pushed onto stack
|
|
49
|
+
return "entering";
|
|
50
|
+
}
|
|
51
|
+
if (prevPhase === "entering") {
|
|
52
|
+
// Was entering, keep entering until animation completes
|
|
53
|
+
return "entering";
|
|
54
|
+
}
|
|
55
|
+
return "active";
|
|
56
|
+
};
|
|
57
|
+
|
|
42
58
|
const result: AnimatedPanel[] = [];
|
|
43
59
|
const currentStackSet = new Set(currentStack);
|
|
44
60
|
|
|
@@ -47,17 +63,7 @@ export function computeAnimatedPanels(
|
|
|
47
63
|
const id = currentStack[i];
|
|
48
64
|
const wasInPrevStack = prevStack.includes(id);
|
|
49
65
|
const prevPanel = prevPanels.find((p) => p.id === id);
|
|
50
|
-
|
|
51
|
-
let phase: PanelAnimationPhase;
|
|
52
|
-
if (!wasInPrevStack && prevStack.length > 0) {
|
|
53
|
-
// New panel pushed onto stack
|
|
54
|
-
phase = "entering";
|
|
55
|
-
} else if (prevPanel?.phase === "entering") {
|
|
56
|
-
// Was entering, keep entering until animation completes
|
|
57
|
-
phase = "entering";
|
|
58
|
-
} else {
|
|
59
|
-
phase = "active";
|
|
60
|
-
}
|
|
66
|
+
const phase = getPanelPhase(wasInPrevStack, prevStack.length, prevPanel?.phase);
|
|
61
67
|
|
|
62
68
|
result.push({ id, depth: i, phase });
|
|
63
69
|
}
|
|
@@ -103,8 +109,7 @@ export function useStackAnimationState(
|
|
|
103
109
|
|
|
104
110
|
// Compute panels synchronously during render
|
|
105
111
|
const prevStack = prevStackRef.current;
|
|
106
|
-
const stackChanged =
|
|
107
|
-
prevStack.length !== stack.length || prevStack.some((id, i) => stack[i] !== id);
|
|
112
|
+
const stackChanged = prevStack.length !== stack.length ? true : prevStack.some((id, i) => stack[i] !== id);
|
|
108
113
|
|
|
109
114
|
if (stackChanged) {
|
|
110
115
|
panelsRef.current = computeAnimatedPanels(panelsRef.current, prevStack, stack);
|
|
@@ -5,6 +5,19 @@ import { renderHook, act } from "@testing-library/react";
|
|
|
5
5
|
import { useStackNavigation } from "./useStackNavigation.js";
|
|
6
6
|
import type { StackPanel } from "./types.js";
|
|
7
7
|
|
|
8
|
+
type CallTracker = {
|
|
9
|
+
calls: ReadonlyArray<ReadonlyArray<unknown>>;
|
|
10
|
+
fn: (...args: ReadonlyArray<unknown>) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const createCallTracker = (): CallTracker => {
|
|
14
|
+
const calls: Array<ReadonlyArray<unknown>> = [];
|
|
15
|
+
const fn = (...args: ReadonlyArray<unknown>): void => {
|
|
16
|
+
calls.push(args);
|
|
17
|
+
};
|
|
18
|
+
return { calls, fn };
|
|
19
|
+
};
|
|
20
|
+
|
|
8
21
|
describe("useStackNavigation", () => {
|
|
9
22
|
const createPanels = (): ReadonlyArray<StackPanel<"root" | "list" | "detail" | "edit">> => [
|
|
10
23
|
{ id: "root", title: "Root", content: "Root Content" },
|
|
@@ -94,16 +107,18 @@ describe("useStackNavigation", () => {
|
|
|
94
107
|
|
|
95
108
|
it("calls onPanelChange when pushing", () => {
|
|
96
109
|
const panels = createPanels();
|
|
97
|
-
const onPanelChange =
|
|
110
|
+
const onPanelChange = createCallTracker();
|
|
98
111
|
const { result } = renderHook(() =>
|
|
99
|
-
useStackNavigation({ panels, displayMode: "overlay", onPanelChange }),
|
|
112
|
+
useStackNavigation({ panels, displayMode: "overlay", onPanelChange: onPanelChange.fn }),
|
|
100
113
|
);
|
|
101
114
|
|
|
102
115
|
act(() => {
|
|
103
116
|
result.current.push("list");
|
|
104
117
|
});
|
|
105
118
|
|
|
106
|
-
expect(onPanelChange).
|
|
119
|
+
expect(onPanelChange.calls).toHaveLength(1);
|
|
120
|
+
expect(onPanelChange.calls[0]?.[0]).toBe("list");
|
|
121
|
+
expect(onPanelChange.calls[0]?.[1]).toBe(1);
|
|
107
122
|
});
|
|
108
123
|
});
|
|
109
124
|
|
|
@@ -474,4 +489,184 @@ describe("useStackNavigation", () => {
|
|
|
474
489
|
expect(result.current.state.depth).toBe(0);
|
|
475
490
|
});
|
|
476
491
|
});
|
|
492
|
+
|
|
493
|
+
describe("rapid navigation stress tests", () => {
|
|
494
|
+
it("handles rapid push-go-push-go cycles correctly", () => {
|
|
495
|
+
const panels = createPanels();
|
|
496
|
+
const { result } = renderHook(() =>
|
|
497
|
+
useStackNavigation({ panels, displayMode: "overlay" }),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
// Simulate rapid clicks: push -> go back -> push -> go back (all in single act)
|
|
501
|
+
act(() => {
|
|
502
|
+
result.current.push("list");
|
|
503
|
+
result.current.go(-1);
|
|
504
|
+
result.current.push("detail");
|
|
505
|
+
result.current.go(-1);
|
|
506
|
+
result.current.push("edit");
|
|
507
|
+
result.current.go(-1);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Should end at root after all back navigations
|
|
511
|
+
expect(result.current.state.stack).toEqual(["root"]);
|
|
512
|
+
expect(result.current.state.depth).toBe(0);
|
|
513
|
+
expect(result.current.currentPanelId).toBe("root");
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("handles deep nesting then rapid back to root", () => {
|
|
517
|
+
const panels = createPanels();
|
|
518
|
+
const { result } = renderHook(() =>
|
|
519
|
+
useStackNavigation({ panels, displayMode: "overlay" }),
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
// Push to max depth
|
|
523
|
+
act(() => {
|
|
524
|
+
result.current.push("list");
|
|
525
|
+
result.current.push("detail");
|
|
526
|
+
result.current.push("edit");
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
expect(result.current.state.depth).toBe(3);
|
|
530
|
+
|
|
531
|
+
// Rapidly go back to root in single act
|
|
532
|
+
act(() => {
|
|
533
|
+
result.current.go(-1);
|
|
534
|
+
result.current.go(-1);
|
|
535
|
+
result.current.go(-1);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
expect(result.current.state.stack).toEqual(["root"]);
|
|
539
|
+
expect(result.current.state.depth).toBe(0);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("handles alternating push-go-push pattern", () => {
|
|
543
|
+
const panels = createPanels();
|
|
544
|
+
const { result } = renderHook(() =>
|
|
545
|
+
useStackNavigation({ panels, displayMode: "overlay" }),
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
act(() => {
|
|
549
|
+
result.current.push("list"); // depth 1
|
|
550
|
+
result.current.go(-1); // depth 0
|
|
551
|
+
result.current.push("list"); // depth 1
|
|
552
|
+
result.current.push("detail"); // depth 2
|
|
553
|
+
result.current.go(-1); // depth 1
|
|
554
|
+
result.current.go(-1); // depth 0
|
|
555
|
+
result.current.push("edit"); // depth 1
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
expect(result.current.state.stack).toEqual(["root", "edit"]);
|
|
559
|
+
expect(result.current.state.depth).toBe(1);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it("handles excessive back navigation attempts gracefully", () => {
|
|
563
|
+
const panels = createPanels();
|
|
564
|
+
const { result } = renderHook(() =>
|
|
565
|
+
useStackNavigation({ panels, displayMode: "overlay" }),
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
act(() => {
|
|
569
|
+
result.current.push("list");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Try to go back more times than possible
|
|
573
|
+
act(() => {
|
|
574
|
+
result.current.go(-1);
|
|
575
|
+
result.current.go(-1); // Already at root, should be ignored
|
|
576
|
+
result.current.go(-1); // Should be ignored
|
|
577
|
+
result.current.go(-1); // Should be ignored
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
expect(result.current.state.stack).toEqual(["root"]);
|
|
581
|
+
expect(result.current.state.depth).toBe(0);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("handles interleaved push/go/replace operations", () => {
|
|
585
|
+
const panels = createPanels();
|
|
586
|
+
const { result } = renderHook(() =>
|
|
587
|
+
useStackNavigation({ panels, displayMode: "overlay" }),
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
act(() => {
|
|
591
|
+
result.current.push("list");
|
|
592
|
+
result.current.replace("detail");
|
|
593
|
+
result.current.push("edit");
|
|
594
|
+
result.current.go(-1);
|
|
595
|
+
result.current.replace("list");
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
expect(result.current.state.stack).toEqual(["root", "list"]);
|
|
599
|
+
expect(result.current.currentPanelId).toBe("list");
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("maintains correct onPanelChange calls during rapid navigation", () => {
|
|
603
|
+
const panels = createPanels();
|
|
604
|
+
const onPanelChange = createCallTracker();
|
|
605
|
+
const { result } = renderHook(() =>
|
|
606
|
+
useStackNavigation({
|
|
607
|
+
panels,
|
|
608
|
+
displayMode: "overlay",
|
|
609
|
+
onPanelChange: onPanelChange.fn,
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
act(() => {
|
|
614
|
+
result.current.push("list");
|
|
615
|
+
result.current.push("detail");
|
|
616
|
+
result.current.go(-1);
|
|
617
|
+
result.current.go(-1);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Should have called onPanelChange for each navigation
|
|
621
|
+
// Final state should be at root
|
|
622
|
+
expect(result.current.state.depth).toBe(0);
|
|
623
|
+
expect(result.current.currentPanelId).toBe("root");
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it("handles move(0) after deep navigation", () => {
|
|
627
|
+
const panels = createPanels();
|
|
628
|
+
const { result } = renderHook(() =>
|
|
629
|
+
useStackNavigation({ panels, displayMode: "overlay" }),
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
act(() => {
|
|
633
|
+
result.current.push("list");
|
|
634
|
+
result.current.push("detail");
|
|
635
|
+
result.current.push("edit");
|
|
636
|
+
result.current.move(0); // Jump directly to root
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
expect(result.current.state.stack).toEqual(["root"]);
|
|
640
|
+
expect(result.current.state.depth).toBe(0);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("canGo returns correct value during rapid state changes", () => {
|
|
644
|
+
const panels = createPanels();
|
|
645
|
+
const { result } = renderHook(() =>
|
|
646
|
+
useStackNavigation({ panels, displayMode: "overlay" }),
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
act(() => {
|
|
650
|
+
result.current.push("list");
|
|
651
|
+
result.current.push("detail");
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
expect(result.current.canGo(-1)).toBe(true);
|
|
655
|
+
expect(result.current.canGo(-2)).toBe(true);
|
|
656
|
+
expect(result.current.canGo(-3)).toBe(false);
|
|
657
|
+
|
|
658
|
+
act(() => {
|
|
659
|
+
result.current.go(-1);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
expect(result.current.canGo(-1)).toBe(true);
|
|
663
|
+
expect(result.current.canGo(-2)).toBe(false);
|
|
664
|
+
|
|
665
|
+
act(() => {
|
|
666
|
+
result.current.go(-1);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
expect(result.current.canGo(-1)).toBe(false);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
477
672
|
});
|
|
@@ -16,6 +16,56 @@ import type {
|
|
|
16
16
|
import { StackContent } from "./StackContent.js";
|
|
17
17
|
import { useContentCache } from "../../hooks/useContentCache.js";
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Navigation action types for centralized state management.
|
|
21
|
+
* All navigation operations go through the reducer to avoid stale closure issues.
|
|
22
|
+
*/
|
|
23
|
+
type StackAction<TId extends string> =
|
|
24
|
+
| { type: "push"; id: TId }
|
|
25
|
+
| { type: "go"; direction: number }
|
|
26
|
+
| { type: "move"; targetDepth: number }
|
|
27
|
+
| { type: "replace"; id: TId };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reducer for stack navigation state.
|
|
31
|
+
* Centralizes all state transitions to ensure consistent behavior during rapid navigation.
|
|
32
|
+
*/
|
|
33
|
+
function stackReducer<TId extends string>(
|
|
34
|
+
state: ReadonlyArray<TId>,
|
|
35
|
+
action: StackAction<TId>,
|
|
36
|
+
): ReadonlyArray<TId> {
|
|
37
|
+
switch (action.type) {
|
|
38
|
+
case "push":
|
|
39
|
+
return [...state, action.id];
|
|
40
|
+
|
|
41
|
+
case "go": {
|
|
42
|
+
if (action.direction >= 0) {
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
const currentDepth = state.length - 1;
|
|
46
|
+
const targetDepth = currentDepth + action.direction;
|
|
47
|
+
if (targetDepth < 0) {
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
50
|
+
return state.slice(0, targetDepth + 1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case "move": {
|
|
54
|
+
if (action.targetDepth < 0 || action.targetDepth >= state.length) {
|
|
55
|
+
return state;
|
|
56
|
+
}
|
|
57
|
+
return state.slice(0, action.targetDepth + 1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case "replace": {
|
|
61
|
+
if (state.length === 0) {
|
|
62
|
+
return [action.id];
|
|
63
|
+
}
|
|
64
|
+
return [...state.slice(0, -1), action.id];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
19
69
|
/**
|
|
20
70
|
* Context for sharing stack state with Outlet component.
|
|
21
71
|
*/
|
|
@@ -108,14 +158,35 @@ export function useStackNavigation<TId extends string = string>(
|
|
|
108
158
|
onPanelChange,
|
|
109
159
|
} = options;
|
|
110
160
|
|
|
111
|
-
// Initialize stack with
|
|
112
|
-
const [
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
161
|
+
// Initialize stack with reducer for centralized state management
|
|
162
|
+
const initialId = initialPanelId ?? (panels[0]?.id as TId);
|
|
163
|
+
if (!initialId) {
|
|
164
|
+
throw new Error("useStackNavigation: No panels provided");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const [stack, dispatch] = React.useReducer(
|
|
168
|
+
stackReducer<TId>,
|
|
169
|
+
[initialId] as ReadonlyArray<TId>,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Ref for accessing current stack in callbacks without stale closure
|
|
173
|
+
const stackRef = React.useRef(stack);
|
|
174
|
+
stackRef.current = stack;
|
|
175
|
+
|
|
176
|
+
// Track previous stack for onPanelChange callback
|
|
177
|
+
const prevStackRef = React.useRef(stack);
|
|
178
|
+
React.useEffect(() => {
|
|
179
|
+
const prevStack = prevStackRef.current;
|
|
180
|
+
prevStackRef.current = stack;
|
|
181
|
+
|
|
182
|
+
if (onPanelChange && stack !== prevStack) {
|
|
183
|
+
const newDepth = stack.length - 1;
|
|
184
|
+
const newPanelId = stack[newDepth];
|
|
185
|
+
if (newPanelId !== undefined) {
|
|
186
|
+
onPanelChange(newPanelId, newDepth);
|
|
187
|
+
}
|
|
116
188
|
}
|
|
117
|
-
|
|
118
|
-
});
|
|
189
|
+
}, [stack, onPanelChange]);
|
|
119
190
|
|
|
120
191
|
// Reveal state for parent peeking
|
|
121
192
|
const [revealState, setRevealState] = React.useState<{
|
|
@@ -138,85 +209,68 @@ export function useStackNavigation<TId extends string = string>(
|
|
|
138
209
|
const currentPanelId = stack[depth] as TId;
|
|
139
210
|
const previousPanelId = depth > 0 ? stack[depth - 1] as TId : null;
|
|
140
211
|
|
|
141
|
-
//
|
|
212
|
+
// All navigation functions dispatch to reducer - no stale closure issues
|
|
142
213
|
const push = React.useCallback((id: TId) => {
|
|
143
214
|
const panel = panels.find((p) => p.id === id);
|
|
144
215
|
if (!panel) {
|
|
145
216
|
return;
|
|
146
217
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}, [panels, depth, onPanelChange]);
|
|
218
|
+
dispatch({ type: "push", id });
|
|
219
|
+
}, [panels]);
|
|
150
220
|
|
|
151
|
-
// Navigate in a direction
|
|
152
221
|
const go = React.useCallback((direction: number) => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
const targetDepth = depth + direction;
|
|
157
|
-
if (targetDepth < 0) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
setStack((prev) => prev.slice(0, targetDepth + 1));
|
|
161
|
-
const targetId = stack[targetDepth] as TId;
|
|
162
|
-
onPanelChange?.(targetId, targetDepth);
|
|
163
|
-
}, [depth, stack, onPanelChange]);
|
|
222
|
+
dispatch({ type: "go", direction });
|
|
223
|
+
}, []);
|
|
164
224
|
|
|
165
|
-
// Move to absolute depth
|
|
166
225
|
const move = React.useCallback((targetDepth: number) => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
setStack((prev) => prev.slice(0, targetDepth + 1));
|
|
171
|
-
const targetId = stack[targetDepth] as TId;
|
|
172
|
-
onPanelChange?.(targetId, targetDepth);
|
|
173
|
-
}, [stack, onPanelChange]);
|
|
226
|
+
dispatch({ type: "move", targetDepth });
|
|
227
|
+
}, []);
|
|
174
228
|
|
|
175
|
-
// Replace current panel
|
|
176
229
|
const replace = React.useCallback((id: TId) => {
|
|
177
230
|
const panel = panels.find((p) => p.id === id);
|
|
178
231
|
if (!panel) {
|
|
179
232
|
return;
|
|
180
233
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}, [panels, depth, onPanelChange]);
|
|
234
|
+
dispatch({ type: "replace", id });
|
|
235
|
+
}, [panels]);
|
|
184
236
|
|
|
185
|
-
//
|
|
237
|
+
// canGo uses stackRef for current state
|
|
186
238
|
const canGo = React.useCallback((direction: number): boolean => {
|
|
187
239
|
if (direction >= 0) {
|
|
188
|
-
return false;
|
|
240
|
+
return false;
|
|
189
241
|
}
|
|
190
|
-
const
|
|
191
|
-
return
|
|
192
|
-
}, [
|
|
242
|
+
const currentDepth = stackRef.current.length - 1;
|
|
243
|
+
return currentDepth + direction >= 0;
|
|
244
|
+
}, []);
|
|
193
245
|
|
|
194
|
-
// Reveal
|
|
246
|
+
// Reveal functions use stackRef for current depth
|
|
195
247
|
const revealParent = React.useCallback((targetDepth?: number) => {
|
|
196
|
-
const
|
|
197
|
-
|
|
248
|
+
const currentDepth = stackRef.current.length - 1;
|
|
249
|
+
const revealTo = targetDepth ?? currentDepth - 1;
|
|
250
|
+
if (revealTo < 0 || revealTo >= currentDepth) {
|
|
198
251
|
return;
|
|
199
252
|
}
|
|
200
253
|
setRevealState({ isRevealing: true, revealDepth: revealTo });
|
|
201
|
-
}, [
|
|
254
|
+
}, []);
|
|
202
255
|
|
|
203
|
-
// Reveal root panel
|
|
204
256
|
const revealRoot = React.useCallback(() => {
|
|
205
|
-
|
|
257
|
+
const currentDepth = stackRef.current.length - 1;
|
|
258
|
+
if (currentDepth === 0) {
|
|
206
259
|
return;
|
|
207
260
|
}
|
|
208
261
|
setRevealState({ isRevealing: true, revealDepth: 0 });
|
|
209
|
-
}, [
|
|
262
|
+
}, []);
|
|
210
263
|
|
|
211
|
-
// Dismiss reveal
|
|
212
264
|
const dismissReveal = React.useCallback(() => {
|
|
213
265
|
setRevealState({ isRevealing: false, revealDepth: null });
|
|
214
266
|
}, []);
|
|
215
267
|
|
|
216
|
-
//
|
|
268
|
+
// getPanelProps uses stackRef for current state
|
|
217
269
|
const getPanelProps = React.useCallback((id: TId): StackPanelProps => {
|
|
218
|
-
const
|
|
219
|
-
const
|
|
270
|
+
const currentStack = stackRef.current;
|
|
271
|
+
const panelIndex = currentStack.indexOf(id);
|
|
272
|
+
const currentDepth = currentStack.length - 1;
|
|
273
|
+
const isActive = panelIndex === currentDepth;
|
|
220
274
|
|
|
221
275
|
return {
|
|
222
276
|
"data-stack-panel": id,
|
|
@@ -224,12 +278,15 @@ export function useStackNavigation<TId extends string = string>(
|
|
|
224
278
|
"data-active": isActive ? "true" : "false",
|
|
225
279
|
"aria-hidden": !isActive,
|
|
226
280
|
};
|
|
227
|
-
}, [
|
|
281
|
+
}, []);
|
|
228
282
|
|
|
229
|
-
//
|
|
283
|
+
// getBackButtonProps uses stackRef for current state
|
|
230
284
|
const getBackButtonProps = React.useCallback((): StackBackButtonProps => {
|
|
231
|
-
const
|
|
232
|
-
const
|
|
285
|
+
const currentStack = stackRef.current;
|
|
286
|
+
const currentDepth = currentStack.length - 1;
|
|
287
|
+
const canGoBack = currentDepth > 0;
|
|
288
|
+
const prevPanelId = currentDepth > 0 ? currentStack[currentDepth - 1] : null;
|
|
289
|
+
const prevPanel = prevPanelId ? panels.find((p) => p.id === prevPanelId) : null;
|
|
233
290
|
const label = prevPanel?.title ? `Back to ${prevPanel.title}` : "Go back";
|
|
234
291
|
|
|
235
292
|
return {
|
|
@@ -237,7 +294,7 @@ export function useStackNavigation<TId extends string = string>(
|
|
|
237
294
|
disabled: !canGoBack,
|
|
238
295
|
"aria-label": label,
|
|
239
296
|
};
|
|
240
|
-
}, [
|
|
297
|
+
}, [panels, go]);
|
|
241
298
|
|
|
242
299
|
// Container style
|
|
243
300
|
const containerStyle: React.CSSProperties = React.useMemo(
|