react-panel-layout 0.6.1 → 0.7.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/FloatingWindow-CE-WzkNv.js +1542 -0
- package/dist/FloatingWindow-CE-WzkNv.js.map +1 -0
- package/dist/FloatingWindow-DpFpmX1f.cjs +2 -0
- package/dist/FloatingWindow-DpFpmX1f.cjs.map +1 -0
- package/dist/GridLayout-EwKszYBy.cjs +2 -0
- package/dist/{GridLayout-DKTg_N61.cjs.map → GridLayout-EwKszYBy.cjs.map} +1 -1
- package/dist/GridLayout-kiWdpMLQ.js +947 -0
- package/dist/{GridLayout-UWNxXw77.js.map → GridLayout-kiWdpMLQ.js.map} +1 -1
- package/dist/PanelSystem-Dmy5YI_6.cjs +3 -0
- package/dist/PanelSystem-Dmy5YI_6.cjs.map +1 -0
- package/dist/{PanelSystem-BqUzNtf2.js → PanelSystem-DrYsYwuV.js} +208 -247
- package/dist/PanelSystem-DrYsYwuV.js.map +1 -0
- package/dist/components/window/Drawer.d.ts +1 -0
- package/dist/components/window/DrawerRevealContext.d.ts +61 -0
- package/dist/components/window/drawerRevealAnimationUtils.d.ts +212 -0
- package/dist/components/window/drawerStyles.d.ts +5 -0
- package/dist/components/window/useDrawerSwipeTransform.d.ts +8 -2
- package/dist/components/window/useDrawerTransform.d.ts +68 -0
- package/dist/components/window/useRevealDrawerTransform.d.ts +56 -0
- package/dist/config.cjs +1 -1
- package/dist/config.cjs.map +1 -1
- package/dist/config.js +8 -7
- package/dist/config.js.map +1 -1
- package/dist/dialog/index.d.ts +1 -1
- package/dist/grid.cjs +1 -1
- package/dist/grid.js +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +4 -4
- package/dist/modules/dialog/DialogContainer.d.ts +22 -2
- package/dist/modules/dialog/Modal.d.ts +23 -2
- package/dist/modules/dialog/SwipeDialogContainer.d.ts +6 -2
- package/dist/modules/dialog/types.d.ts +12 -0
- package/dist/modules/drawer/drawerStateMachine.d.ts +168 -0
- package/dist/modules/drawer/revealDrawerConstants.d.ts +33 -0
- package/dist/modules/drawer/revealDrawerStateMachine.d.ts +146 -0
- package/dist/modules/drawer/strategies/index.d.ts +8 -0
- package/dist/modules/drawer/strategies/overlayStrategy.d.ts +12 -0
- package/dist/modules/drawer/strategies/revealStrategy.d.ts +12 -0
- package/dist/modules/drawer/strategies/types.d.ts +116 -0
- package/dist/panels.cjs +1 -1
- package/dist/panels.js +1 -1
- package/dist/stack.cjs +1 -1
- package/dist/stack.cjs.map +1 -1
- package/dist/stack.js +306 -347
- package/dist/stack.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/useAnimationFrame-CRuFlk5t.js +394 -0
- package/dist/useAnimationFrame-CRuFlk5t.js.map +1 -0
- package/dist/useAnimationFrame-XRpDXkwV.cjs +2 -0
- package/dist/useAnimationFrame-XRpDXkwV.cjs.map +1 -0
- package/dist/window.cjs +1 -1
- package/dist/window.js +1 -1
- package/package.json +1 -1
- package/src/components/gesture/SwipeSafeZone.tsx +1 -0
- package/src/components/grid/GridLayout.tsx +110 -38
- package/src/components/window/Drawer.tsx +114 -10
- package/src/components/window/DrawerLayers.tsx +48 -15
- package/src/components/window/DrawerRevealContext.spec.ts +20 -0
- package/src/components/window/DrawerRevealContext.tsx +99 -0
- package/src/components/window/drawerRevealAnimationUtils.spec.ts +375 -0
- package/src/components/window/drawerRevealAnimationUtils.ts +415 -0
- package/src/components/window/drawerStyles.spec.ts +39 -0
- package/src/components/window/drawerStyles.ts +24 -0
- package/src/components/window/useDrawerSwipeTransform.ts +28 -90
- package/src/components/window/useDrawerTransform.ts +505 -0
- package/src/components/window/useRevealDrawerTransform.spec.ts +1936 -0
- package/src/components/window/useRevealDrawerTransform.ts +105 -0
- package/src/demo/components/FullscreenDemoPage.tsx +47 -0
- package/src/demo/fullscreenRoutes.tsx +32 -0
- package/src/demo/index.tsx +5 -0
- package/src/demo/pages/Dialog/components/CardExpandDemo.tsx +23 -8
- package/src/demo/pages/Drawer/components/DrawerBasics.module.css +6 -1
- package/src/demo/pages/Drawer/components/DrawerBasics.tsx +14 -4
- package/src/demo/pages/Drawer/components/DrawerReveal.module.css +157 -0
- package/src/demo/pages/Drawer/components/DrawerReveal.tsx +128 -0
- package/src/demo/pages/Drawer/reveal/index.tsx +17 -0
- package/src/demo/pages/Drawer/reveal-fullscreen/index.tsx +135 -0
- package/src/demo/pages/Drawer/reveal-fullscreen/styles.module.css +233 -0
- package/src/demo/pages/Stack/components/StackBasics.spec.tsx +56 -52
- package/src/demo/pages/Stack/components/StackTablet.spec.tsx +39 -49
- package/src/demo/routes.tsx +2 -0
- package/src/dialog/index.ts +2 -0
- package/src/hooks/gesture/testing/createGestureSimulator.ts +1 -0
- package/src/hooks/gesture/useNativeGestureGuard.spec.ts +10 -2
- package/src/hooks/gesture/useSwipeInput.spec.ts +69 -0
- package/src/hooks/gesture/useSwipeInput.ts +2 -0
- package/src/hooks/gesture/utils.ts +15 -4
- package/src/hooks/useAnimatedVisibility.spec.ts +3 -3
- package/src/hooks/useOperationContinuity.spec.ts +17 -10
- package/src/hooks/useOperationContinuity.ts +5 -5
- package/src/hooks/useSharedElementTransition.ts +28 -7
- package/src/modules/dialog/DialogContainer.tsx +39 -5
- package/src/modules/dialog/Modal.tsx +46 -4
- package/src/modules/dialog/SwipeDialogContainer.tsx +12 -2
- package/src/modules/dialog/dialogAnimationUtils.spec.ts +0 -1
- package/src/modules/dialog/types.ts +14 -0
- package/src/modules/dialog/useDialogContainer.spec.ts +11 -3
- package/src/modules/dialog/useDialogSwipeInput.spec.ts +49 -28
- package/src/modules/dialog/useDialogSwipeInput.ts +37 -6
- package/src/modules/dialog/useDialogTransform.spec.ts +63 -30
- package/src/modules/drawer/drawerStateMachine.ts +500 -0
- package/src/modules/drawer/revealDrawerConstants.ts +38 -0
- package/src/modules/drawer/revealDrawerStateMachine.spec.ts +558 -0
- package/src/modules/drawer/revealDrawerStateMachine.ts +197 -0
- package/src/modules/drawer/strategies/index.ts +9 -0
- package/src/modules/drawer/strategies/overlayStrategy.ts +133 -0
- package/src/modules/drawer/strategies/revealStrategy.ts +111 -0
- package/src/modules/drawer/strategies/types.ts +160 -0
- package/src/modules/drawer/useDrawerSwipeInput.ts +7 -4
- package/src/modules/pivot/SwipePivotContent.spec.tsx +48 -37
- package/src/modules/pivot/usePivotSwipeInput.spec.ts +8 -8
- package/src/modules/stack/swipeTransitionContinuity.spec.tsx +1 -1
- package/src/types.ts +15 -0
- package/dist/FloatingWindow-CUXnEtrb.js +0 -827
- package/dist/FloatingWindow-CUXnEtrb.js.map +0 -1
- package/dist/FloatingWindow-DMwyK0eK.cjs +0 -2
- package/dist/FloatingWindow-DMwyK0eK.cjs.map +0 -1
- package/dist/GridLayout-DKTg_N61.cjs +0 -2
- package/dist/GridLayout-UWNxXw77.js +0 -926
- package/dist/PanelSystem-BqUzNtf2.js.map +0 -1
- package/dist/PanelSystem-D603LKKv.cjs +0 -3
- package/dist/PanelSystem-D603LKKv.cjs.map +0 -1
- package/dist/useNativeGestureGuard-C7TSqEkr.cjs +0 -2
- package/dist/useNativeGestureGuard-C7TSqEkr.cjs.map +0 -1
- package/dist/useNativeGestureGuard-CGYo6O0r.js +0 -347
- package/dist/useNativeGestureGuard-CGYo6O0r.js.map +0 -1
- package/src/components/window/useDrawerSwipeTransform.spec.ts +0 -234
|
@@ -0,0 +1,1936 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tests for useRevealDrawerTransform hook.
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive tests covering all operation scenarios for reveal drawer.
|
|
5
|
+
* Tests follow TDD approach to ensure animation continuity and correct state management.
|
|
6
|
+
*/
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { renderHook, act } from "@testing-library/react";
|
|
9
|
+
import { useRevealDrawerTransform } from "./useRevealDrawerTransform.js";
|
|
10
|
+
import type { UseRevealDrawerTransformOptions } from "./useRevealDrawerTransform.js";
|
|
11
|
+
import type { ContinuousOperationState } from "../../hooks/gesture/types.js";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Test Utilities
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create mock drawer element with style tracking.
|
|
19
|
+
*/
|
|
20
|
+
function createMockDrawer(): HTMLDivElement {
|
|
21
|
+
const element = document.createElement("div");
|
|
22
|
+
return element;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create mock content element (simulates #root or grid container).
|
|
27
|
+
*/
|
|
28
|
+
function createMockContent(): HTMLDivElement {
|
|
29
|
+
const element = document.createElement("div");
|
|
30
|
+
element.id = "root";
|
|
31
|
+
document.body.appendChild(element);
|
|
32
|
+
return element;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create idle swipe state.
|
|
37
|
+
*/
|
|
38
|
+
function createIdleSwipeState(): ContinuousOperationState {
|
|
39
|
+
return {
|
|
40
|
+
phase: "idle",
|
|
41
|
+
displacement: { x: 0, y: 0 },
|
|
42
|
+
velocity: { x: 0, y: 0 },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create operating swipe state.
|
|
48
|
+
*/
|
|
49
|
+
function createOperatingSwipeState(displacement: { x: number; y: number }): ContinuousOperationState {
|
|
50
|
+
return {
|
|
51
|
+
phase: "operating",
|
|
52
|
+
displacement,
|
|
53
|
+
velocity: { x: 0, y: 0 },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Default options for hook.
|
|
59
|
+
*/
|
|
60
|
+
function createDefaultOptions(
|
|
61
|
+
drawerRef: React.RefObject<HTMLElement | null>,
|
|
62
|
+
overrides: Partial<UseRevealDrawerTransformOptions> = {},
|
|
63
|
+
): UseRevealDrawerTransformOptions {
|
|
64
|
+
return {
|
|
65
|
+
drawerRef,
|
|
66
|
+
placement: "left",
|
|
67
|
+
drawerSize: 300,
|
|
68
|
+
isOpen: false,
|
|
69
|
+
swipeState: createIdleSwipeState(),
|
|
70
|
+
displacement: 0,
|
|
71
|
+
isOpening: false,
|
|
72
|
+
isClosing: false,
|
|
73
|
+
enabled: true,
|
|
74
|
+
inline: false,
|
|
75
|
+
contentBackground: "#fff",
|
|
76
|
+
...overrides,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Setup mock requestAnimationFrame for animation testing.
|
|
82
|
+
*/
|
|
83
|
+
function setupAnimationFrameMock(): { runFrame: () => void; runAllFrames: () => void } {
|
|
84
|
+
const callbacks: FrameRequestCallback[] = [];
|
|
85
|
+
const counter = { frameId: 0 };
|
|
86
|
+
|
|
87
|
+
globalThis.requestAnimationFrame = (callback: FrameRequestCallback): number => {
|
|
88
|
+
callbacks.push(callback);
|
|
89
|
+
counter.frameId += 1;
|
|
90
|
+
return counter.frameId;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
globalThis.cancelAnimationFrame = (): void => {
|
|
94
|
+
// Simple implementation - in real tests we might need to track by ID
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
runFrame: () => {
|
|
99
|
+
const callback = callbacks.shift();
|
|
100
|
+
if (callback) {
|
|
101
|
+
callback(performance.now());
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
runAllFrames: () => {
|
|
105
|
+
while (callbacks.length > 0) {
|
|
106
|
+
const callback = callbacks.shift();
|
|
107
|
+
if (callback) {
|
|
108
|
+
callback(performance.now());
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Cleanup after tests.
|
|
117
|
+
*/
|
|
118
|
+
function cleanup(): void {
|
|
119
|
+
const root = document.getElementById("root");
|
|
120
|
+
if (root) {
|
|
121
|
+
root.remove();
|
|
122
|
+
}
|
|
123
|
+
document.body.style.overflow = "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Tests
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
describe("useRevealDrawerTransform", () => {
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
cleanup();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// --------------------------------------------------------------------------
|
|
136
|
+
// Initial State Tests
|
|
137
|
+
// --------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
describe("Initial State", () => {
|
|
140
|
+
it("starts with closed phase when isOpen=false", () => {
|
|
141
|
+
const drawer = createMockDrawer();
|
|
142
|
+
const ref = { current: drawer };
|
|
143
|
+
createMockContent();
|
|
144
|
+
|
|
145
|
+
const { result } = renderHook(() =>
|
|
146
|
+
useRevealDrawerTransform(createDefaultOptions(ref, { isOpen: false })),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(result.current.phase).toBe("closed");
|
|
150
|
+
expect(result.current.isAnimating).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("starts with open phase when isOpen=true", () => {
|
|
154
|
+
const drawer = createMockDrawer();
|
|
155
|
+
const ref = { current: drawer };
|
|
156
|
+
createMockContent();
|
|
157
|
+
|
|
158
|
+
const { result } = renderHook(() =>
|
|
159
|
+
useRevealDrawerTransform(createDefaultOptions(ref, { isOpen: true })),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(result.current.phase).toBe("open");
|
|
163
|
+
expect(result.current.isAnimating).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("drawer is hidden initially when closed", () => {
|
|
167
|
+
const drawer = createMockDrawer();
|
|
168
|
+
const ref = { current: drawer };
|
|
169
|
+
createMockContent();
|
|
170
|
+
|
|
171
|
+
renderHook(() =>
|
|
172
|
+
useRevealDrawerTransform(createDefaultOptions(ref, { isOpen: false })),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(drawer.style.visibility).toBe("hidden");
|
|
176
|
+
expect(drawer.style.pointerEvents).toBe("none");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("drawer is visible initially when open", () => {
|
|
180
|
+
const drawer = createMockDrawer();
|
|
181
|
+
const ref = { current: drawer };
|
|
182
|
+
createMockContent();
|
|
183
|
+
|
|
184
|
+
renderHook(() =>
|
|
185
|
+
useRevealDrawerTransform(createDefaultOptions(ref, { isOpen: true })),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// When open, drawer should be visible with correct transform
|
|
189
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
190
|
+
expect(drawer.style.pointerEvents).toBe("auto");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// --------------------------------------------------------------------------
|
|
195
|
+
// Swipe Opening Tests
|
|
196
|
+
// --------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
describe("Opening via Swipe", () => {
|
|
199
|
+
it("drawer becomes visible when swipe starts", () => {
|
|
200
|
+
const drawer = createMockDrawer();
|
|
201
|
+
const ref = { current: drawer };
|
|
202
|
+
createMockContent();
|
|
203
|
+
|
|
204
|
+
const { rerender } = renderHook(
|
|
205
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
206
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Start opening swipe
|
|
210
|
+
rerender(createDefaultOptions(ref, {
|
|
211
|
+
isOpen: false,
|
|
212
|
+
swipeState: createOperatingSwipeState({ x: 50, y: 0 }),
|
|
213
|
+
displacement: 50,
|
|
214
|
+
isOpening: true,
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("drawer transform updates during swipe", () => {
|
|
221
|
+
const drawer = createMockDrawer();
|
|
222
|
+
const ref = { current: drawer };
|
|
223
|
+
createMockContent();
|
|
224
|
+
|
|
225
|
+
const { rerender } = renderHook(
|
|
226
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
227
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Swipe to 50%
|
|
231
|
+
rerender(createDefaultOptions(ref, {
|
|
232
|
+
isOpen: false,
|
|
233
|
+
swipeState: createOperatingSwipeState({ x: 150, y: 0 }),
|
|
234
|
+
displacement: 150,
|
|
235
|
+
isOpening: true,
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
// Drawer should have transform applied
|
|
239
|
+
expect(drawer.style.transform).not.toBe("");
|
|
240
|
+
expect(drawer.style.transform).toContain("translate");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("content transform updates during swipe", () => {
|
|
244
|
+
const drawer = createMockDrawer();
|
|
245
|
+
const ref = { current: drawer };
|
|
246
|
+
const content = createMockContent();
|
|
247
|
+
|
|
248
|
+
const { rerender } = renderHook(
|
|
249
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
250
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Swipe to 50%
|
|
254
|
+
rerender(createDefaultOptions(ref, {
|
|
255
|
+
isOpen: false,
|
|
256
|
+
swipeState: createOperatingSwipeState({ x: 150, y: 0 }),
|
|
257
|
+
displacement: 150,
|
|
258
|
+
isOpening: true,
|
|
259
|
+
}));
|
|
260
|
+
|
|
261
|
+
// Content should have transform applied
|
|
262
|
+
expect(content.style.transform).not.toBe("");
|
|
263
|
+
expect(content.style.transform).toContain("translate");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("stacking context is applied to content during swipe", () => {
|
|
267
|
+
const drawer = createMockDrawer();
|
|
268
|
+
const ref = { current: drawer };
|
|
269
|
+
const content = createMockContent();
|
|
270
|
+
|
|
271
|
+
const { rerender } = renderHook(
|
|
272
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
273
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Start opening swipe
|
|
277
|
+
rerender(createDefaultOptions(ref, {
|
|
278
|
+
isOpen: false,
|
|
279
|
+
swipeState: createOperatingSwipeState({ x: 50, y: 0 }),
|
|
280
|
+
displacement: 50,
|
|
281
|
+
isOpening: true,
|
|
282
|
+
}));
|
|
283
|
+
|
|
284
|
+
expect(content.style.position).toBe("relative");
|
|
285
|
+
expect(content.style.zIndex).toBe("1");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// --------------------------------------------------------------------------
|
|
290
|
+
// Animation Continuity Tests (Critical for preventing jumps)
|
|
291
|
+
// --------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
describe("Animation Continuity", () => {
|
|
294
|
+
it("preserves position when swipe ends - no jump", () => {
|
|
295
|
+
const drawer = createMockDrawer();
|
|
296
|
+
const ref = { current: drawer };
|
|
297
|
+
createMockContent();
|
|
298
|
+
setupAnimationFrameMock();
|
|
299
|
+
|
|
300
|
+
const { rerender } = renderHook(
|
|
301
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
302
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Swipe to 50%
|
|
306
|
+
rerender(createDefaultOptions(ref, {
|
|
307
|
+
isOpen: false,
|
|
308
|
+
swipeState: createOperatingSwipeState({ x: 150, y: 0 }),
|
|
309
|
+
displacement: 150,
|
|
310
|
+
isOpening: true,
|
|
311
|
+
}));
|
|
312
|
+
|
|
313
|
+
// End swipe - isOpening becomes false, displacement resets
|
|
314
|
+
// This simulates the multi-render issue where isOpen changes after
|
|
315
|
+
rerender(createDefaultOptions(ref, {
|
|
316
|
+
isOpen: false, // Not yet updated
|
|
317
|
+
swipeState: createIdleSwipeState(),
|
|
318
|
+
displacement: 0, // Reset!
|
|
319
|
+
isOpening: false, // No longer opening
|
|
320
|
+
isClosing: false,
|
|
321
|
+
}));
|
|
322
|
+
|
|
323
|
+
// Transform should NOT have jumped to closed position
|
|
324
|
+
// It should either be the same (preserved) or animation started
|
|
325
|
+
// The key assertion: transform should not reset to closed position immediately
|
|
326
|
+
expect(drawer.style.transform).not.toBe("");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("starts animation from current position when swipe ends with threshold crossed", () => {
|
|
330
|
+
const drawer = createMockDrawer();
|
|
331
|
+
const ref = { current: drawer };
|
|
332
|
+
createMockContent();
|
|
333
|
+
setupAnimationFrameMock();
|
|
334
|
+
|
|
335
|
+
const { result, rerender } = renderHook(
|
|
336
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
337
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Swipe past threshold (50%)
|
|
341
|
+
rerender(createDefaultOptions(ref, {
|
|
342
|
+
isOpen: false,
|
|
343
|
+
swipeState: createOperatingSwipeState({ x: 200, y: 0 }),
|
|
344
|
+
displacement: 200,
|
|
345
|
+
isOpening: true,
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
// End swipe with isOpen changing to true
|
|
349
|
+
act(() => {
|
|
350
|
+
rerender(createDefaultOptions(ref, {
|
|
351
|
+
isOpen: true, // Now open!
|
|
352
|
+
swipeState: createIdleSwipeState(),
|
|
353
|
+
displacement: 0,
|
|
354
|
+
isOpening: false,
|
|
355
|
+
isClosing: false,
|
|
356
|
+
}));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Should be animating to open position
|
|
360
|
+
expect(result.current.phase).toBe("opening");
|
|
361
|
+
expect(result.current.isAnimating).toBe(true);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("starts animation from current position when swipe ends without threshold crossed", () => {
|
|
365
|
+
const drawer = createMockDrawer();
|
|
366
|
+
const ref = { current: drawer };
|
|
367
|
+
createMockContent();
|
|
368
|
+
setupAnimationFrameMock();
|
|
369
|
+
|
|
370
|
+
const { result, rerender } = renderHook(
|
|
371
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
372
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Swipe just a bit (not past threshold)
|
|
376
|
+
rerender(createDefaultOptions(ref, {
|
|
377
|
+
isOpen: false,
|
|
378
|
+
swipeState: createOperatingSwipeState({ x: 50, y: 0 }),
|
|
379
|
+
displacement: 50,
|
|
380
|
+
isOpening: true,
|
|
381
|
+
}));
|
|
382
|
+
|
|
383
|
+
// End swipe - isOpen stays false
|
|
384
|
+
act(() => {
|
|
385
|
+
rerender(createDefaultOptions(ref, {
|
|
386
|
+
isOpen: false,
|
|
387
|
+
swipeState: createIdleSwipeState(),
|
|
388
|
+
displacement: 0,
|
|
389
|
+
isOpening: false,
|
|
390
|
+
isClosing: false,
|
|
391
|
+
}));
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Should be animating back to closed position
|
|
395
|
+
expect(result.current.phase).toBe("closing");
|
|
396
|
+
expect(result.current.isAnimating).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// --------------------------------------------------------------------------
|
|
401
|
+
// Stable State Tests (Critical for "open but shows closed" bug)
|
|
402
|
+
// --------------------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
describe("Stable Open State", () => {
|
|
405
|
+
it("drawer remains visible after opening animation completes", () => {
|
|
406
|
+
const drawer = createMockDrawer();
|
|
407
|
+
const ref = { current: drawer };
|
|
408
|
+
createMockContent();
|
|
409
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
410
|
+
|
|
411
|
+
const { rerender } = renderHook(
|
|
412
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
413
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Trigger open via button (non-swipe)
|
|
417
|
+
act(() => {
|
|
418
|
+
rerender(createDefaultOptions(ref, { isOpen: true }));
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Run animation to completion
|
|
422
|
+
act(() => {
|
|
423
|
+
runAllFrames();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Drawer should remain visible
|
|
427
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
428
|
+
expect(drawer.style.pointerEvents).toBe("auto");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("drawer has correct transform after opening animation completes", () => {
|
|
432
|
+
const drawer = createMockDrawer();
|
|
433
|
+
const ref = { current: drawer };
|
|
434
|
+
createMockContent();
|
|
435
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
436
|
+
|
|
437
|
+
const { rerender } = renderHook(
|
|
438
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
439
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
// Trigger open
|
|
443
|
+
act(() => {
|
|
444
|
+
rerender(createDefaultOptions(ref, { isOpen: true }));
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Run animation to completion
|
|
448
|
+
act(() => {
|
|
449
|
+
runAllFrames();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Drawer should be at open position (translateX(0px) for left placement)
|
|
453
|
+
expect(drawer.style.transform).toContain("0px");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("content has correct transform after opening animation completes", () => {
|
|
457
|
+
const drawer = createMockDrawer();
|
|
458
|
+
const ref = { current: drawer };
|
|
459
|
+
const content = createMockContent();
|
|
460
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
461
|
+
|
|
462
|
+
const { rerender } = renderHook(
|
|
463
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
464
|
+
{
|
|
465
|
+
initialProps: createDefaultOptions(ref, {
|
|
466
|
+
isOpen: false,
|
|
467
|
+
drawerSize: 300,
|
|
468
|
+
}),
|
|
469
|
+
},
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
// Trigger open
|
|
473
|
+
act(() => {
|
|
474
|
+
rerender(createDefaultOptions(ref, { isOpen: true, drawerSize: 300 }));
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Run animation to completion
|
|
478
|
+
act(() => {
|
|
479
|
+
runAllFrames();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Content should be offset by drawer size
|
|
483
|
+
expect(content.style.transform).toContain("300px");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("phase becomes open after animation completes", () => {
|
|
487
|
+
const drawer = createMockDrawer();
|
|
488
|
+
const ref = { current: drawer };
|
|
489
|
+
createMockContent();
|
|
490
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
491
|
+
|
|
492
|
+
const { result, rerender } = renderHook(
|
|
493
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
494
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
// Trigger open
|
|
498
|
+
act(() => {
|
|
499
|
+
rerender(createDefaultOptions(ref, { isOpen: true }));
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Run animation to completion
|
|
503
|
+
act(() => {
|
|
504
|
+
runAllFrames();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
expect(result.current.phase).toBe("open");
|
|
508
|
+
expect(result.current.isAnimating).toBe(false);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe("Stable Closed State", () => {
|
|
513
|
+
it("drawer is hidden after closing animation completes", () => {
|
|
514
|
+
const drawer = createMockDrawer();
|
|
515
|
+
const ref = { current: drawer };
|
|
516
|
+
createMockContent();
|
|
517
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
518
|
+
|
|
519
|
+
const { rerender } = renderHook(
|
|
520
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
521
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
// Trigger close
|
|
525
|
+
act(() => {
|
|
526
|
+
rerender(createDefaultOptions(ref, { isOpen: false }));
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Run animation to completion
|
|
530
|
+
act(() => {
|
|
531
|
+
runAllFrames();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
expect(drawer.style.visibility).toBe("hidden");
|
|
535
|
+
expect(drawer.style.pointerEvents).toBe("none");
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("content transform is cleared after closing animation completes", () => {
|
|
539
|
+
const drawer = createMockDrawer();
|
|
540
|
+
const ref = { current: drawer };
|
|
541
|
+
const content = createMockContent();
|
|
542
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
543
|
+
|
|
544
|
+
const { rerender } = renderHook(
|
|
545
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
546
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// Trigger close
|
|
550
|
+
act(() => {
|
|
551
|
+
rerender(createDefaultOptions(ref, { isOpen: false }));
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Run animation to completion
|
|
555
|
+
act(() => {
|
|
556
|
+
runAllFrames();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
expect(content.style.transform).toBe("");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it("stacking context is cleared after closing animation completes", () => {
|
|
563
|
+
const drawer = createMockDrawer();
|
|
564
|
+
const ref = { current: drawer };
|
|
565
|
+
const content = createMockContent();
|
|
566
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
567
|
+
|
|
568
|
+
const { rerender } = renderHook(
|
|
569
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
570
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
// Trigger close
|
|
574
|
+
act(() => {
|
|
575
|
+
rerender(createDefaultOptions(ref, { isOpen: false }));
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Run animation to completion
|
|
579
|
+
act(() => {
|
|
580
|
+
runAllFrames();
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
expect(content.style.position).toBe("");
|
|
584
|
+
expect(content.style.zIndex).toBe("");
|
|
585
|
+
expect(content.style.background).toBe("");
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("phase becomes closed after closing animation completes", () => {
|
|
589
|
+
const drawer = createMockDrawer();
|
|
590
|
+
const ref = { current: drawer };
|
|
591
|
+
createMockContent();
|
|
592
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
593
|
+
|
|
594
|
+
const { result, rerender } = renderHook(
|
|
595
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
596
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
// Trigger close
|
|
600
|
+
act(() => {
|
|
601
|
+
rerender(createDefaultOptions(ref, { isOpen: false }));
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Run animation to completion
|
|
605
|
+
act(() => {
|
|
606
|
+
runAllFrames();
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
expect(result.current.phase).toBe("closed");
|
|
610
|
+
expect(result.current.isAnimating).toBe(false);
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// --------------------------------------------------------------------------
|
|
615
|
+
// Non-Swipe State Change Tests (Button clicks)
|
|
616
|
+
// --------------------------------------------------------------------------
|
|
617
|
+
|
|
618
|
+
describe("Non-Swipe State Changes (Button)", () => {
|
|
619
|
+
it("animates from closed to open when isOpen changes to true", () => {
|
|
620
|
+
const drawer = createMockDrawer();
|
|
621
|
+
const ref = { current: drawer };
|
|
622
|
+
createMockContent();
|
|
623
|
+
setupAnimationFrameMock();
|
|
624
|
+
|
|
625
|
+
const { result, rerender } = renderHook(
|
|
626
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
627
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// Click open button
|
|
631
|
+
act(() => {
|
|
632
|
+
rerender(createDefaultOptions(ref, { isOpen: true }));
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
expect(result.current.phase).toBe("opening");
|
|
636
|
+
expect(result.current.isAnimating).toBe(true);
|
|
637
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("animates from open to closed when isOpen changes to false", () => {
|
|
641
|
+
const drawer = createMockDrawer();
|
|
642
|
+
const ref = { current: drawer };
|
|
643
|
+
createMockContent();
|
|
644
|
+
setupAnimationFrameMock();
|
|
645
|
+
|
|
646
|
+
const { result, rerender } = renderHook(
|
|
647
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
648
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
// Click close button
|
|
652
|
+
act(() => {
|
|
653
|
+
rerender(createDefaultOptions(ref, { isOpen: false }));
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
expect(result.current.phase).toBe("closing");
|
|
657
|
+
expect(result.current.isAnimating).toBe(true);
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// --------------------------------------------------------------------------
|
|
662
|
+
// Closing via Swipe Tests
|
|
663
|
+
// --------------------------------------------------------------------------
|
|
664
|
+
|
|
665
|
+
describe("Closing via Swipe", () => {
|
|
666
|
+
it("drawer transform updates during close swipe", () => {
|
|
667
|
+
const drawer = createMockDrawer();
|
|
668
|
+
const ref = { current: drawer };
|
|
669
|
+
createMockContent();
|
|
670
|
+
|
|
671
|
+
const { rerender } = renderHook(
|
|
672
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
673
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
// Start closing swipe
|
|
677
|
+
rerender(createDefaultOptions(ref, {
|
|
678
|
+
isOpen: true,
|
|
679
|
+
swipeState: createOperatingSwipeState({ x: -50, y: 0 }),
|
|
680
|
+
displacement: 50, // Displacement is absolute
|
|
681
|
+
isClosing: true,
|
|
682
|
+
}));
|
|
683
|
+
|
|
684
|
+
expect(drawer.style.transform).not.toBe("");
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("animates to closed when close swipe crosses threshold", () => {
|
|
688
|
+
const drawer = createMockDrawer();
|
|
689
|
+
const ref = { current: drawer };
|
|
690
|
+
createMockContent();
|
|
691
|
+
setupAnimationFrameMock();
|
|
692
|
+
|
|
693
|
+
const { result, rerender } = renderHook(
|
|
694
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
695
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
// Swipe past threshold
|
|
699
|
+
rerender(createDefaultOptions(ref, {
|
|
700
|
+
isOpen: true,
|
|
701
|
+
swipeState: createOperatingSwipeState({ x: -150, y: 0 }),
|
|
702
|
+
displacement: 150,
|
|
703
|
+
isClosing: true,
|
|
704
|
+
}));
|
|
705
|
+
|
|
706
|
+
// End swipe with isOpen changing to false
|
|
707
|
+
act(() => {
|
|
708
|
+
rerender(createDefaultOptions(ref, {
|
|
709
|
+
isOpen: false,
|
|
710
|
+
swipeState: createIdleSwipeState(),
|
|
711
|
+
displacement: 0,
|
|
712
|
+
isOpening: false,
|
|
713
|
+
isClosing: false,
|
|
714
|
+
}));
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
expect(result.current.phase).toBe("closing");
|
|
718
|
+
expect(result.current.isAnimating).toBe(true);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("animates back to open when close swipe does not cross threshold", () => {
|
|
722
|
+
const drawer = createMockDrawer();
|
|
723
|
+
const ref = { current: drawer };
|
|
724
|
+
createMockContent();
|
|
725
|
+
setupAnimationFrameMock();
|
|
726
|
+
|
|
727
|
+
const { result, rerender } = renderHook(
|
|
728
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
729
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
// Swipe just a bit
|
|
733
|
+
rerender(createDefaultOptions(ref, {
|
|
734
|
+
isOpen: true,
|
|
735
|
+
swipeState: createOperatingSwipeState({ x: -30, y: 0 }),
|
|
736
|
+
displacement: 30,
|
|
737
|
+
isClosing: true,
|
|
738
|
+
}));
|
|
739
|
+
|
|
740
|
+
// End swipe - isOpen stays true
|
|
741
|
+
act(() => {
|
|
742
|
+
rerender(createDefaultOptions(ref, {
|
|
743
|
+
isOpen: true,
|
|
744
|
+
swipeState: createIdleSwipeState(),
|
|
745
|
+
displacement: 0,
|
|
746
|
+
isOpening: false,
|
|
747
|
+
isClosing: false,
|
|
748
|
+
}));
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
expect(result.current.phase).toBe("opening");
|
|
752
|
+
expect(result.current.isAnimating).toBe(true);
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// --------------------------------------------------------------------------
|
|
757
|
+
// Edge Cases
|
|
758
|
+
// --------------------------------------------------------------------------
|
|
759
|
+
|
|
760
|
+
describe("Edge Cases", () => {
|
|
761
|
+
it("handles disabled state correctly", () => {
|
|
762
|
+
const drawer = createMockDrawer();
|
|
763
|
+
const ref = { current: drawer };
|
|
764
|
+
createMockContent();
|
|
765
|
+
|
|
766
|
+
const { rerender } = renderHook(
|
|
767
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
768
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false, enabled: true }) },
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
// Disable the hook
|
|
772
|
+
rerender(createDefaultOptions(ref, { isOpen: false, enabled: false }));
|
|
773
|
+
|
|
774
|
+
// Transform should be cleared, but visibility stays hidden
|
|
775
|
+
// (disabled drawer should not become visible)
|
|
776
|
+
expect(drawer.style.transform).toBe("");
|
|
777
|
+
expect(drawer.style.visibility).toBe("hidden");
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it("handles null drawer ref gracefully", () => {
|
|
781
|
+
const nullRef: React.RefObject<HTMLElement | null> = { current: null };
|
|
782
|
+
createMockContent();
|
|
783
|
+
|
|
784
|
+
const { result } = renderHook(() =>
|
|
785
|
+
useRevealDrawerTransform(createDefaultOptions(nullRef, { isOpen: false })),
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
// Should not throw and have default state
|
|
789
|
+
expect(result.current.phase).toBe("closed");
|
|
790
|
+
expect(result.current.isAnimating).toBe(false);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("handles content element not found gracefully", () => {
|
|
794
|
+
const drawer = createMockDrawer();
|
|
795
|
+
const ref = { current: drawer };
|
|
796
|
+
// Don't create content element
|
|
797
|
+
|
|
798
|
+
const { result } = renderHook(() =>
|
|
799
|
+
useRevealDrawerTransform(createDefaultOptions(ref, { isOpen: false })),
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
// Should not throw
|
|
803
|
+
expect(result.current.phase).toBe("closed");
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it("handles rapid open/close toggles", () => {
|
|
807
|
+
const drawer = createMockDrawer();
|
|
808
|
+
const ref = { current: drawer };
|
|
809
|
+
createMockContent();
|
|
810
|
+
setupAnimationFrameMock();
|
|
811
|
+
|
|
812
|
+
const { result, rerender } = renderHook(
|
|
813
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
814
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
// Rapid toggles
|
|
818
|
+
act(() => {
|
|
819
|
+
rerender(createDefaultOptions(ref, { isOpen: true }));
|
|
820
|
+
});
|
|
821
|
+
act(() => {
|
|
822
|
+
rerender(createDefaultOptions(ref, { isOpen: false }));
|
|
823
|
+
});
|
|
824
|
+
act(() => {
|
|
825
|
+
rerender(createDefaultOptions(ref, { isOpen: true }));
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// Should handle without error and be in some valid state
|
|
829
|
+
expect(["opening", "closing", "open", "closed"]).toContain(result.current.phase);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it("applies overflow hidden during animation", () => {
|
|
833
|
+
const drawer = createMockDrawer();
|
|
834
|
+
const ref = { current: drawer };
|
|
835
|
+
createMockContent();
|
|
836
|
+
setupAnimationFrameMock();
|
|
837
|
+
|
|
838
|
+
renderHook(
|
|
839
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
840
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
// Body should have overflow hidden when open
|
|
844
|
+
expect(document.body.style.overflow).toBe("hidden");
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// --------------------------------------------------------------------------
|
|
849
|
+
// Abnormal User Behavior Tests (Edge cases for unusual interactions)
|
|
850
|
+
// --------------------------------------------------------------------------
|
|
851
|
+
|
|
852
|
+
describe("Abnormal User Behaviors", () => {
|
|
853
|
+
describe("Opening attempt then give up", () => {
|
|
854
|
+
it("animates back to closed when user starts opening but gives up before threshold", () => {
|
|
855
|
+
const drawer = createMockDrawer();
|
|
856
|
+
const ref = { current: drawer };
|
|
857
|
+
createMockContent();
|
|
858
|
+
setupAnimationFrameMock();
|
|
859
|
+
|
|
860
|
+
const { result, rerender } = renderHook(
|
|
861
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
862
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
// Start opening swipe
|
|
866
|
+
rerender(createDefaultOptions(ref, {
|
|
867
|
+
isOpen: false,
|
|
868
|
+
swipeState: createOperatingSwipeState({ x: 80, y: 0 }),
|
|
869
|
+
displacement: 80, // ~27% of 300, below threshold
|
|
870
|
+
isOpening: true,
|
|
871
|
+
}));
|
|
872
|
+
|
|
873
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
874
|
+
const transformDuringSwipe = drawer.style.transform;
|
|
875
|
+
expect(transformDuringSwipe).not.toBe("");
|
|
876
|
+
|
|
877
|
+
// User gives up - releases without crossing threshold
|
|
878
|
+
act(() => {
|
|
879
|
+
rerender(createDefaultOptions(ref, {
|
|
880
|
+
isOpen: false, // Stays closed
|
|
881
|
+
swipeState: createIdleSwipeState(),
|
|
882
|
+
displacement: 0,
|
|
883
|
+
isOpening: false,
|
|
884
|
+
isClosing: false,
|
|
885
|
+
}));
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// Should animate back to closed
|
|
889
|
+
expect(result.current.phase).toBe("closing");
|
|
890
|
+
expect(result.current.isAnimating).toBe(true);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it("drawer becomes hidden after give-up animation completes", () => {
|
|
894
|
+
const drawer = createMockDrawer();
|
|
895
|
+
const ref = { current: drawer };
|
|
896
|
+
createMockContent();
|
|
897
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
898
|
+
|
|
899
|
+
const { result, rerender } = renderHook(
|
|
900
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
901
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
902
|
+
);
|
|
903
|
+
|
|
904
|
+
// Start then give up
|
|
905
|
+
rerender(createDefaultOptions(ref, {
|
|
906
|
+
isOpen: false,
|
|
907
|
+
swipeState: createOperatingSwipeState({ x: 50, y: 0 }),
|
|
908
|
+
displacement: 50,
|
|
909
|
+
isOpening: true,
|
|
910
|
+
}));
|
|
911
|
+
|
|
912
|
+
act(() => {
|
|
913
|
+
rerender(createDefaultOptions(ref, {
|
|
914
|
+
isOpen: false,
|
|
915
|
+
swipeState: createIdleSwipeState(),
|
|
916
|
+
displacement: 0,
|
|
917
|
+
isOpening: false,
|
|
918
|
+
isClosing: false,
|
|
919
|
+
}));
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// Run animation to completion
|
|
923
|
+
act(() => {
|
|
924
|
+
runAllFrames();
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
expect(result.current.phase).toBe("closed");
|
|
928
|
+
expect(drawer.style.visibility).toBe("hidden");
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
describe("Closing attempt then give up", () => {
|
|
933
|
+
it("animates back to open when user starts closing but gives up before threshold", () => {
|
|
934
|
+
const drawer = createMockDrawer();
|
|
935
|
+
const ref = { current: drawer };
|
|
936
|
+
createMockContent();
|
|
937
|
+
setupAnimationFrameMock();
|
|
938
|
+
|
|
939
|
+
const { result, rerender } = renderHook(
|
|
940
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
941
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
// Start closing swipe
|
|
945
|
+
rerender(createDefaultOptions(ref, {
|
|
946
|
+
isOpen: true,
|
|
947
|
+
swipeState: createOperatingSwipeState({ x: -50, y: 0 }),
|
|
948
|
+
displacement: 50, // Below 30% threshold
|
|
949
|
+
isClosing: true,
|
|
950
|
+
}));
|
|
951
|
+
|
|
952
|
+
// User gives up - releases without crossing threshold
|
|
953
|
+
act(() => {
|
|
954
|
+
rerender(createDefaultOptions(ref, {
|
|
955
|
+
isOpen: true, // Stays open
|
|
956
|
+
swipeState: createIdleSwipeState(),
|
|
957
|
+
displacement: 0,
|
|
958
|
+
isOpening: false,
|
|
959
|
+
isClosing: false,
|
|
960
|
+
}));
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// Should animate back to open
|
|
964
|
+
expect(result.current.phase).toBe("opening");
|
|
965
|
+
expect(result.current.isAnimating).toBe(true);
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it("drawer remains visible after give-up animation completes", () => {
|
|
969
|
+
const drawer = createMockDrawer();
|
|
970
|
+
const ref = { current: drawer };
|
|
971
|
+
createMockContent();
|
|
972
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
973
|
+
|
|
974
|
+
const { result, rerender } = renderHook(
|
|
975
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
976
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
// Start closing then give up
|
|
980
|
+
rerender(createDefaultOptions(ref, {
|
|
981
|
+
isOpen: true,
|
|
982
|
+
swipeState: createOperatingSwipeState({ x: -40, y: 0 }),
|
|
983
|
+
displacement: 40,
|
|
984
|
+
isClosing: true,
|
|
985
|
+
}));
|
|
986
|
+
|
|
987
|
+
act(() => {
|
|
988
|
+
rerender(createDefaultOptions(ref, {
|
|
989
|
+
isOpen: true,
|
|
990
|
+
swipeState: createIdleSwipeState(),
|
|
991
|
+
displacement: 0,
|
|
992
|
+
isOpening: false,
|
|
993
|
+
isClosing: false,
|
|
994
|
+
}));
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// Run animation to completion
|
|
998
|
+
act(() => {
|
|
999
|
+
runAllFrames();
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
expect(result.current.phase).toBe("open");
|
|
1003
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
describe("Incremental small movements (jittery swipe)", () => {
|
|
1008
|
+
it("tracks position correctly during jittery opening swipe", () => {
|
|
1009
|
+
const drawer = createMockDrawer();
|
|
1010
|
+
const ref = { current: drawer };
|
|
1011
|
+
createMockContent();
|
|
1012
|
+
|
|
1013
|
+
const { rerender } = renderHook(
|
|
1014
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1015
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
// Small incremental movements
|
|
1019
|
+
const positions = [10, 15, 12, 20, 18, 25, 30, 28, 35];
|
|
1020
|
+
const transforms: string[] = [];
|
|
1021
|
+
|
|
1022
|
+
for (const pos of positions) {
|
|
1023
|
+
rerender(createDefaultOptions(ref, {
|
|
1024
|
+
isOpen: false,
|
|
1025
|
+
swipeState: createOperatingSwipeState({ x: pos, y: 0 }),
|
|
1026
|
+
displacement: pos,
|
|
1027
|
+
isOpening: true,
|
|
1028
|
+
}));
|
|
1029
|
+
transforms.push(drawer.style.transform);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Each position should have a valid transform
|
|
1033
|
+
for (const transform of transforms) {
|
|
1034
|
+
expect(transform).not.toBe("");
|
|
1035
|
+
expect(transform).toContain("translate");
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it("tracks position correctly during jittery closing swipe then give up", () => {
|
|
1040
|
+
const drawer = createMockDrawer();
|
|
1041
|
+
const ref = { current: drawer };
|
|
1042
|
+
createMockContent();
|
|
1043
|
+
setupAnimationFrameMock();
|
|
1044
|
+
|
|
1045
|
+
const { result, rerender } = renderHook(
|
|
1046
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1047
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
// Jittery closing movements
|
|
1051
|
+
const positions = [10, 15, 12, 20, 18, 25, 20, 15, 10];
|
|
1052
|
+
|
|
1053
|
+
for (const pos of positions) {
|
|
1054
|
+
rerender(createDefaultOptions(ref, {
|
|
1055
|
+
isOpen: true,
|
|
1056
|
+
swipeState: createOperatingSwipeState({ x: -pos, y: 0 }),
|
|
1057
|
+
displacement: pos,
|
|
1058
|
+
isClosing: true,
|
|
1059
|
+
}));
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Give up - didn't cross threshold
|
|
1063
|
+
act(() => {
|
|
1064
|
+
rerender(createDefaultOptions(ref, {
|
|
1065
|
+
isOpen: true,
|
|
1066
|
+
swipeState: createIdleSwipeState(),
|
|
1067
|
+
displacement: 0,
|
|
1068
|
+
isOpening: false,
|
|
1069
|
+
isClosing: false,
|
|
1070
|
+
}));
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
// Should animate back to open
|
|
1074
|
+
expect(result.current.phase).toBe("opening");
|
|
1075
|
+
expect(result.current.isAnimating).toBe(true);
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
describe("Overshoot then reverse", () => {
|
|
1080
|
+
it("handles overshoot opening then reverse to close", () => {
|
|
1081
|
+
const drawer = createMockDrawer();
|
|
1082
|
+
const ref = { current: drawer };
|
|
1083
|
+
createMockContent();
|
|
1084
|
+
setupAnimationFrameMock();
|
|
1085
|
+
|
|
1086
|
+
const { result, rerender } = renderHook(
|
|
1087
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1088
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
// Overshoot past threshold (past 100%)
|
|
1092
|
+
rerender(createDefaultOptions(ref, {
|
|
1093
|
+
isOpen: false,
|
|
1094
|
+
swipeState: createOperatingSwipeState({ x: 350, y: 0 }),
|
|
1095
|
+
displacement: 350, // More than drawer size (300)
|
|
1096
|
+
isOpening: true,
|
|
1097
|
+
}));
|
|
1098
|
+
|
|
1099
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
1100
|
+
|
|
1101
|
+
// Pull back below threshold
|
|
1102
|
+
rerender(createDefaultOptions(ref, {
|
|
1103
|
+
isOpen: false,
|
|
1104
|
+
swipeState: createOperatingSwipeState({ x: 50, y: 0 }),
|
|
1105
|
+
displacement: 50, // Back below threshold
|
|
1106
|
+
isOpening: true,
|
|
1107
|
+
}));
|
|
1108
|
+
|
|
1109
|
+
// Release - should close because below threshold
|
|
1110
|
+
act(() => {
|
|
1111
|
+
rerender(createDefaultOptions(ref, {
|
|
1112
|
+
isOpen: false, // Didn't cross threshold on release
|
|
1113
|
+
swipeState: createIdleSwipeState(),
|
|
1114
|
+
displacement: 0,
|
|
1115
|
+
isOpening: false,
|
|
1116
|
+
isClosing: false,
|
|
1117
|
+
}));
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
// Should animate to closed
|
|
1121
|
+
expect(result.current.phase).toBe("closing");
|
|
1122
|
+
expect(result.current.isAnimating).toBe(true);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it("handles overshoot closing then reverse to stay open", () => {
|
|
1126
|
+
const drawer = createMockDrawer();
|
|
1127
|
+
const ref = { current: drawer };
|
|
1128
|
+
createMockContent();
|
|
1129
|
+
setupAnimationFrameMock();
|
|
1130
|
+
|
|
1131
|
+
const { result, rerender } = renderHook(
|
|
1132
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1133
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
// Overshoot past close threshold
|
|
1137
|
+
rerender(createDefaultOptions(ref, {
|
|
1138
|
+
isOpen: true,
|
|
1139
|
+
swipeState: createOperatingSwipeState({ x: -200, y: 0 }),
|
|
1140
|
+
displacement: 200, // Well past 30% threshold
|
|
1141
|
+
isClosing: true,
|
|
1142
|
+
}));
|
|
1143
|
+
|
|
1144
|
+
// Pull back below threshold
|
|
1145
|
+
rerender(createDefaultOptions(ref, {
|
|
1146
|
+
isOpen: true,
|
|
1147
|
+
swipeState: createOperatingSwipeState({ x: -30, y: 0 }),
|
|
1148
|
+
displacement: 30, // Back below threshold
|
|
1149
|
+
isClosing: true,
|
|
1150
|
+
}));
|
|
1151
|
+
|
|
1152
|
+
// Release - should stay open because below threshold
|
|
1153
|
+
act(() => {
|
|
1154
|
+
rerender(createDefaultOptions(ref, {
|
|
1155
|
+
isOpen: true, // Stays open
|
|
1156
|
+
swipeState: createIdleSwipeState(),
|
|
1157
|
+
displacement: 0,
|
|
1158
|
+
isOpening: false,
|
|
1159
|
+
isClosing: false,
|
|
1160
|
+
}));
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
// Should animate back to open
|
|
1164
|
+
expect(result.current.phase).toBe("opening");
|
|
1165
|
+
expect(result.current.isAnimating).toBe(true);
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it("preserves transform continuity during overshoot and reverse", () => {
|
|
1169
|
+
const drawer = createMockDrawer();
|
|
1170
|
+
const ref = { current: drawer };
|
|
1171
|
+
createMockContent();
|
|
1172
|
+
|
|
1173
|
+
const { rerender } = renderHook(
|
|
1174
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1175
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
// Track transforms during the sequence
|
|
1179
|
+
const sequence = [
|
|
1180
|
+
{ displacement: 100, expected: "moving forward" },
|
|
1181
|
+
{ displacement: 200, expected: "more forward" },
|
|
1182
|
+
{ displacement: 300, expected: "at full open" },
|
|
1183
|
+
{ displacement: 350, expected: "overshoot" },
|
|
1184
|
+
{ displacement: 300, expected: "pulling back" },
|
|
1185
|
+
{ displacement: 200, expected: "more back" },
|
|
1186
|
+
{ displacement: 100, expected: "back to 1/3" },
|
|
1187
|
+
{ displacement: 50, expected: "below threshold" },
|
|
1188
|
+
];
|
|
1189
|
+
|
|
1190
|
+
const transforms: string[] = [];
|
|
1191
|
+
|
|
1192
|
+
for (const step of sequence) {
|
|
1193
|
+
rerender(createDefaultOptions(ref, {
|
|
1194
|
+
isOpen: false,
|
|
1195
|
+
swipeState: createOperatingSwipeState({ x: step.displacement, y: 0 }),
|
|
1196
|
+
displacement: step.displacement,
|
|
1197
|
+
isOpening: true,
|
|
1198
|
+
}));
|
|
1199
|
+
transforms.push(drawer.style.transform);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// All transforms should be valid (no empty strings or undefined)
|
|
1203
|
+
for (const transform of transforms) {
|
|
1204
|
+
expect(transform).not.toBe("");
|
|
1205
|
+
expect(transform).toContain("translateX");
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Transforms should actually change with displacement
|
|
1209
|
+
const uniqueTransforms = new Set(transforms);
|
|
1210
|
+
expect(uniqueTransforms.size).toBeGreaterThan(1);
|
|
1211
|
+
});
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
describe("Interrupted animations", () => {
|
|
1215
|
+
it("handles swipe interrupting an opening animation", () => {
|
|
1216
|
+
const drawer = createMockDrawer();
|
|
1217
|
+
const ref = { current: drawer };
|
|
1218
|
+
createMockContent();
|
|
1219
|
+
setupAnimationFrameMock();
|
|
1220
|
+
|
|
1221
|
+
const { result, rerender } = renderHook(
|
|
1222
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1223
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
// Start opening via button
|
|
1227
|
+
act(() => {
|
|
1228
|
+
rerender(createDefaultOptions(ref, { isOpen: true }));
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
expect(result.current.isAnimating).toBe(true);
|
|
1232
|
+
|
|
1233
|
+
// User starts swiping during animation
|
|
1234
|
+
rerender(createDefaultOptions(ref, {
|
|
1235
|
+
isOpen: true,
|
|
1236
|
+
swipeState: createOperatingSwipeState({ x: -50, y: 0 }),
|
|
1237
|
+
displacement: 50,
|
|
1238
|
+
isClosing: true,
|
|
1239
|
+
}));
|
|
1240
|
+
|
|
1241
|
+
// Animation should be cancelled during operation
|
|
1242
|
+
expect(result.current.isAnimating).toBe(false);
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
it("handles swipe interrupting a closing animation", () => {
|
|
1246
|
+
const drawer = createMockDrawer();
|
|
1247
|
+
const ref = { current: drawer };
|
|
1248
|
+
createMockContent();
|
|
1249
|
+
setupAnimationFrameMock();
|
|
1250
|
+
|
|
1251
|
+
const { result, rerender } = renderHook(
|
|
1252
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1253
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true }) },
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
// Start closing via button
|
|
1257
|
+
act(() => {
|
|
1258
|
+
rerender(createDefaultOptions(ref, { isOpen: false }));
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
expect(result.current.isAnimating).toBe(true);
|
|
1262
|
+
|
|
1263
|
+
// User starts swiping during animation (trying to reopen)
|
|
1264
|
+
rerender(createDefaultOptions(ref, {
|
|
1265
|
+
isOpen: false,
|
|
1266
|
+
swipeState: createOperatingSwipeState({ x: 100, y: 0 }),
|
|
1267
|
+
displacement: 100,
|
|
1268
|
+
isOpening: true,
|
|
1269
|
+
}));
|
|
1270
|
+
|
|
1271
|
+
// Animation should be cancelled during operation
|
|
1272
|
+
expect(result.current.isAnimating).toBe(false);
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
describe("Multiple direction changes during single gesture", () => {
|
|
1277
|
+
it("handles back-and-forth movement during opening", () => {
|
|
1278
|
+
const drawer = createMockDrawer();
|
|
1279
|
+
const ref = { current: drawer };
|
|
1280
|
+
createMockContent();
|
|
1281
|
+
|
|
1282
|
+
const { rerender } = renderHook(
|
|
1283
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1284
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
1285
|
+
);
|
|
1286
|
+
|
|
1287
|
+
// Simulate back-and-forth user movement
|
|
1288
|
+
const movements = [50, 100, 80, 150, 120, 200, 180, 250, 200, 280];
|
|
1289
|
+
|
|
1290
|
+
for (const displacement of movements) {
|
|
1291
|
+
rerender(createDefaultOptions(ref, {
|
|
1292
|
+
isOpen: false,
|
|
1293
|
+
swipeState: createOperatingSwipeState({ x: displacement, y: 0 }),
|
|
1294
|
+
displacement,
|
|
1295
|
+
isOpening: true,
|
|
1296
|
+
}));
|
|
1297
|
+
|
|
1298
|
+
// Should always have valid transform
|
|
1299
|
+
expect(drawer.style.transform).not.toBe("");
|
|
1300
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
describe("Edge: Very small movements", () => {
|
|
1306
|
+
it("handles micro-movements without breaking", () => {
|
|
1307
|
+
const drawer = createMockDrawer();
|
|
1308
|
+
const ref = { current: drawer };
|
|
1309
|
+
createMockContent();
|
|
1310
|
+
setupAnimationFrameMock();
|
|
1311
|
+
|
|
1312
|
+
const { result, rerender } = renderHook(
|
|
1313
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1314
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false }) },
|
|
1315
|
+
);
|
|
1316
|
+
|
|
1317
|
+
// Very small movements (1-5 pixels)
|
|
1318
|
+
for (let i = 1; i <= 5; i++) {
|
|
1319
|
+
rerender(createDefaultOptions(ref, {
|
|
1320
|
+
isOpen: false,
|
|
1321
|
+
swipeState: createOperatingSwipeState({ x: i, y: 0 }),
|
|
1322
|
+
displacement: i,
|
|
1323
|
+
isOpening: true,
|
|
1324
|
+
}));
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Release with tiny displacement
|
|
1328
|
+
act(() => {
|
|
1329
|
+
rerender(createDefaultOptions(ref, {
|
|
1330
|
+
isOpen: false,
|
|
1331
|
+
swipeState: createIdleSwipeState(),
|
|
1332
|
+
displacement: 0,
|
|
1333
|
+
isOpening: false,
|
|
1334
|
+
isClosing: false,
|
|
1335
|
+
}));
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
// Should handle gracefully
|
|
1339
|
+
expect(["closing", "closed"]).toContain(result.current.phase);
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
describe("Edge: Maximum displacement (beyond drawer size)", () => {
|
|
1344
|
+
it("handles displacement larger than drawer size", () => {
|
|
1345
|
+
const drawer = createMockDrawer();
|
|
1346
|
+
const ref = { current: drawer };
|
|
1347
|
+
createMockContent();
|
|
1348
|
+
setupAnimationFrameMock();
|
|
1349
|
+
|
|
1350
|
+
const { result, rerender } = renderHook(
|
|
1351
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1352
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false, drawerSize: 300 }) },
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1355
|
+
// Displacement way beyond drawer size
|
|
1356
|
+
rerender(createDefaultOptions(ref, {
|
|
1357
|
+
isOpen: false,
|
|
1358
|
+
drawerSize: 300,
|
|
1359
|
+
swipeState: createOperatingSwipeState({ x: 500, y: 0 }),
|
|
1360
|
+
displacement: 500, // 167% of drawer size
|
|
1361
|
+
isOpening: true,
|
|
1362
|
+
}));
|
|
1363
|
+
|
|
1364
|
+
// Should handle without error
|
|
1365
|
+
expect(drawer.style.transform).not.toBe("");
|
|
1366
|
+
|
|
1367
|
+
// Release
|
|
1368
|
+
act(() => {
|
|
1369
|
+
rerender(createDefaultOptions(ref, {
|
|
1370
|
+
isOpen: true, // Threshold crossed
|
|
1371
|
+
drawerSize: 300,
|
|
1372
|
+
swipeState: createIdleSwipeState(),
|
|
1373
|
+
displacement: 0,
|
|
1374
|
+
isOpening: false,
|
|
1375
|
+
isClosing: false,
|
|
1376
|
+
}));
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
// When overshoot > 100%, we're already at target, so may snap directly to "open"
|
|
1380
|
+
// or animate if there's a small distance
|
|
1381
|
+
expect(["opening", "open"]).toContain(result.current.phase);
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
// =========================================================================
|
|
1386
|
+
// Overshoot Issues (Critical bugs to fix)
|
|
1387
|
+
// =========================================================================
|
|
1388
|
+
|
|
1389
|
+
describe("Overshoot Issues", () => {
|
|
1390
|
+
describe("Opening with overshoot should stay open", () => {
|
|
1391
|
+
it("drawer stays open when released at overshoot position", () => {
|
|
1392
|
+
const drawer = createMockDrawer();
|
|
1393
|
+
const ref = { current: drawer };
|
|
1394
|
+
createMockContent();
|
|
1395
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
1396
|
+
|
|
1397
|
+
const { result, rerender } = renderHook(
|
|
1398
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1399
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false, drawerSize: 300 }) },
|
|
1400
|
+
);
|
|
1401
|
+
|
|
1402
|
+
// Overshoot past 100% - swipe to 400px when drawer is 300px
|
|
1403
|
+
rerender(createDefaultOptions(ref, {
|
|
1404
|
+
isOpen: false,
|
|
1405
|
+
drawerSize: 300,
|
|
1406
|
+
swipeState: createOperatingSwipeState({ x: 400, y: 0 }),
|
|
1407
|
+
displacement: 400,
|
|
1408
|
+
isOpening: true,
|
|
1409
|
+
}));
|
|
1410
|
+
|
|
1411
|
+
// Drawer should be fully visible (progress clamped to 1)
|
|
1412
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
1413
|
+
|
|
1414
|
+
// Release at overshoot position - isOpen should become true
|
|
1415
|
+
act(() => {
|
|
1416
|
+
rerender(createDefaultOptions(ref, {
|
|
1417
|
+
isOpen: true, // Threshold definitely crossed
|
|
1418
|
+
drawerSize: 300,
|
|
1419
|
+
swipeState: createIdleSwipeState(),
|
|
1420
|
+
displacement: 0,
|
|
1421
|
+
isOpening: false,
|
|
1422
|
+
isClosing: false,
|
|
1423
|
+
}));
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
// When at overshoot position, we're already at target, so may snap to "open" directly
|
|
1427
|
+
// The key assertion: drawer should NOT be closing
|
|
1428
|
+
expect(["opening", "open"]).toContain(result.current.phase);
|
|
1429
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
1430
|
+
|
|
1431
|
+
// Run animation to completion (if any)
|
|
1432
|
+
act(() => {
|
|
1433
|
+
runAllFrames();
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
// Final state should be open
|
|
1437
|
+
expect(result.current.phase).toBe("open");
|
|
1438
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
it("drawer transform is at open position after overshoot release", () => {
|
|
1442
|
+
const drawer = createMockDrawer();
|
|
1443
|
+
const ref = { current: drawer };
|
|
1444
|
+
createMockContent();
|
|
1445
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
1446
|
+
|
|
1447
|
+
const { rerender } = renderHook(
|
|
1448
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1449
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false, drawerSize: 300 }) },
|
|
1450
|
+
);
|
|
1451
|
+
|
|
1452
|
+
// Overshoot
|
|
1453
|
+
rerender(createDefaultOptions(ref, {
|
|
1454
|
+
isOpen: false,
|
|
1455
|
+
drawerSize: 300,
|
|
1456
|
+
swipeState: createOperatingSwipeState({ x: 350, y: 0 }),
|
|
1457
|
+
displacement: 350,
|
|
1458
|
+
isOpening: true,
|
|
1459
|
+
}));
|
|
1460
|
+
|
|
1461
|
+
// Release
|
|
1462
|
+
act(() => {
|
|
1463
|
+
rerender(createDefaultOptions(ref, {
|
|
1464
|
+
isOpen: true,
|
|
1465
|
+
drawerSize: 300,
|
|
1466
|
+
swipeState: createIdleSwipeState(),
|
|
1467
|
+
displacement: 0,
|
|
1468
|
+
isOpening: false,
|
|
1469
|
+
isClosing: false,
|
|
1470
|
+
}));
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
// Run to completion
|
|
1474
|
+
act(() => {
|
|
1475
|
+
runAllFrames();
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
// Drawer should be at 0px (fully open) for left placement
|
|
1479
|
+
expect(drawer.style.transform).toBe("translateX(0px)");
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
it("content is offset by drawer size after overshoot open", () => {
|
|
1483
|
+
const drawer = createMockDrawer();
|
|
1484
|
+
const ref = { current: drawer };
|
|
1485
|
+
const content = createMockContent();
|
|
1486
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
1487
|
+
|
|
1488
|
+
const { rerender } = renderHook(
|
|
1489
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1490
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false, drawerSize: 300 }) },
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
// Overshoot and release
|
|
1494
|
+
rerender(createDefaultOptions(ref, {
|
|
1495
|
+
isOpen: false,
|
|
1496
|
+
drawerSize: 300,
|
|
1497
|
+
swipeState: createOperatingSwipeState({ x: 400, y: 0 }),
|
|
1498
|
+
displacement: 400,
|
|
1499
|
+
isOpening: true,
|
|
1500
|
+
}));
|
|
1501
|
+
|
|
1502
|
+
act(() => {
|
|
1503
|
+
rerender(createDefaultOptions(ref, {
|
|
1504
|
+
isOpen: true,
|
|
1505
|
+
drawerSize: 300,
|
|
1506
|
+
swipeState: createIdleSwipeState(),
|
|
1507
|
+
displacement: 0,
|
|
1508
|
+
isOpening: false,
|
|
1509
|
+
isClosing: false,
|
|
1510
|
+
}));
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
act(() => {
|
|
1514
|
+
runAllFrames();
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
// Content should be offset by 300px
|
|
1518
|
+
expect(content.style.transform).toBe("translateX(300px)");
|
|
1519
|
+
});
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
describe("Closing with overshoot should not cause content jump", () => {
|
|
1523
|
+
it("content position is correct during and after overshoot close", () => {
|
|
1524
|
+
const drawer = createMockDrawer();
|
|
1525
|
+
const ref = { current: drawer };
|
|
1526
|
+
const content = createMockContent();
|
|
1527
|
+
setupAnimationFrameMock();
|
|
1528
|
+
|
|
1529
|
+
const { result, rerender } = renderHook(
|
|
1530
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1531
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true, drawerSize: 300 }) },
|
|
1532
|
+
);
|
|
1533
|
+
|
|
1534
|
+
// Overshoot close - swipe past the drawer size
|
|
1535
|
+
rerender(createDefaultOptions(ref, {
|
|
1536
|
+
isOpen: true,
|
|
1537
|
+
drawerSize: 300,
|
|
1538
|
+
swipeState: createOperatingSwipeState({ x: -350, y: 0 }),
|
|
1539
|
+
displacement: 350, // Overshoot past drawer size
|
|
1540
|
+
isClosing: true,
|
|
1541
|
+
}));
|
|
1542
|
+
|
|
1543
|
+
// Content should be at 0px (closed position) due to progress clamping
|
|
1544
|
+
// Progress = 1 - 350/300 = clamped to 0
|
|
1545
|
+
const duringOvershootTransform = content.style.transform;
|
|
1546
|
+
expect(duringOvershootTransform).toBe("translateX(0px)");
|
|
1547
|
+
|
|
1548
|
+
// Release at overshoot
|
|
1549
|
+
act(() => {
|
|
1550
|
+
rerender(createDefaultOptions(ref, {
|
|
1551
|
+
isOpen: false, // Threshold crossed
|
|
1552
|
+
drawerSize: 300,
|
|
1553
|
+
swipeState: createIdleSwipeState(),
|
|
1554
|
+
displacement: 0,
|
|
1555
|
+
isOpening: false,
|
|
1556
|
+
isClosing: false,
|
|
1557
|
+
}));
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
// Since we're already at target (0px), should go directly to closed
|
|
1561
|
+
// No animation needed - distance is 0
|
|
1562
|
+
expect(result.current.phase).toBe("closed");
|
|
1563
|
+
|
|
1564
|
+
// Content transform should be cleared (we were already at target)
|
|
1565
|
+
expect(content.style.transform).toBe("");
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
it("content does not jump when closing with partial overshoot", () => {
|
|
1569
|
+
const drawer = createMockDrawer();
|
|
1570
|
+
const ref = { current: drawer };
|
|
1571
|
+
const content = createMockContent();
|
|
1572
|
+
const { runFrame } = setupAnimationFrameMock();
|
|
1573
|
+
|
|
1574
|
+
const { result, rerender } = renderHook(
|
|
1575
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1576
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true, drawerSize: 300 }) },
|
|
1577
|
+
);
|
|
1578
|
+
|
|
1579
|
+
// Close swipe not quite at overshoot - should still animate
|
|
1580
|
+
rerender(createDefaultOptions(ref, {
|
|
1581
|
+
isOpen: true,
|
|
1582
|
+
drawerSize: 300,
|
|
1583
|
+
swipeState: createOperatingSwipeState({ x: -250, y: 0 }),
|
|
1584
|
+
displacement: 250,
|
|
1585
|
+
isClosing: true,
|
|
1586
|
+
}));
|
|
1587
|
+
|
|
1588
|
+
// Progress = 1 - 250/300 ≈ 0.167, content at ~50px
|
|
1589
|
+
const duringSwipeTransform = content.style.transform;
|
|
1590
|
+
// Use regex to match approximately 50px (floating point precision)
|
|
1591
|
+
expect(duringSwipeTransform).toMatch(/translateX\(49\.9+|translateX\(50/);
|
|
1592
|
+
|
|
1593
|
+
// Release
|
|
1594
|
+
act(() => {
|
|
1595
|
+
rerender(createDefaultOptions(ref, {
|
|
1596
|
+
isOpen: false,
|
|
1597
|
+
drawerSize: 300,
|
|
1598
|
+
swipeState: createIdleSwipeState(),
|
|
1599
|
+
displacement: 0,
|
|
1600
|
+
isOpening: false,
|
|
1601
|
+
isClosing: false,
|
|
1602
|
+
}));
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
// Should be animating (distance = 50 > 1)
|
|
1606
|
+
expect(result.current.phase).toBe("closing");
|
|
1607
|
+
expect(result.current.isAnimating).toBe(true);
|
|
1608
|
+
|
|
1609
|
+
// First animation frame - content should be close to 50px, not jumping
|
|
1610
|
+
act(() => {
|
|
1611
|
+
runFrame();
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
// Content should be animating from ~50px toward 0px
|
|
1615
|
+
// At very start of animation, should be close to starting position
|
|
1616
|
+
const afterFirstFrame = content.style.transform;
|
|
1617
|
+
expect(afterFirstFrame).toContain("px");
|
|
1618
|
+
// Should not have jumped to some random position
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
it("drawer hides correctly after overshoot close completes", () => {
|
|
1622
|
+
const drawer = createMockDrawer();
|
|
1623
|
+
const ref = { current: drawer };
|
|
1624
|
+
createMockContent();
|
|
1625
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
1626
|
+
|
|
1627
|
+
const { result, rerender } = renderHook(
|
|
1628
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1629
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true, drawerSize: 300 }) },
|
|
1630
|
+
);
|
|
1631
|
+
|
|
1632
|
+
// Overshoot close
|
|
1633
|
+
rerender(createDefaultOptions(ref, {
|
|
1634
|
+
isOpen: true,
|
|
1635
|
+
drawerSize: 300,
|
|
1636
|
+
swipeState: createOperatingSwipeState({ x: -400, y: 0 }),
|
|
1637
|
+
displacement: 400,
|
|
1638
|
+
isClosing: true,
|
|
1639
|
+
}));
|
|
1640
|
+
|
|
1641
|
+
// Release
|
|
1642
|
+
act(() => {
|
|
1643
|
+
rerender(createDefaultOptions(ref, {
|
|
1644
|
+
isOpen: false,
|
|
1645
|
+
drawerSize: 300,
|
|
1646
|
+
swipeState: createIdleSwipeState(),
|
|
1647
|
+
displacement: 0,
|
|
1648
|
+
isOpening: false,
|
|
1649
|
+
isClosing: false,
|
|
1650
|
+
}));
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
// Run animation
|
|
1654
|
+
act(() => {
|
|
1655
|
+
runAllFrames();
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
// Drawer should be hidden
|
|
1659
|
+
expect(result.current.phase).toBe("closed");
|
|
1660
|
+
expect(drawer.style.visibility).toBe("hidden");
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
it("content transform is cleared after overshoot close completes", () => {
|
|
1664
|
+
const drawer = createMockDrawer();
|
|
1665
|
+
const ref = { current: drawer };
|
|
1666
|
+
const content = createMockContent();
|
|
1667
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
1668
|
+
|
|
1669
|
+
const { rerender } = renderHook(
|
|
1670
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1671
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true, drawerSize: 300 }) },
|
|
1672
|
+
);
|
|
1673
|
+
|
|
1674
|
+
// Overshoot and release
|
|
1675
|
+
rerender(createDefaultOptions(ref, {
|
|
1676
|
+
isOpen: true,
|
|
1677
|
+
drawerSize: 300,
|
|
1678
|
+
swipeState: createOperatingSwipeState({ x: -350, y: 0 }),
|
|
1679
|
+
displacement: 350,
|
|
1680
|
+
isClosing: true,
|
|
1681
|
+
}));
|
|
1682
|
+
|
|
1683
|
+
act(() => {
|
|
1684
|
+
rerender(createDefaultOptions(ref, {
|
|
1685
|
+
isOpen: false,
|
|
1686
|
+
drawerSize: 300,
|
|
1687
|
+
swipeState: createIdleSwipeState(),
|
|
1688
|
+
displacement: 0,
|
|
1689
|
+
isOpening: false,
|
|
1690
|
+
isClosing: false,
|
|
1691
|
+
}));
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
act(() => {
|
|
1695
|
+
runAllFrames();
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
// Content transform should be cleared (back to normal)
|
|
1699
|
+
expect(content.style.transform).toBe("");
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
it("no CSS transition on content during close animation", () => {
|
|
1703
|
+
const drawer = createMockDrawer();
|
|
1704
|
+
const ref = { current: drawer };
|
|
1705
|
+
const content = createMockContent();
|
|
1706
|
+
setupAnimationFrameMock();
|
|
1707
|
+
|
|
1708
|
+
const { rerender } = renderHook(
|
|
1709
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1710
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: true, drawerSize: 300 }) },
|
|
1711
|
+
);
|
|
1712
|
+
|
|
1713
|
+
// Close swipe
|
|
1714
|
+
rerender(createDefaultOptions(ref, {
|
|
1715
|
+
isOpen: true,
|
|
1716
|
+
drawerSize: 300,
|
|
1717
|
+
swipeState: createOperatingSwipeState({ x: -200, y: 0 }),
|
|
1718
|
+
displacement: 200,
|
|
1719
|
+
isClosing: true,
|
|
1720
|
+
}));
|
|
1721
|
+
|
|
1722
|
+
// During swipe - no transition
|
|
1723
|
+
expect(content.style.transition).toBe("none");
|
|
1724
|
+
|
|
1725
|
+
// Release
|
|
1726
|
+
act(() => {
|
|
1727
|
+
rerender(createDefaultOptions(ref, {
|
|
1728
|
+
isOpen: false,
|
|
1729
|
+
drawerSize: 300,
|
|
1730
|
+
swipeState: createIdleSwipeState(),
|
|
1731
|
+
displacement: 0,
|
|
1732
|
+
isOpening: false,
|
|
1733
|
+
isClosing: false,
|
|
1734
|
+
}));
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
// During animation - still no CSS transition (using RAF)
|
|
1738
|
+
expect(content.style.transition).toBe("none");
|
|
1739
|
+
});
|
|
1740
|
+
});
|
|
1741
|
+
});
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
// --------------------------------------------------------------------------
|
|
1745
|
+
// Placement Tests
|
|
1746
|
+
// --------------------------------------------------------------------------
|
|
1747
|
+
|
|
1748
|
+
describe("Placement Variations", () => {
|
|
1749
|
+
it("uses translateX for left placement", () => {
|
|
1750
|
+
const drawer = createMockDrawer();
|
|
1751
|
+
const ref = { current: drawer };
|
|
1752
|
+
createMockContent();
|
|
1753
|
+
|
|
1754
|
+
const { rerender } = renderHook(
|
|
1755
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1756
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false, placement: "left" }) },
|
|
1757
|
+
);
|
|
1758
|
+
|
|
1759
|
+
rerender(createDefaultOptions(ref, {
|
|
1760
|
+
isOpen: false,
|
|
1761
|
+
placement: "left",
|
|
1762
|
+
swipeState: createOperatingSwipeState({ x: 100, y: 0 }),
|
|
1763
|
+
displacement: 100,
|
|
1764
|
+
isOpening: true,
|
|
1765
|
+
}));
|
|
1766
|
+
|
|
1767
|
+
expect(drawer.style.transform).toContain("translateX");
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
it("uses translateX for right placement", () => {
|
|
1771
|
+
const drawer = createMockDrawer();
|
|
1772
|
+
const ref = { current: drawer };
|
|
1773
|
+
createMockContent();
|
|
1774
|
+
|
|
1775
|
+
const { rerender } = renderHook(
|
|
1776
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1777
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false, placement: "right" }) },
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1780
|
+
rerender(createDefaultOptions(ref, {
|
|
1781
|
+
isOpen: false,
|
|
1782
|
+
placement: "right",
|
|
1783
|
+
swipeState: createOperatingSwipeState({ x: -100, y: 0 }),
|
|
1784
|
+
displacement: 100,
|
|
1785
|
+
isOpening: true,
|
|
1786
|
+
}));
|
|
1787
|
+
|
|
1788
|
+
expect(drawer.style.transform).toContain("translateX");
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
it("uses translateY for top placement", () => {
|
|
1792
|
+
const drawer = createMockDrawer();
|
|
1793
|
+
const ref = { current: drawer };
|
|
1794
|
+
createMockContent();
|
|
1795
|
+
|
|
1796
|
+
const { rerender } = renderHook(
|
|
1797
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1798
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false, placement: "top" }) },
|
|
1799
|
+
);
|
|
1800
|
+
|
|
1801
|
+
rerender(createDefaultOptions(ref, {
|
|
1802
|
+
isOpen: false,
|
|
1803
|
+
placement: "top",
|
|
1804
|
+
swipeState: createOperatingSwipeState({ x: 0, y: 100 }),
|
|
1805
|
+
displacement: 100,
|
|
1806
|
+
isOpening: true,
|
|
1807
|
+
}));
|
|
1808
|
+
|
|
1809
|
+
expect(drawer.style.transform).toContain("translateY");
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
it("uses translateY for bottom placement", () => {
|
|
1813
|
+
const drawer = createMockDrawer();
|
|
1814
|
+
const ref = { current: drawer };
|
|
1815
|
+
createMockContent();
|
|
1816
|
+
|
|
1817
|
+
const { rerender } = renderHook(
|
|
1818
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1819
|
+
{ initialProps: createDefaultOptions(ref, { isOpen: false, placement: "bottom" }) },
|
|
1820
|
+
);
|
|
1821
|
+
|
|
1822
|
+
rerender(createDefaultOptions(ref, {
|
|
1823
|
+
isOpen: false,
|
|
1824
|
+
placement: "bottom",
|
|
1825
|
+
swipeState: createOperatingSwipeState({ x: 0, y: -100 }),
|
|
1826
|
+
displacement: 100,
|
|
1827
|
+
isOpening: true,
|
|
1828
|
+
}));
|
|
1829
|
+
|
|
1830
|
+
expect(drawer.style.transform).toContain("translateY");
|
|
1831
|
+
});
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
// --------------------------------------------------------------------------
|
|
1835
|
+
// React Strict Mode Compatibility Tests
|
|
1836
|
+
// --------------------------------------------------------------------------
|
|
1837
|
+
|
|
1838
|
+
describe("React StrictMode Compatibility", () => {
|
|
1839
|
+
/**
|
|
1840
|
+
* CRITICAL: These tests verify the hook works correctly in StrictMode.
|
|
1841
|
+
*
|
|
1842
|
+
* In StrictMode, React calls the render function twice and mounts/unmounts
|
|
1843
|
+
* effects to catch bugs. The drawer hook must handle this correctly:
|
|
1844
|
+
* - Drawer visibility must be set on initial mount
|
|
1845
|
+
* - Cleanup must not break subsequent mounts
|
|
1846
|
+
*/
|
|
1847
|
+
const StrictModeWrapper = ({ children }: { children: React.ReactNode }): React.ReactNode => {
|
|
1848
|
+
return React.createElement(React.StrictMode, null, children);
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1851
|
+
it("drawer is hidden on initial render when closed (Strict Mode)", () => {
|
|
1852
|
+
const drawer = createMockDrawer();
|
|
1853
|
+
const ref = { current: drawer };
|
|
1854
|
+
createMockContent();
|
|
1855
|
+
|
|
1856
|
+
renderHook(
|
|
1857
|
+
() => useRevealDrawerTransform(createDefaultOptions(ref, { isOpen: false })),
|
|
1858
|
+
{ wrapper: StrictModeWrapper },
|
|
1859
|
+
);
|
|
1860
|
+
|
|
1861
|
+
// After StrictMode's double-mount, drawer should still be hidden
|
|
1862
|
+
expect(drawer.style.visibility).toBe("hidden");
|
|
1863
|
+
expect(drawer.style.pointerEvents).toBe("none");
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
it("drawer is visible on initial render when open (Strict Mode)", () => {
|
|
1867
|
+
const drawer = createMockDrawer();
|
|
1868
|
+
const ref = { current: drawer };
|
|
1869
|
+
createMockContent();
|
|
1870
|
+
|
|
1871
|
+
renderHook(
|
|
1872
|
+
() => useRevealDrawerTransform(createDefaultOptions(ref, { isOpen: true })),
|
|
1873
|
+
{ wrapper: StrictModeWrapper },
|
|
1874
|
+
);
|
|
1875
|
+
|
|
1876
|
+
// After StrictMode's double-mount, drawer should still be visible
|
|
1877
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
1878
|
+
expect(drawer.style.pointerEvents).toBe("auto");
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
it("drawer visibility survives StrictMode double-mount cycle", () => {
|
|
1882
|
+
const drawer = createMockDrawer();
|
|
1883
|
+
const ref = { current: drawer };
|
|
1884
|
+
createMockContent();
|
|
1885
|
+
const { runAllFrames } = setupAnimationFrameMock();
|
|
1886
|
+
|
|
1887
|
+
// Start closed
|
|
1888
|
+
const { rerender } = renderHook(
|
|
1889
|
+
(props: UseRevealDrawerTransformOptions) => useRevealDrawerTransform(props),
|
|
1890
|
+
{
|
|
1891
|
+
initialProps: createDefaultOptions(ref, { isOpen: false }),
|
|
1892
|
+
wrapper: StrictModeWrapper,
|
|
1893
|
+
},
|
|
1894
|
+
);
|
|
1895
|
+
|
|
1896
|
+
expect(drawer.style.visibility).toBe("hidden");
|
|
1897
|
+
|
|
1898
|
+
// Open drawer
|
|
1899
|
+
rerender(createDefaultOptions(ref, { isOpen: true }));
|
|
1900
|
+
expect(drawer.style.visibility).toBe("visible");
|
|
1901
|
+
|
|
1902
|
+
// Close drawer - animation starts
|
|
1903
|
+
rerender(createDefaultOptions(ref, { isOpen: false }));
|
|
1904
|
+
// During animation, drawer is still visible
|
|
1905
|
+
|
|
1906
|
+
// Complete animation
|
|
1907
|
+
act(() => {
|
|
1908
|
+
runAllFrames();
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
// After animation, drawer should be hidden
|
|
1912
|
+
expect(drawer.style.visibility).toBe("hidden");
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
it("body overflow is cleared on unmount (navigation scenario)", () => {
|
|
1916
|
+
const drawer = createMockDrawer();
|
|
1917
|
+
const ref = { current: drawer };
|
|
1918
|
+
createMockContent();
|
|
1919
|
+
|
|
1920
|
+
// Open drawer with body overflow
|
|
1921
|
+
const { unmount } = renderHook(
|
|
1922
|
+
() => useRevealDrawerTransform(createDefaultOptions(ref, { isOpen: true })),
|
|
1923
|
+
{ wrapper: StrictModeWrapper },
|
|
1924
|
+
);
|
|
1925
|
+
|
|
1926
|
+
// Body should have overflow hidden when drawer is open
|
|
1927
|
+
expect(document.body.style.overflow).toBe("hidden");
|
|
1928
|
+
|
|
1929
|
+
// Unmount (simulates navigation)
|
|
1930
|
+
unmount();
|
|
1931
|
+
|
|
1932
|
+
// Body overflow should be cleared
|
|
1933
|
+
expect(document.body.style.overflow).toBe("");
|
|
1934
|
+
});
|
|
1935
|
+
});
|
|
1936
|
+
});
|