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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tests for StackBasics component logic.
|
|
3
|
+
*
|
|
4
|
+
* These tests focus on the panel visibility and depth calculation logic,
|
|
5
|
+
* particularly around the exitingPanelId handling during rapid navigation.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pure function version of the isExiting logic for testing.
|
|
11
|
+
* This matches the logic in StackBasics.tsx.
|
|
12
|
+
*/
|
|
13
|
+
function computeIsExiting(
|
|
14
|
+
panelId: string,
|
|
15
|
+
exitingPanelId: string | null,
|
|
16
|
+
stack: readonly string[],
|
|
17
|
+
): boolean {
|
|
18
|
+
// FIX: Check if panel is in current stack - if so, it's not exiting
|
|
19
|
+
const isInCurrentStack = stack.includes(panelId);
|
|
20
|
+
return panelId === exitingPanelId && !isInCurrentStack;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pure function version of panelDepth calculation.
|
|
25
|
+
*/
|
|
26
|
+
function computePanelDepth(
|
|
27
|
+
panelId: string,
|
|
28
|
+
isExiting: boolean,
|
|
29
|
+
stack: readonly string[],
|
|
30
|
+
depth: number,
|
|
31
|
+
): number {
|
|
32
|
+
return isExiting ? depth + 1 : stack.indexOf(panelId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("StackBasics panel calculation logic", () => {
|
|
36
|
+
describe("isExiting calculation", () => {
|
|
37
|
+
it("returns true when panel is exitingPanelId and not in stack", () => {
|
|
38
|
+
// Back navigation: detail was removed from stack but exitingPanelId is set
|
|
39
|
+
const result = computeIsExiting("detail", "detail", ["list"]);
|
|
40
|
+
expect(result).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns false when panel is in current stack even if it matches exitingPanelId", () => {
|
|
44
|
+
// Bug scenario: user pushed detail again before exitingPanelId timeout cleared
|
|
45
|
+
// detail is in stack but exitingPanelId is still "detail"
|
|
46
|
+
const result = computeIsExiting("detail", "detail", ["list", "detail"]);
|
|
47
|
+
expect(result).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns false when panel does not match exitingPanelId", () => {
|
|
51
|
+
const result = computeIsExiting("list", "detail", ["list"]);
|
|
52
|
+
expect(result).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns false when exitingPanelId is null", () => {
|
|
56
|
+
const result = computeIsExiting("detail", null, ["list", "detail"]);
|
|
57
|
+
expect(result).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("panelDepth calculation", () => {
|
|
62
|
+
it("returns correct depth for active panel in stack", () => {
|
|
63
|
+
const isExiting = computeIsExiting("detail", null, ["list", "detail"]);
|
|
64
|
+
const depth = computePanelDepth("detail", isExiting, ["list", "detail"], 1);
|
|
65
|
+
expect(depth).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns depth + 1 for exiting panel", () => {
|
|
69
|
+
const isExiting = computeIsExiting("detail", "detail", ["list"]);
|
|
70
|
+
const depth = computePanelDepth("detail", isExiting, ["list"], 0);
|
|
71
|
+
expect(depth).toBe(1); // 0 + 1
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("returns correct depth for re-pushed panel (not exiting)", () => {
|
|
75
|
+
// Bug scenario: detail was exiting but got re-pushed
|
|
76
|
+
// Should use stack.indexOf, not depth + 1
|
|
77
|
+
const stack = ["list", "detail"];
|
|
78
|
+
const isExiting = computeIsExiting("detail", "detail", stack);
|
|
79
|
+
const depth = computePanelDepth("detail", isExiting, stack, 1);
|
|
80
|
+
expect(depth).toBe(1); // stack.indexOf("detail") = 1, NOT depth + 1 = 2
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("rapid navigation scenarios", () => {
|
|
85
|
+
it("handles push → back → push sequence correctly", () => {
|
|
86
|
+
// Initial: list active
|
|
87
|
+
let stack: readonly string[] = ["list"];
|
|
88
|
+
let depth = 0;
|
|
89
|
+
let exitingPanelId: string | null = null;
|
|
90
|
+
|
|
91
|
+
// Push detail
|
|
92
|
+
stack = ["list", "detail"];
|
|
93
|
+
depth = 1;
|
|
94
|
+
|
|
95
|
+
// Back to list (detail becomes exiting)
|
|
96
|
+
stack = ["list"];
|
|
97
|
+
depth = 0;
|
|
98
|
+
exitingPanelId = "detail";
|
|
99
|
+
|
|
100
|
+
// Verify detail is exiting
|
|
101
|
+
let isExiting = computeIsExiting("detail", exitingPanelId, stack);
|
|
102
|
+
expect(isExiting).toBe(true);
|
|
103
|
+
|
|
104
|
+
// Push detail again BEFORE exitingPanelId clears
|
|
105
|
+
stack = ["list", "detail"];
|
|
106
|
+
depth = 1;
|
|
107
|
+
// exitingPanelId is still "detail" (timeout hasn't fired yet)
|
|
108
|
+
|
|
109
|
+
// Now detail should NOT be exiting because it's in the stack
|
|
110
|
+
isExiting = computeIsExiting("detail", exitingPanelId, stack);
|
|
111
|
+
expect(isExiting).toBe(false);
|
|
112
|
+
|
|
113
|
+
// Panel depth should be correct
|
|
114
|
+
const panelDepth = computePanelDepth("detail", isExiting, stack, depth);
|
|
115
|
+
expect(panelDepth).toBe(1); // Not 2!
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("handles deep navigation with rapid back operations", () => {
|
|
119
|
+
// Initial: list → detail → edit
|
|
120
|
+
let stack: readonly string[] = ["list", "detail", "edit"];
|
|
121
|
+
let depth = 2;
|
|
122
|
+
let exitingPanelId: string | null = null;
|
|
123
|
+
|
|
124
|
+
// Back to detail (edit becomes exiting)
|
|
125
|
+
stack = ["list", "detail"];
|
|
126
|
+
depth = 1;
|
|
127
|
+
exitingPanelId = "edit";
|
|
128
|
+
|
|
129
|
+
const editIsExiting = computeIsExiting("edit", exitingPanelId, stack);
|
|
130
|
+
expect(editIsExiting).toBe(true);
|
|
131
|
+
|
|
132
|
+
// Back to list immediately (detail becomes exiting, edit timeout still pending)
|
|
133
|
+
stack = ["list"];
|
|
134
|
+
depth = 0;
|
|
135
|
+
exitingPanelId = "detail"; // Updated to detail
|
|
136
|
+
|
|
137
|
+
const detailIsExiting = computeIsExiting("detail", exitingPanelId, stack);
|
|
138
|
+
expect(detailIsExiting).toBe(true);
|
|
139
|
+
|
|
140
|
+
// Push to detail again
|
|
141
|
+
stack = ["list", "detail"];
|
|
142
|
+
depth = 1;
|
|
143
|
+
// exitingPanelId still "detail"
|
|
144
|
+
|
|
145
|
+
const detailIsExitingAfterPush = computeIsExiting("detail", exitingPanelId, stack);
|
|
146
|
+
expect(detailIsExitingAfterPush).toBe(false);
|
|
147
|
+
|
|
148
|
+
const detailDepth = computePanelDepth("detail", detailIsExitingAfterPush, stack, depth);
|
|
149
|
+
expect(detailDepth).toBe(1);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Stack Navigation demo - iOS-style hierarchical navigation
|
|
3
|
+
* Uses SwipeStackContent for direct DOM manipulation during swipe gestures.
|
|
3
4
|
*/
|
|
4
5
|
import * as React from "react";
|
|
5
6
|
import { useStackNavigation } from "../../../../modules/stack/useStackNavigation.js";
|
|
6
7
|
import { useStackSwipeInput } from "../../../../modules/stack/useStackSwipeInput.js";
|
|
7
|
-
import {
|
|
8
|
+
import { SwipeStackContent } from "../../../../modules/stack/SwipeStackContent.js";
|
|
9
|
+
import { useResizeObserver } from "../../../../hooks/useResizeObserver.js";
|
|
10
|
+
import { toContinuousOperationState } from "../../../../hooks/gesture/types.js";
|
|
8
11
|
import type { StackPanel } from "../../../../modules/stack/types.js";
|
|
9
12
|
import styles from "./Stack.module.css";
|
|
10
13
|
|
|
14
|
+
const ANIMATION_DURATION = 300;
|
|
15
|
+
|
|
11
16
|
const panels: StackPanel[] = [
|
|
12
17
|
{
|
|
13
18
|
id: "list",
|
|
@@ -61,16 +66,78 @@ export const StackBasics: React.FC = () => {
|
|
|
61
66
|
const navigation = useStackNavigation({
|
|
62
67
|
panels,
|
|
63
68
|
displayMode: "overlay",
|
|
64
|
-
transitionMode: "
|
|
69
|
+
transitionMode: "none", // Using direct DOM manipulation
|
|
65
70
|
});
|
|
66
71
|
|
|
67
|
-
const { isEdgeSwiping, progress, containerProps } = useStackSwipeInput({
|
|
72
|
+
const { isEdgeSwiping, progress, inputState, containerProps } = useStackSwipeInput({
|
|
68
73
|
containerRef,
|
|
69
74
|
navigation,
|
|
70
75
|
edge: "left",
|
|
71
76
|
edgeWidth: 30,
|
|
72
77
|
});
|
|
73
78
|
|
|
79
|
+
// Track container size for SwipeStackContent
|
|
80
|
+
const { rect } = useResizeObserver(containerRef, { box: "border-box" });
|
|
81
|
+
const containerSize = rect?.width ?? 0;
|
|
82
|
+
|
|
83
|
+
const { stack, depth } = navigation.state;
|
|
84
|
+
|
|
85
|
+
// Track exiting panel when navigating back.
|
|
86
|
+
// CRITICAL: exitingPanelId must be computed synchronously during render
|
|
87
|
+
// to prevent the exiting panel from being unmounted for a frame.
|
|
88
|
+
const prevDepthRef = React.useRef(depth);
|
|
89
|
+
const prevStackRef = React.useRef<ReadonlyArray<string>>(stack);
|
|
90
|
+
const exitingPanelClearTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
91
|
+
const [exitingPanelState, setExitingPanelState] = React.useState<string | null>(null);
|
|
92
|
+
|
|
93
|
+
// Compute exiting panel ID synchronously during render
|
|
94
|
+
const prevDepth = prevDepthRef.current;
|
|
95
|
+
const prevStack = prevStackRef.current;
|
|
96
|
+
const isNavigatingBack = depth < prevDepth;
|
|
97
|
+
const computedExitingId = isNavigatingBack ? (prevStack[prevDepth] ?? null) : null;
|
|
98
|
+
const exitingPanelId = computedExitingId ?? exitingPanelState;
|
|
99
|
+
|
|
100
|
+
// Update refs and state in effect
|
|
101
|
+
React.useLayoutEffect(() => {
|
|
102
|
+
prevDepthRef.current = depth;
|
|
103
|
+
prevStackRef.current = stack;
|
|
104
|
+
|
|
105
|
+
if (computedExitingId != null) {
|
|
106
|
+
setExitingPanelState(computedExitingId);
|
|
107
|
+
|
|
108
|
+
if (exitingPanelClearTimeoutRef.current != null) {
|
|
109
|
+
clearTimeout(exitingPanelClearTimeoutRef.current);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
exitingPanelClearTimeoutRef.current = setTimeout(() => {
|
|
113
|
+
setExitingPanelState(null);
|
|
114
|
+
exitingPanelClearTimeoutRef.current = null;
|
|
115
|
+
}, ANIMATION_DURATION);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return () => {
|
|
119
|
+
if (exitingPanelClearTimeoutRef.current != null) {
|
|
120
|
+
clearTimeout(exitingPanelClearTimeoutRef.current);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}, [depth, stack, computedExitingId]);
|
|
124
|
+
|
|
125
|
+
// Get visible panel IDs: active + behind (for swipe reveal) + exiting (for back animation)
|
|
126
|
+
const visiblePanelIds = React.useMemo(() => {
|
|
127
|
+
const ids: string[] = [];
|
|
128
|
+
// Behind panel (if exists)
|
|
129
|
+
if (depth > 0) {
|
|
130
|
+
ids.push(stack[depth - 1]);
|
|
131
|
+
}
|
|
132
|
+
// Active panel
|
|
133
|
+
ids.push(stack[depth]);
|
|
134
|
+
// Include exiting panel if not already in the list
|
|
135
|
+
if (exitingPanelId != null && !ids.includes(exitingPanelId)) {
|
|
136
|
+
ids.push(exitingPanelId);
|
|
137
|
+
}
|
|
138
|
+
return ids;
|
|
139
|
+
}, [stack, depth, exitingPanelId]);
|
|
140
|
+
|
|
74
141
|
const handleItemClick = (item: typeof listItems[0]) => {
|
|
75
142
|
setSelectedItem(item);
|
|
76
143
|
navigation.push("detail");
|
|
@@ -98,11 +165,93 @@ export const StackBasics: React.FC = () => {
|
|
|
98
165
|
const showBackButton = navigation.state.depth > 0;
|
|
99
166
|
const showEditButton = navigation.state.depth === 1;
|
|
100
167
|
|
|
168
|
+
const renderBackButton = (): React.ReactNode => {
|
|
169
|
+
if (!showBackButton) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return <BackButton onClick={backButtonProps.onClick} disabled={backButtonProps.disabled} />;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const renderPanelContent = (panelId: string): React.ReactNode => {
|
|
176
|
+
if (panelId === "list") {
|
|
177
|
+
return (
|
|
178
|
+
<div className={styles.panel}>
|
|
179
|
+
<ul className={styles.list}>
|
|
180
|
+
{listItems.map((item) => (
|
|
181
|
+
<li key={item.id} className={styles.listItem}>
|
|
182
|
+
<button
|
|
183
|
+
className={styles.listItemButton}
|
|
184
|
+
onClick={() => handleItemClick(item)}
|
|
185
|
+
>
|
|
186
|
+
<span className={styles.listItemName}>{item.name}</span>
|
|
187
|
+
<span className={styles.listItemDesc}>{item.description}</span>
|
|
188
|
+
<span className={styles.chevron}>→</span>
|
|
189
|
+
</button>
|
|
190
|
+
</li>
|
|
191
|
+
))}
|
|
192
|
+
</ul>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (panelId === "detail") {
|
|
198
|
+
return (
|
|
199
|
+
<div className={styles.panel}>
|
|
200
|
+
<div className={styles.detailContent}>
|
|
201
|
+
<h2>{selectedItem?.name}</h2>
|
|
202
|
+
<p>{selectedItem?.description}</p>
|
|
203
|
+
<div className={styles.detailMeta}>
|
|
204
|
+
<span>ID: {selectedItem?.id}</span>
|
|
205
|
+
</div>
|
|
206
|
+
<p className={styles.hint}>
|
|
207
|
+
Swipe from the left edge to go back, or tap the Back button.
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (panelId === "edit") {
|
|
215
|
+
return (
|
|
216
|
+
<div className={styles.panel}>
|
|
217
|
+
<div className={styles.editContent}>
|
|
218
|
+
<h2>Edit {selectedItem?.name}</h2>
|
|
219
|
+
<div className={styles.form}>
|
|
220
|
+
<label className={styles.label}>
|
|
221
|
+
Name
|
|
222
|
+
<input
|
|
223
|
+
type="text"
|
|
224
|
+
className={styles.input}
|
|
225
|
+
defaultValue={selectedItem?.name}
|
|
226
|
+
/>
|
|
227
|
+
</label>
|
|
228
|
+
<label className={styles.label}>
|
|
229
|
+
Description
|
|
230
|
+
<textarea
|
|
231
|
+
className={styles.textarea}
|
|
232
|
+
defaultValue={selectedItem?.description}
|
|
233
|
+
/>
|
|
234
|
+
</label>
|
|
235
|
+
</div>
|
|
236
|
+
<button
|
|
237
|
+
className={styles.saveButton}
|
|
238
|
+
onClick={() => navigation.go(-1)}
|
|
239
|
+
>
|
|
240
|
+
Save Changes
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return null;
|
|
248
|
+
};
|
|
249
|
+
|
|
101
250
|
return (
|
|
102
251
|
<div className={styles.container}>
|
|
103
252
|
{/* Header */}
|
|
104
253
|
<header className={styles.header}>
|
|
105
|
-
{
|
|
254
|
+
{renderBackButton()}
|
|
106
255
|
<h1 className={styles.title}>{getHeaderTitle()}</h1>
|
|
107
256
|
{showEditButton ? <EditButton onClick={handleEdit} /> : null}
|
|
108
257
|
</header>
|
|
@@ -113,97 +262,32 @@ export const StackBasics: React.FC = () => {
|
|
|
113
262
|
className={styles.stackContainer}
|
|
114
263
|
{...containerProps}
|
|
115
264
|
>
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
</StackContent>
|
|
143
|
-
|
|
144
|
-
{/* Detail Panel */}
|
|
145
|
-
<StackContent
|
|
146
|
-
id="detail"
|
|
147
|
-
depth={1}
|
|
148
|
-
isActive={navigation.currentPanelId === "detail"}
|
|
149
|
-
displayMode="overlay"
|
|
150
|
-
transitionMode="css"
|
|
151
|
-
navigationState={navigation.state}
|
|
152
|
-
swipeProgress={navigation.currentPanelId === "detail" ? progress : undefined}
|
|
153
|
-
>
|
|
154
|
-
<div className={styles.panel}>
|
|
155
|
-
<div className={styles.detailContent}>
|
|
156
|
-
<h2>{selectedItem?.name}</h2>
|
|
157
|
-
<p>{selectedItem?.description}</p>
|
|
158
|
-
<div className={styles.detailMeta}>
|
|
159
|
-
<span>ID: {selectedItem?.id}</span>
|
|
160
|
-
</div>
|
|
161
|
-
<p className={styles.hint}>
|
|
162
|
-
Swipe from the left edge to go back, or tap the Back button.
|
|
163
|
-
</p>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
</StackContent>
|
|
167
|
-
|
|
168
|
-
{/* Edit Panel */}
|
|
169
|
-
<StackContent
|
|
170
|
-
id="edit"
|
|
171
|
-
depth={2}
|
|
172
|
-
isActive={navigation.currentPanelId === "edit"}
|
|
173
|
-
displayMode="overlay"
|
|
174
|
-
transitionMode="css"
|
|
175
|
-
navigationState={navigation.state}
|
|
176
|
-
swipeProgress={navigation.currentPanelId === "edit" ? progress : undefined}
|
|
177
|
-
>
|
|
178
|
-
<div className={styles.panel}>
|
|
179
|
-
<div className={styles.editContent}>
|
|
180
|
-
<h2>Edit {selectedItem?.name}</h2>
|
|
181
|
-
<div className={styles.form}>
|
|
182
|
-
<label className={styles.label}>
|
|
183
|
-
Name
|
|
184
|
-
<input
|
|
185
|
-
type="text"
|
|
186
|
-
className={styles.input}
|
|
187
|
-
defaultValue={selectedItem?.name}
|
|
188
|
-
/>
|
|
189
|
-
</label>
|
|
190
|
-
<label className={styles.label}>
|
|
191
|
-
Description
|
|
192
|
-
<textarea
|
|
193
|
-
className={styles.textarea}
|
|
194
|
-
defaultValue={selectedItem?.description}
|
|
195
|
-
/>
|
|
196
|
-
</label>
|
|
197
|
-
</div>
|
|
198
|
-
<button
|
|
199
|
-
className={styles.saveButton}
|
|
200
|
-
onClick={() => navigation.go(-1)}
|
|
201
|
-
>
|
|
202
|
-
Save Changes
|
|
203
|
-
</button>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
</StackContent>
|
|
265
|
+
{visiblePanelIds.map((panelId) => {
|
|
266
|
+
// Panel is only "exiting" if it matches exitingPanelId AND is not in current stack
|
|
267
|
+
// This prevents the bug where a re-pushed panel is incorrectly treated as exiting
|
|
268
|
+
const isInCurrentStack = stack.includes(panelId);
|
|
269
|
+
const isExiting = panelId === exitingPanelId && !isInCurrentStack;
|
|
270
|
+
// For exiting panels, use depth + 1 since they were previously at the active position
|
|
271
|
+
const panelDepth = isExiting ? depth + 1 : stack.indexOf(panelId);
|
|
272
|
+
const isActive = panelDepth === depth && !isExiting;
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<SwipeStackContent
|
|
276
|
+
key={panelId}
|
|
277
|
+
id={panelId}
|
|
278
|
+
depth={panelDepth}
|
|
279
|
+
navigationDepth={depth}
|
|
280
|
+
isActive={isActive}
|
|
281
|
+
operationState={toContinuousOperationState(inputState)}
|
|
282
|
+
containerSize={containerSize}
|
|
283
|
+
animateOnMount={true}
|
|
284
|
+
animationDuration={ANIMATION_DURATION}
|
|
285
|
+
displayMode="overlay"
|
|
286
|
+
>
|
|
287
|
+
{renderPanelContent(panelId)}
|
|
288
|
+
</SwipeStackContent>
|
|
289
|
+
);
|
|
290
|
+
})}
|
|
207
291
|
</div>
|
|
208
292
|
|
|
209
293
|
{/* Debug info */}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tests for StackTablet component logic.
|
|
3
|
+
*
|
|
4
|
+
* These tests focus on the panel visibility and depth calculation logic,
|
|
5
|
+
* particularly around the exitingPanelId handling during rapid navigation.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pure function version of the isExiting logic for testing.
|
|
11
|
+
* This matches the logic in StackTablet.tsx.
|
|
12
|
+
*/
|
|
13
|
+
function computeIsExiting(
|
|
14
|
+
panelId: string,
|
|
15
|
+
exitingPanelId: string | null,
|
|
16
|
+
stack: readonly string[],
|
|
17
|
+
): boolean {
|
|
18
|
+
// FIX: Check if panel is in current stack - if so, it's not exiting
|
|
19
|
+
const isInCurrentStack = stack.includes(panelId);
|
|
20
|
+
return panelId === exitingPanelId && !isInCurrentStack;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pure function version of panelDepth calculation.
|
|
25
|
+
*/
|
|
26
|
+
function computePanelDepth(
|
|
27
|
+
panelId: string,
|
|
28
|
+
isExiting: boolean,
|
|
29
|
+
stack: readonly string[],
|
|
30
|
+
depth: number,
|
|
31
|
+
): number {
|
|
32
|
+
return isExiting ? depth + 1 : stack.indexOf(panelId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("StackTablet panel calculation logic", () => {
|
|
36
|
+
describe("isExiting calculation", () => {
|
|
37
|
+
it("returns false when panel is in current stack even if it matches exitingPanelId", () => {
|
|
38
|
+
// Bug scenario: user pushed panel again before exitingPanelId timeout cleared
|
|
39
|
+
const result = computeIsExiting("general", "general", ["root", "general"]);
|
|
40
|
+
expect(result).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("panelDepth calculation", () => {
|
|
45
|
+
it("returns correct depth for re-pushed panel (not exiting)", () => {
|
|
46
|
+
// Bug scenario: general was exiting but got re-pushed
|
|
47
|
+
const stack = ["root", "general"];
|
|
48
|
+
const isExiting = computeIsExiting("general", "general", stack);
|
|
49
|
+
const depth = computePanelDepth("general", isExiting, stack, 1);
|
|
50
|
+
expect(depth).toBe(1); // stack.indexOf("general") = 1, NOT depth + 1 = 2
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("rapid navigation scenarios", () => {
|
|
55
|
+
it("handles push → back → push sequence correctly", () => {
|
|
56
|
+
// Initial: root active
|
|
57
|
+
let stack: readonly string[] = ["root"];
|
|
58
|
+
let depth = 0;
|
|
59
|
+
let exitingPanelId: string | null = null;
|
|
60
|
+
|
|
61
|
+
// Push general
|
|
62
|
+
stack = ["root", "general"];
|
|
63
|
+
depth = 1;
|
|
64
|
+
|
|
65
|
+
// Back to root (general becomes exiting)
|
|
66
|
+
stack = ["root"];
|
|
67
|
+
depth = 0;
|
|
68
|
+
exitingPanelId = "general";
|
|
69
|
+
|
|
70
|
+
// Push general again BEFORE exitingPanelId clears
|
|
71
|
+
stack = ["root", "general"];
|
|
72
|
+
depth = 1;
|
|
73
|
+
// exitingPanelId is still "general" (timeout hasn't fired yet)
|
|
74
|
+
|
|
75
|
+
// Now general should NOT be exiting because it's in the stack
|
|
76
|
+
const isExiting = computeIsExiting("general", exitingPanelId, stack);
|
|
77
|
+
expect(isExiting).toBe(false);
|
|
78
|
+
|
|
79
|
+
// Panel depth should be correct
|
|
80
|
+
const panelDepth = computePanelDepth("general", isExiting, stack, depth);
|
|
81
|
+
expect(panelDepth).toBe(1); // Not 2!
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("handles Settings menu rapid navigation", () => {
|
|
85
|
+
// Simulating actual Settings menu navigation
|
|
86
|
+
let stack: readonly string[] = ["root"];
|
|
87
|
+
let depth = 0;
|
|
88
|
+
let exitingPanelId: string | null = null;
|
|
89
|
+
|
|
90
|
+
// Click "General"
|
|
91
|
+
stack = ["root", "general"];
|
|
92
|
+
depth = 1;
|
|
93
|
+
|
|
94
|
+
// Click "About"
|
|
95
|
+
stack = ["root", "general", "about"];
|
|
96
|
+
depth = 2;
|
|
97
|
+
|
|
98
|
+
// Back to General
|
|
99
|
+
stack = ["root", "general"];
|
|
100
|
+
depth = 1;
|
|
101
|
+
exitingPanelId = "about";
|
|
102
|
+
|
|
103
|
+
// Immediately back to root
|
|
104
|
+
stack = ["root"];
|
|
105
|
+
depth = 0;
|
|
106
|
+
exitingPanelId = "general";
|
|
107
|
+
|
|
108
|
+
// Immediately push General again
|
|
109
|
+
stack = ["root", "general"];
|
|
110
|
+
depth = 1;
|
|
111
|
+
|
|
112
|
+
// general should NOT be exiting
|
|
113
|
+
const isExiting = computeIsExiting("general", exitingPanelId, stack);
|
|
114
|
+
expect(isExiting).toBe(false);
|
|
115
|
+
|
|
116
|
+
const panelDepth = computePanelDepth("general", isExiting, stack, depth);
|
|
117
|
+
expect(panelDepth).toBe(1);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|