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,1133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tests for swipe-to-navigation transition continuity.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that when a swipe gesture completes and triggers navigation,
|
|
5
|
+
* the behind panel (which becomes active) should NOT restart its animation from
|
|
6
|
+
* the original "behind" position, but should continue smoothly from its current position.
|
|
7
|
+
*
|
|
8
|
+
* Issue: When swiping back strongly, the panel that was "behind" becomes "active",
|
|
9
|
+
* and its animation appears to restart from the beginning rather than continuing
|
|
10
|
+
* from where the swipe left it.
|
|
11
|
+
*/
|
|
12
|
+
import { render, act } from "@testing-library/react";
|
|
13
|
+
import { SwipeStackContent } from "./SwipeStackContent.js";
|
|
14
|
+
import type { ContinuousOperationState } from "../../hooks/gesture/types.js";
|
|
15
|
+
|
|
16
|
+
// Mock requestAnimationFrame for animation testing
|
|
17
|
+
const rafState = {
|
|
18
|
+
callbacks: [] as FrameRequestCallback[],
|
|
19
|
+
id: 0,
|
|
20
|
+
mockTimestamp: 0,
|
|
21
|
+
originalRAF: globalThis.requestAnimationFrame,
|
|
22
|
+
originalCAF: globalThis.cancelAnimationFrame,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const resetRafState = (): void => {
|
|
26
|
+
rafState.callbacks = [];
|
|
27
|
+
rafState.id = 0;
|
|
28
|
+
rafState.mockTimestamp = 0;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const mockRAF = (callback: FrameRequestCallback): number => {
|
|
32
|
+
rafState.callbacks = [...rafState.callbacks, callback];
|
|
33
|
+
rafState.id += 1;
|
|
34
|
+
return rafState.id;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const mockCAF = (): void => {};
|
|
38
|
+
|
|
39
|
+
const flushRAF = (advanceMs = 400): void => {
|
|
40
|
+
rafState.mockTimestamp += advanceMs;
|
|
41
|
+
const callbacks = rafState.callbacks;
|
|
42
|
+
rafState.callbacks = [];
|
|
43
|
+
callbacks.forEach((cb) => cb(rafState.mockTimestamp));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
resetRafState();
|
|
48
|
+
globalThis.requestAnimationFrame = mockRAF;
|
|
49
|
+
globalThis.cancelAnimationFrame = mockCAF;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
globalThis.requestAnimationFrame = rafState.originalRAF;
|
|
54
|
+
globalThis.cancelAnimationFrame = rafState.originalCAF;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const IDLE_STATE: ContinuousOperationState = {
|
|
58
|
+
phase: "idle",
|
|
59
|
+
displacement: { x: 0, y: 0 },
|
|
60
|
+
velocity: { x: 0, y: 0 },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const createOperatingState = (displacementX: number): ContinuousOperationState => ({
|
|
64
|
+
phase: "operating",
|
|
65
|
+
displacement: { x: displacementX, y: 0 },
|
|
66
|
+
velocity: { x: 0.5, y: 0 },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("Swipe transition continuity", () => {
|
|
70
|
+
describe("behind panel becoming active after full swipe", () => {
|
|
71
|
+
/**
|
|
72
|
+
* Scenario: User swipes 100% to go back.
|
|
73
|
+
*
|
|
74
|
+
* Before swipe:
|
|
75
|
+
* - Panel at depth 0: role=behind, targetPx=-120, visible=hidden
|
|
76
|
+
* - Panel at depth 1: role=active, targetPx=0, visible=visible
|
|
77
|
+
*
|
|
78
|
+
* During 100% swipe:
|
|
79
|
+
* - Panel at depth 0: role=behind, at 0px (fully revealed via parallax)
|
|
80
|
+
* - Panel at depth 1: role=active, at 400px (fully off-screen)
|
|
81
|
+
*
|
|
82
|
+
* After swipe (navigation changed, depth 1→0):
|
|
83
|
+
* - Panel at depth 0: role=active, should stay at 0px (NO animation needed)
|
|
84
|
+
* - Panel at depth 1: role=hidden, should stay at 400px
|
|
85
|
+
*
|
|
86
|
+
* Bug: The panel at depth 0 incorrectly animates from -120px to 0px,
|
|
87
|
+
* even though it's already at 0px.
|
|
88
|
+
*/
|
|
89
|
+
it("behind panel should NOT animate after completing 100% swipe back", () => {
|
|
90
|
+
const containerSize = 400;
|
|
91
|
+
const behindOffset = -0.3; // -120px
|
|
92
|
+
|
|
93
|
+
// Step 1: Render behind panel at depth 0, navigationDepth 1
|
|
94
|
+
const { container, rerender } = render(
|
|
95
|
+
<SwipeStackContent
|
|
96
|
+
id="behind"
|
|
97
|
+
depth={0}
|
|
98
|
+
navigationDepth={1}
|
|
99
|
+
isActive={false}
|
|
100
|
+
operationState={IDLE_STATE}
|
|
101
|
+
containerSize={containerSize}
|
|
102
|
+
>
|
|
103
|
+
Content
|
|
104
|
+
</SwipeStackContent>,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const element = container.firstChild as HTMLElement;
|
|
108
|
+
|
|
109
|
+
// Initial: behind position at -120px
|
|
110
|
+
expect(element.style.transform).toBe("translateX(-120px)");
|
|
111
|
+
|
|
112
|
+
// Step 2: Swipe 100% (400px displacement)
|
|
113
|
+
// Behind panel should be at 0px (fully revealed)
|
|
114
|
+
rerender(
|
|
115
|
+
<SwipeStackContent
|
|
116
|
+
id="behind"
|
|
117
|
+
depth={0}
|
|
118
|
+
navigationDepth={1}
|
|
119
|
+
isActive={false}
|
|
120
|
+
operationState={createOperatingState(400)}
|
|
121
|
+
containerSize={containerSize}
|
|
122
|
+
>
|
|
123
|
+
Content
|
|
124
|
+
</SwipeStackContent>,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Behind panel at 0px (parallax completed)
|
|
128
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
129
|
+
|
|
130
|
+
// Step 3: Swipe ends AND navigation changes simultaneously
|
|
131
|
+
// - operationState becomes IDLE (displacement = 0)
|
|
132
|
+
// - navigationDepth changes to 0 (this panel becomes active)
|
|
133
|
+
// - role changes from "behind" to "active"
|
|
134
|
+
// - targetPx changes from -120 to 0
|
|
135
|
+
//
|
|
136
|
+
// CRITICAL: Since the panel is ALREADY at 0px, it should NOT animate.
|
|
137
|
+
rerender(
|
|
138
|
+
<SwipeStackContent
|
|
139
|
+
id="behind"
|
|
140
|
+
depth={0}
|
|
141
|
+
navigationDepth={0}
|
|
142
|
+
isActive={true}
|
|
143
|
+
operationState={IDLE_STATE}
|
|
144
|
+
containerSize={containerSize}
|
|
145
|
+
>
|
|
146
|
+
Content
|
|
147
|
+
</SwipeStackContent>,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// The panel should stay at 0px immediately (no jump to -120px)
|
|
151
|
+
// This is the bug: it might incorrectly start an animation from -120px
|
|
152
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
153
|
+
|
|
154
|
+
// Flush any pending animations
|
|
155
|
+
act(() => {
|
|
156
|
+
flushRAF(0);
|
|
157
|
+
flushRAF(400);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// After potential animation, should still be at 0px
|
|
161
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Scenario: User swipes 80% and releases (meets threshold).
|
|
166
|
+
*
|
|
167
|
+
* The behind panel at depth 0 should animate from its current position
|
|
168
|
+
* (-24px at 80% swipe) to 0px, NOT from -120px to 0px.
|
|
169
|
+
*/
|
|
170
|
+
it("behind panel should animate from current position after 80% swipe", () => {
|
|
171
|
+
const containerSize = 400;
|
|
172
|
+
|
|
173
|
+
const { container, rerender } = render(
|
|
174
|
+
<SwipeStackContent
|
|
175
|
+
id="behind"
|
|
176
|
+
depth={0}
|
|
177
|
+
navigationDepth={1}
|
|
178
|
+
isActive={false}
|
|
179
|
+
operationState={IDLE_STATE}
|
|
180
|
+
containerSize={containerSize}
|
|
181
|
+
>
|
|
182
|
+
Content
|
|
183
|
+
</SwipeStackContent>,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const element = container.firstChild as HTMLElement;
|
|
187
|
+
|
|
188
|
+
// Swipe 80% (320px)
|
|
189
|
+
// Behind panel: -120 + 0.8 * 120 = -120 + 96 = -24px
|
|
190
|
+
rerender(
|
|
191
|
+
<SwipeStackContent
|
|
192
|
+
id="behind"
|
|
193
|
+
depth={0}
|
|
194
|
+
navigationDepth={1}
|
|
195
|
+
isActive={false}
|
|
196
|
+
operationState={createOperatingState(320)}
|
|
197
|
+
containerSize={containerSize}
|
|
198
|
+
>
|
|
199
|
+
Content
|
|
200
|
+
</SwipeStackContent>,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// At 80% swipe, behind panel should be at -24px
|
|
204
|
+
expect(element.style.transform).toBe("translateX(-24px)");
|
|
205
|
+
|
|
206
|
+
// Swipe ends, navigation changes
|
|
207
|
+
rerender(
|
|
208
|
+
<SwipeStackContent
|
|
209
|
+
id="behind"
|
|
210
|
+
depth={0}
|
|
211
|
+
navigationDepth={0}
|
|
212
|
+
isActive={true}
|
|
213
|
+
operationState={IDLE_STATE}
|
|
214
|
+
containerSize={containerSize}
|
|
215
|
+
>
|
|
216
|
+
Content
|
|
217
|
+
</SwipeStackContent>,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Animation should start from -24px (current position), not -120px
|
|
221
|
+
// Immediately after render, should still be at -24px (animation hasn't run yet)
|
|
222
|
+
expect(element.style.transform).toBe("translateX(-24px)");
|
|
223
|
+
|
|
224
|
+
// Flush animations
|
|
225
|
+
act(() => {
|
|
226
|
+
flushRAF(0);
|
|
227
|
+
flushRAF(400);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// After animation, should be at 0px
|
|
231
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("swipe continuation beyond threshold", () => {
|
|
236
|
+
/**
|
|
237
|
+
* Scenario: User swipes past 100% and continues moving.
|
|
238
|
+
*
|
|
239
|
+
* This tests the "strong swipe" behavior where the user swipes
|
|
240
|
+
* past the container width.
|
|
241
|
+
*/
|
|
242
|
+
it("handles swipe beyond 100% without animation glitches", () => {
|
|
243
|
+
const containerSize = 400;
|
|
244
|
+
|
|
245
|
+
const { container, rerender } = render(
|
|
246
|
+
<SwipeStackContent
|
|
247
|
+
id="behind"
|
|
248
|
+
depth={0}
|
|
249
|
+
navigationDepth={1}
|
|
250
|
+
isActive={false}
|
|
251
|
+
operationState={IDLE_STATE}
|
|
252
|
+
containerSize={containerSize}
|
|
253
|
+
>
|
|
254
|
+
Content
|
|
255
|
+
</SwipeStackContent>,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const element = container.firstChild as HTMLElement;
|
|
259
|
+
|
|
260
|
+
// Swipe beyond 100% (500px, 125%)
|
|
261
|
+
rerender(
|
|
262
|
+
<SwipeStackContent
|
|
263
|
+
id="behind"
|
|
264
|
+
depth={0}
|
|
265
|
+
navigationDepth={1}
|
|
266
|
+
isActive={false}
|
|
267
|
+
operationState={createOperatingState(500)}
|
|
268
|
+
containerSize={containerSize}
|
|
269
|
+
>
|
|
270
|
+
Content
|
|
271
|
+
</SwipeStackContent>,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Behind panel should be at 0px (capped at full reveal)
|
|
275
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
276
|
+
|
|
277
|
+
// Swipe ends, navigation changes
|
|
278
|
+
rerender(
|
|
279
|
+
<SwipeStackContent
|
|
280
|
+
id="behind"
|
|
281
|
+
depth={0}
|
|
282
|
+
navigationDepth={0}
|
|
283
|
+
isActive={true}
|
|
284
|
+
operationState={IDLE_STATE}
|
|
285
|
+
containerSize={containerSize}
|
|
286
|
+
>
|
|
287
|
+
Content
|
|
288
|
+
</SwipeStackContent>,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Should stay at 0px, no animation needed
|
|
292
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
293
|
+
|
|
294
|
+
act(() => {
|
|
295
|
+
flushRAF(0);
|
|
296
|
+
flushRAF(400);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("role change during operation", () => {
|
|
304
|
+
/**
|
|
305
|
+
* Edge case: What happens if role changes while still operating?
|
|
306
|
+
*
|
|
307
|
+
* This could happen when navigation changes before the swipe gesture fully ends.
|
|
308
|
+
* The panel should maintain its current position to avoid visual jumps,
|
|
309
|
+
* then transition to the new role's behavior when the swipe ends.
|
|
310
|
+
*/
|
|
311
|
+
it("maintains position continuity when role changes during swipe", () => {
|
|
312
|
+
const containerSize = 400;
|
|
313
|
+
|
|
314
|
+
const { container, rerender } = render(
|
|
315
|
+
<SwipeStackContent
|
|
316
|
+
id="behind"
|
|
317
|
+
depth={0}
|
|
318
|
+
navigationDepth={1}
|
|
319
|
+
isActive={false}
|
|
320
|
+
operationState={createOperatingState(200)}
|
|
321
|
+
containerSize={containerSize}
|
|
322
|
+
>
|
|
323
|
+
Content
|
|
324
|
+
</SwipeStackContent>,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const element = container.firstChild as HTMLElement;
|
|
328
|
+
|
|
329
|
+
// At 50% swipe, behind panel at -60px (parallax effect)
|
|
330
|
+
expect(element.style.transform).toBe("translateX(-60px)");
|
|
331
|
+
|
|
332
|
+
// Role changes while still operating (navigation happened before swipe ended)
|
|
333
|
+
rerender(
|
|
334
|
+
<SwipeStackContent
|
|
335
|
+
id="behind"
|
|
336
|
+
depth={0}
|
|
337
|
+
navigationDepth={0}
|
|
338
|
+
isActive={true}
|
|
339
|
+
operationState={createOperatingState(200)}
|
|
340
|
+
containerSize={containerSize}
|
|
341
|
+
>
|
|
342
|
+
Content
|
|
343
|
+
</SwipeStackContent>,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// KEY BEHAVIOR: Position should be maintained to avoid visual jump
|
|
347
|
+
// Panel continues using previous role's calculation during swipe
|
|
348
|
+
expect(element.style.transform).toBe("translateX(-60px)");
|
|
349
|
+
|
|
350
|
+
// When swipe ends, panel transitions to new role
|
|
351
|
+
rerender(
|
|
352
|
+
<SwipeStackContent
|
|
353
|
+
id="behind"
|
|
354
|
+
depth={0}
|
|
355
|
+
navigationDepth={0}
|
|
356
|
+
isActive={true}
|
|
357
|
+
operationState={IDLE_STATE}
|
|
358
|
+
containerSize={containerSize}
|
|
359
|
+
>
|
|
360
|
+
Content
|
|
361
|
+
</SwipeStackContent>,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Now at rest position for active panel
|
|
365
|
+
act(() => {
|
|
366
|
+
flushRAF(0);
|
|
367
|
+
flushRAF(400);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("consecutive operations", () => {
|
|
375
|
+
/**
|
|
376
|
+
* Scenario: Complete a swipe back, then immediately start another swipe.
|
|
377
|
+
*
|
|
378
|
+
* This tests the "strong swipe" followed by another operation scenario.
|
|
379
|
+
*/
|
|
380
|
+
it("handles immediate second swipe after first swipe completes", () => {
|
|
381
|
+
const containerSize = 400;
|
|
382
|
+
|
|
383
|
+
// Start as behind panel
|
|
384
|
+
const { container, rerender } = render(
|
|
385
|
+
<SwipeStackContent
|
|
386
|
+
id="panel"
|
|
387
|
+
depth={0}
|
|
388
|
+
navigationDepth={1}
|
|
389
|
+
isActive={false}
|
|
390
|
+
operationState={IDLE_STATE}
|
|
391
|
+
containerSize={containerSize}
|
|
392
|
+
>
|
|
393
|
+
Content
|
|
394
|
+
</SwipeStackContent>,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const element = container.firstChild as HTMLElement;
|
|
398
|
+
expect(element.style.transform).toBe("translateX(-120px)");
|
|
399
|
+
|
|
400
|
+
// First swipe: 100%
|
|
401
|
+
rerender(
|
|
402
|
+
<SwipeStackContent
|
|
403
|
+
id="panel"
|
|
404
|
+
depth={0}
|
|
405
|
+
navigationDepth={1}
|
|
406
|
+
isActive={false}
|
|
407
|
+
operationState={createOperatingState(400)}
|
|
408
|
+
containerSize={containerSize}
|
|
409
|
+
>
|
|
410
|
+
Content
|
|
411
|
+
</SwipeStackContent>,
|
|
412
|
+
);
|
|
413
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
414
|
+
|
|
415
|
+
// First swipe ends, navigation changes
|
|
416
|
+
rerender(
|
|
417
|
+
<SwipeStackContent
|
|
418
|
+
id="panel"
|
|
419
|
+
depth={0}
|
|
420
|
+
navigationDepth={0}
|
|
421
|
+
isActive={true}
|
|
422
|
+
operationState={IDLE_STATE}
|
|
423
|
+
containerSize={containerSize}
|
|
424
|
+
>
|
|
425
|
+
Content
|
|
426
|
+
</SwipeStackContent>,
|
|
427
|
+
);
|
|
428
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
429
|
+
|
|
430
|
+
// Immediately start a second swipe (even though we can't go back further)
|
|
431
|
+
// This simulates the user continuing to swipe after navigation completed
|
|
432
|
+
rerender(
|
|
433
|
+
<SwipeStackContent
|
|
434
|
+
id="panel"
|
|
435
|
+
depth={0}
|
|
436
|
+
navigationDepth={0}
|
|
437
|
+
isActive={true}
|
|
438
|
+
operationState={createOperatingState(50)}
|
|
439
|
+
containerSize={containerSize}
|
|
440
|
+
>
|
|
441
|
+
Content
|
|
442
|
+
</SwipeStackContent>,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// Panel should respond to the new displacement
|
|
446
|
+
expect(element.style.transform).toBe("translateX(50px)");
|
|
447
|
+
|
|
448
|
+
// Release the second swipe
|
|
449
|
+
rerender(
|
|
450
|
+
<SwipeStackContent
|
|
451
|
+
id="panel"
|
|
452
|
+
depth={0}
|
|
453
|
+
navigationDepth={0}
|
|
454
|
+
isActive={true}
|
|
455
|
+
operationState={IDLE_STATE}
|
|
456
|
+
containerSize={containerSize}
|
|
457
|
+
>
|
|
458
|
+
Content
|
|
459
|
+
</SwipeStackContent>,
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
// Should snap back to 0
|
|
463
|
+
act(() => {
|
|
464
|
+
flushRAF(0);
|
|
465
|
+
flushRAF(400);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Scenario: Swipe ends with animation in progress, then new operation starts.
|
|
473
|
+
*
|
|
474
|
+
* This tests interrupting an animation with a new swipe.
|
|
475
|
+
*/
|
|
476
|
+
it("interrupts snap-back animation with new swipe", () => {
|
|
477
|
+
const containerSize = 400;
|
|
478
|
+
|
|
479
|
+
const { container, rerender } = render(
|
|
480
|
+
<SwipeStackContent
|
|
481
|
+
id="panel"
|
|
482
|
+
depth={0}
|
|
483
|
+
navigationDepth={0}
|
|
484
|
+
isActive={true}
|
|
485
|
+
operationState={IDLE_STATE}
|
|
486
|
+
containerSize={containerSize}
|
|
487
|
+
>
|
|
488
|
+
Content
|
|
489
|
+
</SwipeStackContent>,
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const element = container.firstChild as HTMLElement;
|
|
493
|
+
|
|
494
|
+
// Start a swipe
|
|
495
|
+
rerender(
|
|
496
|
+
<SwipeStackContent
|
|
497
|
+
id="panel"
|
|
498
|
+
depth={0}
|
|
499
|
+
navigationDepth={0}
|
|
500
|
+
isActive={true}
|
|
501
|
+
operationState={createOperatingState(200)}
|
|
502
|
+
containerSize={containerSize}
|
|
503
|
+
>
|
|
504
|
+
Content
|
|
505
|
+
</SwipeStackContent>,
|
|
506
|
+
);
|
|
507
|
+
expect(element.style.transform).toBe("translateX(200px)");
|
|
508
|
+
|
|
509
|
+
// Release (starts snap-back animation)
|
|
510
|
+
rerender(
|
|
511
|
+
<SwipeStackContent
|
|
512
|
+
id="panel"
|
|
513
|
+
depth={0}
|
|
514
|
+
navigationDepth={0}
|
|
515
|
+
isActive={true}
|
|
516
|
+
operationState={IDLE_STATE}
|
|
517
|
+
containerSize={containerSize}
|
|
518
|
+
>
|
|
519
|
+
Content
|
|
520
|
+
</SwipeStackContent>,
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
// Animation starts but we don't complete it
|
|
524
|
+
act(() => {
|
|
525
|
+
flushRAF(0); // Start animation
|
|
526
|
+
flushRAF(100); // Partial progress
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Position should be somewhere between 200 and 0
|
|
530
|
+
const currentTransform = element.style.transform;
|
|
531
|
+
const matchResult = currentTransform.match(/translateX\(([^)]+)px\)/);
|
|
532
|
+
const currentPx = matchResult ? parseFloat(matchResult[1]) : NaN;
|
|
533
|
+
|
|
534
|
+
// Should be between 0 and 200 (animation in progress)
|
|
535
|
+
expect(currentPx).toBeGreaterThanOrEqual(0);
|
|
536
|
+
expect(currentPx).toBeLessThanOrEqual(200);
|
|
537
|
+
|
|
538
|
+
// Start a new swipe (interrupts animation)
|
|
539
|
+
rerender(
|
|
540
|
+
<SwipeStackContent
|
|
541
|
+
id="panel"
|
|
542
|
+
depth={0}
|
|
543
|
+
navigationDepth={0}
|
|
544
|
+
isActive={true}
|
|
545
|
+
operationState={createOperatingState(150)}
|
|
546
|
+
containerSize={containerSize}
|
|
547
|
+
>
|
|
548
|
+
Content
|
|
549
|
+
</SwipeStackContent>,
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
// Should now be at the new displacement position
|
|
553
|
+
expect(element.style.transform).toBe("translateX(150px)");
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
describe("stack display mode (scale effect)", () => {
|
|
558
|
+
/**
|
|
559
|
+
* Scenario: Stack display mode adds scale transform.
|
|
560
|
+
*
|
|
561
|
+
* In stack mode, behind panels have a scale < 1.
|
|
562
|
+
* When the panel becomes active, the scale should animate to 1.
|
|
563
|
+
* This should not interfere with the translate animation.
|
|
564
|
+
*/
|
|
565
|
+
it("behind panel in stack mode transitions smoothly when becoming active", () => {
|
|
566
|
+
const containerSize = 400;
|
|
567
|
+
|
|
568
|
+
const { container, rerender } = render(
|
|
569
|
+
<SwipeStackContent
|
|
570
|
+
id="panel"
|
|
571
|
+
depth={0}
|
|
572
|
+
navigationDepth={1}
|
|
573
|
+
isActive={false}
|
|
574
|
+
operationState={IDLE_STATE}
|
|
575
|
+
containerSize={containerSize}
|
|
576
|
+
displayMode="stack"
|
|
577
|
+
>
|
|
578
|
+
Content
|
|
579
|
+
</SwipeStackContent>,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
const element = container.firstChild as HTMLElement;
|
|
583
|
+
|
|
584
|
+
// Behind panel should have scale(0.95)
|
|
585
|
+
expect(element.style.transform).toContain("scale(0.95)");
|
|
586
|
+
expect(element.style.transform).toContain("translateX(-120px)");
|
|
587
|
+
|
|
588
|
+
// 100% swipe
|
|
589
|
+
rerender(
|
|
590
|
+
<SwipeStackContent
|
|
591
|
+
id="panel"
|
|
592
|
+
depth={0}
|
|
593
|
+
navigationDepth={1}
|
|
594
|
+
isActive={false}
|
|
595
|
+
operationState={createOperatingState(400)}
|
|
596
|
+
containerSize={containerSize}
|
|
597
|
+
displayMode="stack"
|
|
598
|
+
>
|
|
599
|
+
Content
|
|
600
|
+
</SwipeStackContent>,
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
// At 100% swipe, scale should be 1 and position 0
|
|
604
|
+
expect(element.style.transform).toContain("translateX(0px)");
|
|
605
|
+
expect(element.style.transform).toContain("scale(1)");
|
|
606
|
+
|
|
607
|
+
// Swipe ends, becomes active
|
|
608
|
+
rerender(
|
|
609
|
+
<SwipeStackContent
|
|
610
|
+
id="panel"
|
|
611
|
+
depth={0}
|
|
612
|
+
navigationDepth={0}
|
|
613
|
+
isActive={true}
|
|
614
|
+
operationState={IDLE_STATE}
|
|
615
|
+
containerSize={containerSize}
|
|
616
|
+
displayMode="stack"
|
|
617
|
+
>
|
|
618
|
+
Content
|
|
619
|
+
</SwipeStackContent>,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// Should stay at 0 with scale 1 (no animation restart)
|
|
623
|
+
expect(element.style.transform).toContain("translateX(0px)");
|
|
624
|
+
|
|
625
|
+
act(() => {
|
|
626
|
+
flushRAF(0);
|
|
627
|
+
flushRAF(400);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
expect(element.style.transform).toContain("translateX(0px)");
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Scenario: Partial swipe in stack mode.
|
|
635
|
+
*
|
|
636
|
+
* After 80% swipe, behind panel should animate from -24px to 0px.
|
|
637
|
+
* Scale should also animate from ~0.99 to 1.
|
|
638
|
+
*/
|
|
639
|
+
it("partial swipe in stack mode animates from correct position", () => {
|
|
640
|
+
const containerSize = 400;
|
|
641
|
+
|
|
642
|
+
const { container, rerender } = render(
|
|
643
|
+
<SwipeStackContent
|
|
644
|
+
id="panel"
|
|
645
|
+
depth={0}
|
|
646
|
+
navigationDepth={1}
|
|
647
|
+
isActive={false}
|
|
648
|
+
operationState={IDLE_STATE}
|
|
649
|
+
containerSize={containerSize}
|
|
650
|
+
displayMode="stack"
|
|
651
|
+
>
|
|
652
|
+
Content
|
|
653
|
+
</SwipeStackContent>,
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const element = container.firstChild as HTMLElement;
|
|
657
|
+
|
|
658
|
+
// 80% swipe
|
|
659
|
+
rerender(
|
|
660
|
+
<SwipeStackContent
|
|
661
|
+
id="panel"
|
|
662
|
+
depth={0}
|
|
663
|
+
navigationDepth={1}
|
|
664
|
+
isActive={false}
|
|
665
|
+
operationState={createOperatingState(320)}
|
|
666
|
+
containerSize={containerSize}
|
|
667
|
+
displayMode="stack"
|
|
668
|
+
>
|
|
669
|
+
Content
|
|
670
|
+
</SwipeStackContent>,
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
expect(element.style.transform).toContain("translateX(-24px)");
|
|
674
|
+
|
|
675
|
+
// Becomes active
|
|
676
|
+
rerender(
|
|
677
|
+
<SwipeStackContent
|
|
678
|
+
id="panel"
|
|
679
|
+
depth={0}
|
|
680
|
+
navigationDepth={0}
|
|
681
|
+
isActive={true}
|
|
682
|
+
operationState={IDLE_STATE}
|
|
683
|
+
containerSize={containerSize}
|
|
684
|
+
displayMode="stack"
|
|
685
|
+
>
|
|
686
|
+
Content
|
|
687
|
+
</SwipeStackContent>,
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
// Should start animation from -24px
|
|
691
|
+
expect(element.style.transform).toContain("translateX(-24px)");
|
|
692
|
+
|
|
693
|
+
act(() => {
|
|
694
|
+
flushRAF(0);
|
|
695
|
+
flushRAF(400);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
expect(element.style.transform).toContain("translateX(0px)");
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
describe("over-swipe behavior (beyond 100%)", () => {
|
|
703
|
+
/**
|
|
704
|
+
* Scenario: User swipes beyond 100% (e.g., 500px when container is 400px).
|
|
705
|
+
*
|
|
706
|
+
* This is the "strong swipe" case:
|
|
707
|
+
* - behind panel should stay at 0px (clamped)
|
|
708
|
+
* - active panel should be at 500px (beyond container)
|
|
709
|
+
* - On release, exiting panel should animate from 500px to 400px
|
|
710
|
+
* - behind (now active) panel should stay at 0px (no animation needed)
|
|
711
|
+
*/
|
|
712
|
+
it("behind panel stays at 0px when over-swiped 125%", () => {
|
|
713
|
+
const containerSize = 400;
|
|
714
|
+
|
|
715
|
+
const { container, rerender } = render(
|
|
716
|
+
<SwipeStackContent
|
|
717
|
+
id="behind"
|
|
718
|
+
depth={0}
|
|
719
|
+
navigationDepth={1}
|
|
720
|
+
isActive={false}
|
|
721
|
+
operationState={IDLE_STATE}
|
|
722
|
+
containerSize={containerSize}
|
|
723
|
+
>
|
|
724
|
+
Content
|
|
725
|
+
</SwipeStackContent>,
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
const element = container.firstChild as HTMLElement;
|
|
729
|
+
expect(element.style.transform).toBe("translateX(-120px)");
|
|
730
|
+
|
|
731
|
+
// Over-swipe: 125% (500px)
|
|
732
|
+
rerender(
|
|
733
|
+
<SwipeStackContent
|
|
734
|
+
id="behind"
|
|
735
|
+
depth={0}
|
|
736
|
+
navigationDepth={1}
|
|
737
|
+
isActive={false}
|
|
738
|
+
operationState={createOperatingState(500)}
|
|
739
|
+
containerSize={containerSize}
|
|
740
|
+
>
|
|
741
|
+
Content
|
|
742
|
+
</SwipeStackContent>,
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
// Behind panel should be clamped at 0px (not going positive)
|
|
746
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
747
|
+
|
|
748
|
+
// Release and navigation changes
|
|
749
|
+
rerender(
|
|
750
|
+
<SwipeStackContent
|
|
751
|
+
id="behind"
|
|
752
|
+
depth={0}
|
|
753
|
+
navigationDepth={0}
|
|
754
|
+
isActive={true}
|
|
755
|
+
operationState={IDLE_STATE}
|
|
756
|
+
containerSize={containerSize}
|
|
757
|
+
>
|
|
758
|
+
Content
|
|
759
|
+
</SwipeStackContent>,
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
// Should stay at 0px immediately (no jump)
|
|
763
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
764
|
+
|
|
765
|
+
act(() => {
|
|
766
|
+
flushRAF(0);
|
|
767
|
+
flushRAF(400);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// After any animation, still at 0px
|
|
771
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it("exiting panel at 125% swipe snaps to target (no backward animation)", () => {
|
|
775
|
+
const containerSize = 400;
|
|
776
|
+
|
|
777
|
+
const { container, rerender } = render(
|
|
778
|
+
<SwipeStackContent
|
|
779
|
+
id="exiting"
|
|
780
|
+
depth={1}
|
|
781
|
+
navigationDepth={1}
|
|
782
|
+
isActive={true}
|
|
783
|
+
operationState={IDLE_STATE}
|
|
784
|
+
containerSize={containerSize}
|
|
785
|
+
>
|
|
786
|
+
Content
|
|
787
|
+
</SwipeStackContent>,
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
const element = container.firstChild as HTMLElement;
|
|
791
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
792
|
+
|
|
793
|
+
// Over-swipe: 125%
|
|
794
|
+
rerender(
|
|
795
|
+
<SwipeStackContent
|
|
796
|
+
id="exiting"
|
|
797
|
+
depth={1}
|
|
798
|
+
navigationDepth={1}
|
|
799
|
+
isActive={true}
|
|
800
|
+
operationState={createOperatingState(500)}
|
|
801
|
+
containerSize={containerSize}
|
|
802
|
+
>
|
|
803
|
+
Content
|
|
804
|
+
</SwipeStackContent>,
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
// Active panel follows displacement exactly (no clamp)
|
|
808
|
+
expect(element.style.transform).toBe("translateX(500px)");
|
|
809
|
+
|
|
810
|
+
// Over-swipe triggers navigation change while STILL SWIPING
|
|
811
|
+
// This is the key: navigation changes before the gesture ends
|
|
812
|
+
rerender(
|
|
813
|
+
<SwipeStackContent
|
|
814
|
+
id="exiting"
|
|
815
|
+
depth={1}
|
|
816
|
+
navigationDepth={0}
|
|
817
|
+
isActive={false}
|
|
818
|
+
operationState={createOperatingState(500)}
|
|
819
|
+
containerSize={containerSize}
|
|
820
|
+
>
|
|
821
|
+
Content
|
|
822
|
+
</SwipeStackContent>,
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
// Panel maintains position (useOperationContinuity keeps using old role)
|
|
826
|
+
expect(element.style.transform).toBe("translateX(500px)");
|
|
827
|
+
|
|
828
|
+
// Now release - gesture ends
|
|
829
|
+
rerender(
|
|
830
|
+
<SwipeStackContent
|
|
831
|
+
id="exiting"
|
|
832
|
+
depth={1}
|
|
833
|
+
navigationDepth={0}
|
|
834
|
+
isActive={false}
|
|
835
|
+
operationState={IDLE_STATE}
|
|
836
|
+
containerSize={containerSize}
|
|
837
|
+
>
|
|
838
|
+
Content
|
|
839
|
+
</SwipeStackContent>,
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
// Should snap to target position (400px) - no backward ANIMATION
|
|
843
|
+
// The key is we snap (instant) rather than animate backward
|
|
844
|
+
// Since the panel is off-screen, snapping to 400px is visually acceptable
|
|
845
|
+
expect(element.style.transform).toBe("translateX(400px)");
|
|
846
|
+
|
|
847
|
+
act(() => {
|
|
848
|
+
flushRAF(0);
|
|
849
|
+
flushRAF(400);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// After frames, still at target position (no animation occurred)
|
|
853
|
+
expect(element.style.transform).toBe("translateX(400px)");
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* This tests both panels together during over-swipe.
|
|
858
|
+
* The exiting panel should stay at 500px (no backward animation).
|
|
859
|
+
*/
|
|
860
|
+
it("both panels handle 125% over-swipe transition smoothly", () => {
|
|
861
|
+
const containerSize = 400;
|
|
862
|
+
|
|
863
|
+
// Render behind panel
|
|
864
|
+
const behindResult = render(
|
|
865
|
+
<SwipeStackContent
|
|
866
|
+
id="behind"
|
|
867
|
+
depth={0}
|
|
868
|
+
navigationDepth={1}
|
|
869
|
+
isActive={false}
|
|
870
|
+
operationState={IDLE_STATE}
|
|
871
|
+
containerSize={containerSize}
|
|
872
|
+
>
|
|
873
|
+
Behind
|
|
874
|
+
</SwipeStackContent>,
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
// Render active panel
|
|
878
|
+
const activeResult = render(
|
|
879
|
+
<SwipeStackContent
|
|
880
|
+
id="active"
|
|
881
|
+
depth={1}
|
|
882
|
+
navigationDepth={1}
|
|
883
|
+
isActive={true}
|
|
884
|
+
operationState={IDLE_STATE}
|
|
885
|
+
containerSize={containerSize}
|
|
886
|
+
>
|
|
887
|
+
Active
|
|
888
|
+
</SwipeStackContent>,
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
const behindEl = behindResult.container.firstChild as HTMLElement;
|
|
892
|
+
const activeEl = activeResult.container.firstChild as HTMLElement;
|
|
893
|
+
|
|
894
|
+
// Over-swipe 125%
|
|
895
|
+
const overSwipeState = createOperatingState(500);
|
|
896
|
+
|
|
897
|
+
behindResult.rerender(
|
|
898
|
+
<SwipeStackContent
|
|
899
|
+
id="behind"
|
|
900
|
+
depth={0}
|
|
901
|
+
navigationDepth={1}
|
|
902
|
+
isActive={false}
|
|
903
|
+
operationState={overSwipeState}
|
|
904
|
+
containerSize={containerSize}
|
|
905
|
+
>
|
|
906
|
+
Behind
|
|
907
|
+
</SwipeStackContent>,
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
activeResult.rerender(
|
|
911
|
+
<SwipeStackContent
|
|
912
|
+
id="active"
|
|
913
|
+
depth={1}
|
|
914
|
+
navigationDepth={1}
|
|
915
|
+
isActive={true}
|
|
916
|
+
operationState={overSwipeState}
|
|
917
|
+
containerSize={containerSize}
|
|
918
|
+
>
|
|
919
|
+
Active
|
|
920
|
+
</SwipeStackContent>,
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
// Positions during over-swipe
|
|
924
|
+
expect(behindEl.style.transform).toBe("translateX(0px)");
|
|
925
|
+
expect(activeEl.style.transform).toBe("translateX(500px)");
|
|
926
|
+
|
|
927
|
+
// Over-swipe triggers navigation change while STILL SWIPING
|
|
928
|
+
behindResult.rerender(
|
|
929
|
+
<SwipeStackContent
|
|
930
|
+
id="behind"
|
|
931
|
+
depth={0}
|
|
932
|
+
navigationDepth={0}
|
|
933
|
+
isActive={true}
|
|
934
|
+
operationState={overSwipeState}
|
|
935
|
+
containerSize={containerSize}
|
|
936
|
+
>
|
|
937
|
+
Behind
|
|
938
|
+
</SwipeStackContent>,
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
activeResult.rerender(
|
|
942
|
+
<SwipeStackContent
|
|
943
|
+
id="active"
|
|
944
|
+
depth={1}
|
|
945
|
+
navigationDepth={0}
|
|
946
|
+
isActive={false}
|
|
947
|
+
operationState={overSwipeState}
|
|
948
|
+
containerSize={containerSize}
|
|
949
|
+
>
|
|
950
|
+
Active
|
|
951
|
+
</SwipeStackContent>,
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
// Panels maintain positions (useOperationContinuity keeps using old roles)
|
|
955
|
+
expect(behindEl.style.transform).toBe("translateX(0px)");
|
|
956
|
+
expect(activeEl.style.transform).toBe("translateX(500px)");
|
|
957
|
+
|
|
958
|
+
// Now release - gesture ends
|
|
959
|
+
behindResult.rerender(
|
|
960
|
+
<SwipeStackContent
|
|
961
|
+
id="behind"
|
|
962
|
+
depth={0}
|
|
963
|
+
navigationDepth={0}
|
|
964
|
+
isActive={true}
|
|
965
|
+
operationState={IDLE_STATE}
|
|
966
|
+
containerSize={containerSize}
|
|
967
|
+
>
|
|
968
|
+
Behind
|
|
969
|
+
</SwipeStackContent>,
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
activeResult.rerender(
|
|
973
|
+
<SwipeStackContent
|
|
974
|
+
id="active"
|
|
975
|
+
depth={1}
|
|
976
|
+
navigationDepth={0}
|
|
977
|
+
isActive={false}
|
|
978
|
+
operationState={IDLE_STATE}
|
|
979
|
+
containerSize={containerSize}
|
|
980
|
+
>
|
|
981
|
+
Active
|
|
982
|
+
</SwipeStackContent>,
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
// Behind (now active) should stay at 0
|
|
986
|
+
expect(behindEl.style.transform).toBe("translateX(0px)");
|
|
987
|
+
// Active (now exiting) snaps to target (400px) - no backward ANIMATION
|
|
988
|
+
expect(activeEl.style.transform).toBe("translateX(400px)");
|
|
989
|
+
|
|
990
|
+
act(() => {
|
|
991
|
+
flushRAF(0);
|
|
992
|
+
flushRAF(400);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// Final positions - both at their target positions, no animation occurred
|
|
996
|
+
expect(behindEl.style.transform).toBe("translateX(0px)");
|
|
997
|
+
expect(activeEl.style.transform).toBe("translateX(400px)");
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
describe("exiting panel behavior", () => {
|
|
1002
|
+
/**
|
|
1003
|
+
* Scenario: Panel exits (becomes hidden) after swipe back.
|
|
1004
|
+
*
|
|
1005
|
+
* When a panel at depth 1 becomes hidden (navigationDepth changes to 0),
|
|
1006
|
+
* it should animate off-screen from its current position.
|
|
1007
|
+
*/
|
|
1008
|
+
it("exiting panel animates from current position", () => {
|
|
1009
|
+
const containerSize = 400;
|
|
1010
|
+
|
|
1011
|
+
// Panel at depth 1, currently active
|
|
1012
|
+
const { container, rerender } = render(
|
|
1013
|
+
<SwipeStackContent
|
|
1014
|
+
id="exiting"
|
|
1015
|
+
depth={1}
|
|
1016
|
+
navigationDepth={1}
|
|
1017
|
+
isActive={true}
|
|
1018
|
+
operationState={IDLE_STATE}
|
|
1019
|
+
containerSize={containerSize}
|
|
1020
|
+
>
|
|
1021
|
+
Content
|
|
1022
|
+
</SwipeStackContent>,
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
const element = container.firstChild as HTMLElement;
|
|
1026
|
+
expect(element.style.transform).toBe("translateX(0px)");
|
|
1027
|
+
|
|
1028
|
+
// Swipe 100% to go back
|
|
1029
|
+
rerender(
|
|
1030
|
+
<SwipeStackContent
|
|
1031
|
+
id="exiting"
|
|
1032
|
+
depth={1}
|
|
1033
|
+
navigationDepth={1}
|
|
1034
|
+
isActive={true}
|
|
1035
|
+
operationState={createOperatingState(400)}
|
|
1036
|
+
containerSize={containerSize}
|
|
1037
|
+
>
|
|
1038
|
+
Content
|
|
1039
|
+
</SwipeStackContent>,
|
|
1040
|
+
);
|
|
1041
|
+
expect(element.style.transform).toBe("translateX(400px)");
|
|
1042
|
+
|
|
1043
|
+
// Navigation changes - panel becomes hidden
|
|
1044
|
+
rerender(
|
|
1045
|
+
<SwipeStackContent
|
|
1046
|
+
id="exiting"
|
|
1047
|
+
depth={1}
|
|
1048
|
+
navigationDepth={0}
|
|
1049
|
+
isActive={false}
|
|
1050
|
+
operationState={IDLE_STATE}
|
|
1051
|
+
containerSize={containerSize}
|
|
1052
|
+
>
|
|
1053
|
+
Content
|
|
1054
|
+
</SwipeStackContent>,
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
// Panel is already at 400px (off-screen), should stay there
|
|
1058
|
+
expect(element.style.transform).toBe("translateX(400px)");
|
|
1059
|
+
|
|
1060
|
+
act(() => {
|
|
1061
|
+
flushRAF(0);
|
|
1062
|
+
flushRAF(400);
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// After animation, should be at containerSize (off-screen)
|
|
1066
|
+
expect(element.style.transform).toBe("translateX(400px)");
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Scenario: Panel exits after 80% swipe.
|
|
1071
|
+
*
|
|
1072
|
+
* The panel should animate from ~320px to 400px, not from 0px.
|
|
1073
|
+
*/
|
|
1074
|
+
it("exiting panel at 80% swipe animates from 320px to 400px", () => {
|
|
1075
|
+
const containerSize = 400;
|
|
1076
|
+
|
|
1077
|
+
const { container, rerender } = render(
|
|
1078
|
+
<SwipeStackContent
|
|
1079
|
+
id="exiting"
|
|
1080
|
+
depth={1}
|
|
1081
|
+
navigationDepth={1}
|
|
1082
|
+
isActive={true}
|
|
1083
|
+
operationState={IDLE_STATE}
|
|
1084
|
+
containerSize={containerSize}
|
|
1085
|
+
>
|
|
1086
|
+
Content
|
|
1087
|
+
</SwipeStackContent>,
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
const element = container.firstChild as HTMLElement;
|
|
1091
|
+
|
|
1092
|
+
// Swipe 80%
|
|
1093
|
+
rerender(
|
|
1094
|
+
<SwipeStackContent
|
|
1095
|
+
id="exiting"
|
|
1096
|
+
depth={1}
|
|
1097
|
+
navigationDepth={1}
|
|
1098
|
+
isActive={true}
|
|
1099
|
+
operationState={createOperatingState(320)}
|
|
1100
|
+
containerSize={containerSize}
|
|
1101
|
+
>
|
|
1102
|
+
Content
|
|
1103
|
+
</SwipeStackContent>,
|
|
1104
|
+
);
|
|
1105
|
+
expect(element.style.transform).toBe("translateX(320px)");
|
|
1106
|
+
|
|
1107
|
+
// Navigation changes
|
|
1108
|
+
rerender(
|
|
1109
|
+
<SwipeStackContent
|
|
1110
|
+
id="exiting"
|
|
1111
|
+
depth={1}
|
|
1112
|
+
navigationDepth={0}
|
|
1113
|
+
isActive={false}
|
|
1114
|
+
operationState={IDLE_STATE}
|
|
1115
|
+
containerSize={containerSize}
|
|
1116
|
+
>
|
|
1117
|
+
Content
|
|
1118
|
+
</SwipeStackContent>,
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
// Should be at 320px (animation starting point)
|
|
1122
|
+
expect(element.style.transform).toBe("translateX(320px)");
|
|
1123
|
+
|
|
1124
|
+
act(() => {
|
|
1125
|
+
flushRAF(0);
|
|
1126
|
+
flushRAF(400);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
// After animation, at containerSize
|
|
1130
|
+
expect(element.style.transform).toBe("translateX(400px)");
|
|
1131
|
+
});
|
|
1132
|
+
});
|
|
1133
|
+
});
|