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
|
@@ -85,3 +85,94 @@ export const mergeGestureContainerProps = (
|
|
|
85
85
|
style: mergedStyle,
|
|
86
86
|
};
|
|
87
87
|
};
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Scroll Detection Utilities
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if an element is scrollable in any direction.
|
|
95
|
+
*/
|
|
96
|
+
export function isScrollableElement(element: HTMLElement): boolean {
|
|
97
|
+
const style = getComputedStyle(element);
|
|
98
|
+
|
|
99
|
+
const isScrollableX =
|
|
100
|
+
(style.overflowX === "scroll" || style.overflowX === "auto") &&
|
|
101
|
+
element.scrollWidth > element.clientWidth;
|
|
102
|
+
|
|
103
|
+
const isScrollableY =
|
|
104
|
+
(style.overflowY === "scroll" || style.overflowY === "auto") &&
|
|
105
|
+
element.scrollHeight > element.clientHeight;
|
|
106
|
+
|
|
107
|
+
return isScrollableX || isScrollableY;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if we should start drag based on scroll state.
|
|
112
|
+
* Returns false if the target is inside a scrollable element.
|
|
113
|
+
*/
|
|
114
|
+
export function shouldStartDrag(
|
|
115
|
+
event: React.PointerEvent,
|
|
116
|
+
container: HTMLElement,
|
|
117
|
+
): boolean {
|
|
118
|
+
// eslint-disable-next-line no-restricted-syntax -- loop variable requires let
|
|
119
|
+
let current = event.target as HTMLElement | null;
|
|
120
|
+
|
|
121
|
+
while (current !== null && current !== container) {
|
|
122
|
+
if (isScrollableElement(current)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
current = current.parentElement;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if an element or its ancestors are scrollable in the specified direction.
|
|
133
|
+
* Returns true if scrolling is possible and would block the swipe gesture.
|
|
134
|
+
*
|
|
135
|
+
* @param element - The target element to check
|
|
136
|
+
* @param container - The container boundary
|
|
137
|
+
* @param axis - The axis to check ("x" or "y")
|
|
138
|
+
* @param direction - The swipe direction (1 = right/down, -1 = left/up)
|
|
139
|
+
*/
|
|
140
|
+
export function isScrollableInDirection(
|
|
141
|
+
element: HTMLElement,
|
|
142
|
+
container: HTMLElement,
|
|
143
|
+
axis: "x" | "y",
|
|
144
|
+
direction: 1 | -1,
|
|
145
|
+
): boolean {
|
|
146
|
+
// eslint-disable-next-line no-restricted-syntax -- loop variable requires let
|
|
147
|
+
let current: HTMLElement | null = element;
|
|
148
|
+
|
|
149
|
+
while (current !== null && current !== container) {
|
|
150
|
+
const style = getComputedStyle(current);
|
|
151
|
+
const isHorizontal = axis === "x";
|
|
152
|
+
|
|
153
|
+
const overflow = isHorizontal ? style.overflowX : style.overflowY;
|
|
154
|
+
const isScrollable = overflow === "scroll" || overflow === "auto";
|
|
155
|
+
|
|
156
|
+
if (isScrollable) {
|
|
157
|
+
const scrollSize = isHorizontal
|
|
158
|
+
? current.scrollWidth - current.clientWidth
|
|
159
|
+
: current.scrollHeight - current.clientHeight;
|
|
160
|
+
|
|
161
|
+
if (scrollSize > 0) {
|
|
162
|
+
const scrollPos = isHorizontal ? current.scrollLeft : current.scrollTop;
|
|
163
|
+
|
|
164
|
+
// If swiping in close direction and not at boundary, block swipe
|
|
165
|
+
if (direction === -1 && scrollPos > 1) {
|
|
166
|
+
return true; // Can scroll left/up, block swipe
|
|
167
|
+
}
|
|
168
|
+
if (direction === 1 && scrollPos < scrollSize - 1) {
|
|
169
|
+
return true; // Can scroll right/down, block swipe
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
current = current.parentElement;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
@@ -5,9 +5,46 @@
|
|
|
5
5
|
* 1. アニメーションなし → 即座にdisplay:none
|
|
6
6
|
* 2. アニメーションあり → 完了待ってdisplay:none
|
|
7
7
|
*/
|
|
8
|
+
import type * as React from "react";
|
|
8
9
|
import { renderHook, act } from "@testing-library/react";
|
|
9
10
|
import { useAnimatedVisibility } from "./useAnimatedVisibility.js";
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Create a mock AnimationEvent for testing.
|
|
14
|
+
*/
|
|
15
|
+
function createMockAnimationEvent(
|
|
16
|
+
target: EventTarget,
|
|
17
|
+
currentTarget: EventTarget,
|
|
18
|
+
): React.AnimationEvent {
|
|
19
|
+
const noop = (): void => {};
|
|
20
|
+
const noopBool = (): boolean => false;
|
|
21
|
+
const nativeEvent = {
|
|
22
|
+
animationName: "test",
|
|
23
|
+
elapsedTime: 0,
|
|
24
|
+
pseudoElement: "",
|
|
25
|
+
} as AnimationEvent;
|
|
26
|
+
return {
|
|
27
|
+
target,
|
|
28
|
+
currentTarget,
|
|
29
|
+
nativeEvent,
|
|
30
|
+
bubbles: true,
|
|
31
|
+
cancelable: false,
|
|
32
|
+
defaultPrevented: false,
|
|
33
|
+
eventPhase: 0,
|
|
34
|
+
isTrusted: true,
|
|
35
|
+
preventDefault: noop,
|
|
36
|
+
isDefaultPrevented: noopBool,
|
|
37
|
+
stopPropagation: noop,
|
|
38
|
+
isPropagationStopped: noopBool,
|
|
39
|
+
persist: noop,
|
|
40
|
+
timeStamp: Date.now(),
|
|
41
|
+
type: "animationend",
|
|
42
|
+
animationName: "test",
|
|
43
|
+
elapsedTime: 0,
|
|
44
|
+
pseudoElement: "",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
11
48
|
describe("useAnimatedVisibility", () => {
|
|
12
49
|
describe("initial state", () => {
|
|
13
50
|
it("displays when initially visible", () => {
|
|
@@ -120,10 +157,7 @@ describe("useAnimatedVisibility", () => {
|
|
|
120
157
|
|
|
121
158
|
// Simulate animationend event
|
|
122
159
|
const sharedElement = document.createElement("div");
|
|
123
|
-
const mockEvent =
|
|
124
|
-
target: sharedElement,
|
|
125
|
-
currentTarget: sharedElement,
|
|
126
|
-
} as unknown as React.AnimationEvent;
|
|
160
|
+
const mockEvent = createMockAnimationEvent(sharedElement, sharedElement);
|
|
127
161
|
|
|
128
162
|
act(() => {
|
|
129
163
|
result.current.props.onAnimationEnd(mockEvent);
|
|
@@ -149,10 +183,7 @@ describe("useAnimatedVisibility", () => {
|
|
|
149
183
|
// Simulate animationend from a child element (target !== currentTarget)
|
|
150
184
|
const parent = document.createElement("div");
|
|
151
185
|
const child = document.createElement("div");
|
|
152
|
-
const mockEvent =
|
|
153
|
-
target: child,
|
|
154
|
-
currentTarget: parent,
|
|
155
|
-
} as unknown as React.AnimationEvent;
|
|
186
|
+
const mockEvent = createMockAnimationEvent(child, parent);
|
|
156
187
|
|
|
157
188
|
act(() => {
|
|
158
189
|
result.current.props.onAnimationEnd(mockEvent);
|
|
@@ -188,14 +219,12 @@ describe("useAnimatedVisibility", () => {
|
|
|
188
219
|
|
|
189
220
|
describe("timeout fallback", () => {
|
|
190
221
|
it("hides after timeout if animationEnd never fires", async () => {
|
|
191
|
-
vi.useFakeTimers();
|
|
192
|
-
|
|
193
222
|
const { result, rerender } = renderHook(
|
|
194
223
|
({ isVisible }) =>
|
|
195
224
|
useAnimatedVisibility({
|
|
196
225
|
isVisible,
|
|
197
226
|
leaveAnimation: "fadeOut 200ms ease-out",
|
|
198
|
-
animationTimeout:
|
|
227
|
+
animationTimeout: 10,
|
|
199
228
|
}),
|
|
200
229
|
{ initialProps: { isVisible: true } },
|
|
201
230
|
);
|
|
@@ -206,25 +235,21 @@ describe("useAnimatedVisibility", () => {
|
|
|
206
235
|
|
|
207
236
|
// Advance time past timeout
|
|
208
237
|
await act(async () => {
|
|
209
|
-
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
210
239
|
});
|
|
211
240
|
|
|
212
241
|
// Should be hidden now (fallback triggered)
|
|
213
242
|
expect(result.current.style.display).toBe("none");
|
|
214
243
|
expect(result.current.state.isAnimatingOut).toBe(false);
|
|
215
|
-
|
|
216
|
-
vi.useRealTimers();
|
|
217
244
|
});
|
|
218
245
|
|
|
219
246
|
it("clears timeout when animationEnd fires before timeout", async () => {
|
|
220
|
-
vi.useFakeTimers();
|
|
221
|
-
|
|
222
247
|
const { result, rerender } = renderHook(
|
|
223
248
|
({ isVisible }) =>
|
|
224
249
|
useAnimatedVisibility({
|
|
225
250
|
isVisible,
|
|
226
251
|
leaveAnimation: "fadeOut 200ms ease-out",
|
|
227
|
-
animationTimeout:
|
|
252
|
+
animationTimeout: 10,
|
|
228
253
|
}),
|
|
229
254
|
{ initialProps: { isVisible: true } },
|
|
230
255
|
);
|
|
@@ -233,10 +258,7 @@ describe("useAnimatedVisibility", () => {
|
|
|
233
258
|
|
|
234
259
|
// Fire animationEnd before timeout
|
|
235
260
|
const sharedElement = document.createElement("div");
|
|
236
|
-
const mockEvent =
|
|
237
|
-
target: sharedElement,
|
|
238
|
-
currentTarget: sharedElement,
|
|
239
|
-
} as unknown as React.AnimationEvent;
|
|
261
|
+
const mockEvent = createMockAnimationEvent(sharedElement, sharedElement);
|
|
240
262
|
|
|
241
263
|
act(() => {
|
|
242
264
|
result.current.props.onAnimationEnd(mockEvent);
|
|
@@ -246,12 +268,10 @@ describe("useAnimatedVisibility", () => {
|
|
|
246
268
|
|
|
247
269
|
// Advance past timeout - should not affect state
|
|
248
270
|
await act(async () => {
|
|
249
|
-
|
|
271
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
250
272
|
});
|
|
251
273
|
|
|
252
274
|
expect(result.current.style.display).toBe("none");
|
|
253
|
-
|
|
254
|
-
vi.useRealTimers();
|
|
255
275
|
});
|
|
256
276
|
});
|
|
257
277
|
});
|
|
@@ -72,6 +72,32 @@ export function useAnimatedVisibility({
|
|
|
72
72
|
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
73
73
|
|
|
74
74
|
// Clear timeout on unmount
|
|
75
|
+
const shouldSkipLeaveAnimation = (
|
|
76
|
+
isSkipped: boolean,
|
|
77
|
+
animation: string | undefined,
|
|
78
|
+
): boolean => {
|
|
79
|
+
if (isSkipped) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (!animation) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (animation === "none") {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const getShouldDisplay = (visible: boolean, animatingOut: boolean): boolean => {
|
|
92
|
+
if (visible) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (animatingOut) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
};
|
|
100
|
+
|
|
75
101
|
React.useEffect(() => {
|
|
76
102
|
return () => {
|
|
77
103
|
if (timeoutRef.current) {
|
|
@@ -92,7 +118,7 @@ export function useAnimatedVisibility({
|
|
|
92
118
|
|
|
93
119
|
if (wasVisible && !isVisible) {
|
|
94
120
|
// Transitioning from visible to hidden
|
|
95
|
-
if (skipAnimation
|
|
121
|
+
if (shouldSkipLeaveAnimation(skipAnimation, leaveAnimation)) {
|
|
96
122
|
// No animation, hide immediately
|
|
97
123
|
setIsAnimatingOut(false);
|
|
98
124
|
} else {
|
|
@@ -129,7 +155,7 @@ export function useAnimatedVisibility({
|
|
|
129
155
|
// Element should be displayed if:
|
|
130
156
|
// - It's visible, OR
|
|
131
157
|
// - It's animating out (leave animation in progress)
|
|
132
|
-
const shouldDisplay = isVisible
|
|
158
|
+
const shouldDisplay = getShouldDisplay(isVisible, isAnimatingOut);
|
|
133
159
|
|
|
134
160
|
return {
|
|
135
161
|
state: {
|
|
@@ -40,6 +40,14 @@ export const easings = {
|
|
|
40
40
|
}
|
|
41
41
|
return 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
42
42
|
},
|
|
43
|
+
|
|
44
|
+
/** Ease in expo (accelerating, for "suck in" effect) */
|
|
45
|
+
easeInExpo: (t: number): number => {
|
|
46
|
+
if (t === 0) {
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
return Math.pow(2, 10 * t - 10);
|
|
50
|
+
},
|
|
43
51
|
} as const;
|
|
44
52
|
|
|
45
53
|
/**
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tests for useOperationContinuity hook.
|
|
3
|
+
*/
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { renderHook } from "@testing-library/react";
|
|
6
|
+
import { useOperationContinuity } from "./useOperationContinuity.js";
|
|
7
|
+
|
|
8
|
+
const StrictModeWrapper = ({ children }: { children: React.ReactNode }): React.ReactNode => {
|
|
9
|
+
return React.createElement(React.StrictMode, null, children);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("useOperationContinuity", () => {
|
|
13
|
+
describe("value continuity", () => {
|
|
14
|
+
it("returns current value when not retaining", () => {
|
|
15
|
+
const { result } = renderHook(() => useOperationContinuity("active", false));
|
|
16
|
+
expect(result.current.value).toBe("active");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns current value when retaining but value unchanged", () => {
|
|
20
|
+
const { result } = renderHook(() => useOperationContinuity("active", true));
|
|
21
|
+
expect(result.current.value).toBe("active");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("retains previous value when shouldRetainPrevious is true", () => {
|
|
25
|
+
const { result, rerender } = renderHook(
|
|
26
|
+
({ value, shouldRetainPrevious }) => useOperationContinuity(value, shouldRetainPrevious),
|
|
27
|
+
{ initialProps: { value: "behind", shouldRetainPrevious: true } },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(result.current.value).toBe("behind");
|
|
31
|
+
|
|
32
|
+
// Value changes but we're still retaining
|
|
33
|
+
rerender({ value: "active", shouldRetainPrevious: true });
|
|
34
|
+
expect(result.current.value).toBe("behind");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("accepts new value when shouldRetainPrevious becomes false", () => {
|
|
38
|
+
const { result, rerender } = renderHook(
|
|
39
|
+
({ value, shouldRetainPrevious }) => useOperationContinuity(value, shouldRetainPrevious),
|
|
40
|
+
{ initialProps: { value: "behind", shouldRetainPrevious: true } },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Value changes while retaining
|
|
44
|
+
rerender({ value: "active", shouldRetainPrevious: true });
|
|
45
|
+
expect(result.current.value).toBe("behind");
|
|
46
|
+
|
|
47
|
+
// Stop retaining - should accept new value
|
|
48
|
+
rerender({ value: "active", shouldRetainPrevious: false });
|
|
49
|
+
expect(result.current.value).toBe("active");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("updates stored value when not retaining", () => {
|
|
53
|
+
const { result, rerender } = renderHook(
|
|
54
|
+
({ value, shouldRetainPrevious }) => useOperationContinuity(value, shouldRetainPrevious),
|
|
55
|
+
{ initialProps: { value: "behind", shouldRetainPrevious: false } },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
rerender({ value: "active", shouldRetainPrevious: false });
|
|
59
|
+
expect(result.current.value).toBe("active");
|
|
60
|
+
|
|
61
|
+
// Start retaining - should keep "active"
|
|
62
|
+
rerender({ value: "hidden", shouldRetainPrevious: true });
|
|
63
|
+
expect(result.current.value).toBe("active");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("changedDuringOperation tracking", () => {
|
|
68
|
+
it("returns false when value never changed", () => {
|
|
69
|
+
const { result, rerender } = renderHook(
|
|
70
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
71
|
+
{ initialProps: { value: "active", retain: true } },
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
75
|
+
|
|
76
|
+
// End retention without value change
|
|
77
|
+
rerender({ value: "active", retain: false });
|
|
78
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns true when value changed during retention", () => {
|
|
82
|
+
const { result, rerender } = renderHook(
|
|
83
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
84
|
+
{ initialProps: { value: "behind", retain: true } },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
88
|
+
|
|
89
|
+
// Value changes while retaining
|
|
90
|
+
rerender({ value: "active", retain: true });
|
|
91
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
92
|
+
|
|
93
|
+
// End retention - should still be true (for this render)
|
|
94
|
+
rerender({ value: "active", retain: false });
|
|
95
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("resets changedDuringOperation after operation ends", () => {
|
|
99
|
+
const { result, rerender } = renderHook(
|
|
100
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
101
|
+
{ initialProps: { value: "behind", retain: true } },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Value changes while retaining
|
|
105
|
+
rerender({ value: "active", retain: true });
|
|
106
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
107
|
+
|
|
108
|
+
// End retention
|
|
109
|
+
rerender({ value: "active", retain: false });
|
|
110
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
111
|
+
|
|
112
|
+
// Next render - should be reset
|
|
113
|
+
rerender({ value: "active", retain: false });
|
|
114
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("tracks changes across multiple operations", () => {
|
|
118
|
+
const { result, rerender } = renderHook(
|
|
119
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
120
|
+
{ initialProps: { value: "behind", retain: true } },
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// First operation: value changes
|
|
124
|
+
rerender({ value: "active", retain: true });
|
|
125
|
+
rerender({ value: "active", retain: false });
|
|
126
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
127
|
+
|
|
128
|
+
// Reset
|
|
129
|
+
rerender({ value: "active", retain: false });
|
|
130
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
131
|
+
|
|
132
|
+
// Second operation: no value change
|
|
133
|
+
rerender({ value: "active", retain: true });
|
|
134
|
+
rerender({ value: "active", retain: false });
|
|
135
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
136
|
+
|
|
137
|
+
// Third operation: value changes again
|
|
138
|
+
rerender({ value: "active", retain: true });
|
|
139
|
+
rerender({ value: "hidden", retain: true });
|
|
140
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
141
|
+
rerender({ value: "hidden", retain: false });
|
|
142
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("simultaneous value and retention change", () => {
|
|
147
|
+
/**
|
|
148
|
+
* CRITICAL: This tests the over-swipe bug scenario.
|
|
149
|
+
*
|
|
150
|
+
* In the real app, when user releases after over-swipe:
|
|
151
|
+
* - displacement becomes 0 (shouldRetainPrevious becomes false)
|
|
152
|
+
* - role changes from "active" to "hidden"
|
|
153
|
+
* Both happen in the same render!
|
|
154
|
+
*
|
|
155
|
+
* The hook should detect that the value changed even though
|
|
156
|
+
* the change happened at the exact moment retention ended.
|
|
157
|
+
*/
|
|
158
|
+
it("detects value change when it happens simultaneously with retention ending", () => {
|
|
159
|
+
const { result, rerender } = renderHook(
|
|
160
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
161
|
+
{ initialProps: { role: "active" as const, displacement: 500 } },
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// During swipe: role="active", retaining
|
|
165
|
+
expect(result.current.value).toBe("active");
|
|
166
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
167
|
+
|
|
168
|
+
// Simulate release: BOTH displacement becomes 0 AND role changes to "hidden"
|
|
169
|
+
// This is what happens in the real app during over-swipe
|
|
170
|
+
rerender({ role: "hidden" as const, displacement: 0 });
|
|
171
|
+
|
|
172
|
+
// value should now be "hidden" (retention ended)
|
|
173
|
+
expect(result.current.value).toBe("hidden");
|
|
174
|
+
// CRITICAL: changedDuringOperation should be TRUE because the value
|
|
175
|
+
// changed from "active" to "hidden" at the moment retention ended
|
|
176
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("does not report change when value stays the same at retention end", () => {
|
|
180
|
+
const { result, rerender } = renderHook(
|
|
181
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
182
|
+
{ initialProps: { role: "active" as const, displacement: 500 } },
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(result.current.value).toBe("active");
|
|
186
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
187
|
+
|
|
188
|
+
// Release but role stays "active" (e.g., partial swipe that didn't trigger navigation)
|
|
189
|
+
rerender({ role: "active" as const, displacement: 0 });
|
|
190
|
+
|
|
191
|
+
expect(result.current.value).toBe("active");
|
|
192
|
+
// No change occurred
|
|
193
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("does NOT report change during button navigation (no operation)", () => {
|
|
197
|
+
// This is the button navigation case: value changes but there was never
|
|
198
|
+
// any retention (no swipe operation). We should NOT report changedDuringOperation
|
|
199
|
+
// because this is normal navigation, not an operation-related change.
|
|
200
|
+
const { result, rerender } = renderHook(
|
|
201
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
202
|
+
{ initialProps: { role: "active" as const, displacement: 0 } },
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(result.current.value).toBe("active");
|
|
206
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
207
|
+
|
|
208
|
+
// Button navigation: role changes but there's no operation (displacement is always 0)
|
|
209
|
+
rerender({ role: "behind" as const, displacement: 0 });
|
|
210
|
+
|
|
211
|
+
expect(result.current.value).toBe("behind");
|
|
212
|
+
// CRITICAL: changedDuringOperation should be FALSE because there was no operation
|
|
213
|
+
// This allows the animation to happen normally for button navigation
|
|
214
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("role transition scenarios", () => {
|
|
219
|
+
it("maintains role continuity during swipe (behind -> active)", () => {
|
|
220
|
+
// Simulates: behind panel becomes active during swipe
|
|
221
|
+
// displacement > 0, so we should retain the previous role
|
|
222
|
+
const { result, rerender } = renderHook(
|
|
223
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
224
|
+
{ initialProps: { role: "behind" as const, displacement: 100 } },
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(result.current.value).toBe("behind");
|
|
228
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
229
|
+
|
|
230
|
+
// Navigation changes role to "active" but displacement is still positive
|
|
231
|
+
rerender({ role: "active" as const, displacement: 100 });
|
|
232
|
+
expect(result.current.value).toBe("behind");
|
|
233
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
234
|
+
|
|
235
|
+
// Swipe ends (displacement becomes 0)
|
|
236
|
+
rerender({ role: "active" as const, displacement: 0 });
|
|
237
|
+
expect(result.current.value).toBe("active");
|
|
238
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("maintains role continuity during swipe (active -> hidden)", () => {
|
|
242
|
+
// Simulates: over-swipe where active panel becomes hidden
|
|
243
|
+
const { result, rerender } = renderHook(
|
|
244
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
245
|
+
{ initialProps: { role: "active" as const, displacement: 400 } },
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
expect(result.current.value).toBe("active");
|
|
249
|
+
|
|
250
|
+
// Over-swipe triggers navigation change
|
|
251
|
+
rerender({ role: "hidden" as const, displacement: 500 });
|
|
252
|
+
expect(result.current.value).toBe("active");
|
|
253
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
254
|
+
|
|
255
|
+
// Swipe ends
|
|
256
|
+
rerender({ role: "hidden" as const, displacement: 0 });
|
|
257
|
+
expect(result.current.value).toBe("hidden");
|
|
258
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("provides changedDuringOperation for animation decision", () => {
|
|
262
|
+
// This test demonstrates the intended use case:
|
|
263
|
+
// Use changedDuringOperation to decide whether to animate on operation end
|
|
264
|
+
const { result, rerender } = renderHook(
|
|
265
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
266
|
+
{ initialProps: { role: "behind" as const, displacement: 100 } },
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Simulate role change during swipe
|
|
270
|
+
rerender({ role: "active" as const, displacement: 100 });
|
|
271
|
+
|
|
272
|
+
// When swipe ends, changedDuringOperation tells us to skip animation
|
|
273
|
+
rerender({ role: "active" as const, displacement: 0 });
|
|
274
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
275
|
+
// Consumer would use this to skip target change animation
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("works with different value types", () => {
|
|
280
|
+
it("works with numbers", () => {
|
|
281
|
+
const { result, rerender } = renderHook(
|
|
282
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
283
|
+
{ initialProps: { value: 0, retain: true } },
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
rerender({ value: 1, retain: true });
|
|
287
|
+
expect(result.current.value).toBe(0);
|
|
288
|
+
|
|
289
|
+
rerender({ value: 1, retain: false });
|
|
290
|
+
expect(result.current.value).toBe(1);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("works with objects (by reference)", () => {
|
|
294
|
+
const obj1 = { id: 1 };
|
|
295
|
+
const obj2 = { id: 2 };
|
|
296
|
+
|
|
297
|
+
const { result, rerender } = renderHook(
|
|
298
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
299
|
+
{ initialProps: { value: obj1, retain: true } },
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
rerender({ value: obj2, retain: true });
|
|
303
|
+
expect(result.current.value).toBe(obj1);
|
|
304
|
+
|
|
305
|
+
rerender({ value: obj2, retain: false });
|
|
306
|
+
expect(result.current.value).toBe(obj2);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("React StrictMode compatibility", () => {
|
|
311
|
+
/**
|
|
312
|
+
* CRITICAL: These tests verify the hook works correctly in StrictMode.
|
|
313
|
+
*
|
|
314
|
+
* In StrictMode, React calls the render function twice. Hooks that mutate
|
|
315
|
+
* refs during render will see the mutated value on the second call, which
|
|
316
|
+
* can cause bugs.
|
|
317
|
+
*
|
|
318
|
+
* This hook uses useLayoutEffect for ref mutations to avoid this issue.
|
|
319
|
+
*/
|
|
320
|
+
it("operationJustEnded is correct in StrictMode", () => {
|
|
321
|
+
const { result, rerender } = renderHook(
|
|
322
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
323
|
+
{
|
|
324
|
+
initialProps: { value: "active", retain: true },
|
|
325
|
+
wrapper: StrictModeWrapper,
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// During retention
|
|
330
|
+
expect(result.current.operationJustEnded).toBe(false);
|
|
331
|
+
|
|
332
|
+
// End retention - operationJustEnded should be true
|
|
333
|
+
rerender({ value: "active", retain: false });
|
|
334
|
+
expect(result.current.operationJustEnded).toBe(true);
|
|
335
|
+
|
|
336
|
+
// Next render - should be false again
|
|
337
|
+
rerender({ value: "active", retain: false });
|
|
338
|
+
expect(result.current.operationJustEnded).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("over-swipe scenario works in StrictMode", () => {
|
|
342
|
+
// This is the exact scenario that was broken before the fix:
|
|
343
|
+
// User swipes beyond 100%, releases, and we need operationJustEnded=true
|
|
344
|
+
// to prevent the visual jump.
|
|
345
|
+
const { result, rerender } = renderHook(
|
|
346
|
+
({ role, displacement }) => useOperationContinuity(role, displacement > 0),
|
|
347
|
+
{
|
|
348
|
+
initialProps: { role: "active" as const, displacement: 500 },
|
|
349
|
+
wrapper: StrictModeWrapper,
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// During over-swipe
|
|
354
|
+
expect(result.current.value).toBe("active");
|
|
355
|
+
expect(result.current.operationJustEnded).toBe(false);
|
|
356
|
+
|
|
357
|
+
// Release (displacement becomes 0)
|
|
358
|
+
rerender({ role: "active" as const, displacement: 0 });
|
|
359
|
+
|
|
360
|
+
// CRITICAL: operationJustEnded must be true even in StrictMode
|
|
361
|
+
// This is what was broken before the fix
|
|
362
|
+
expect(result.current.operationJustEnded).toBe(true);
|
|
363
|
+
expect(result.current.value).toBe("active");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("changedDuringOperation is tracked correctly in StrictMode", () => {
|
|
367
|
+
const { result, rerender } = renderHook(
|
|
368
|
+
({ value, retain }) => useOperationContinuity(value, retain),
|
|
369
|
+
{
|
|
370
|
+
initialProps: { value: "behind", retain: true },
|
|
371
|
+
wrapper: StrictModeWrapper,
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
expect(result.current.changedDuringOperation).toBe(false);
|
|
376
|
+
|
|
377
|
+
// Value changes during retention
|
|
378
|
+
rerender({ value: "active", retain: true });
|
|
379
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
380
|
+
|
|
381
|
+
// End retention
|
|
382
|
+
rerender({ value: "active", retain: false });
|
|
383
|
+
expect(result.current.changedDuringOperation).toBe(true);
|
|
384
|
+
expect(result.current.operationJustEnded).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
});
|