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.
Files changed (216) hide show
  1. package/dist/{FloatingPanelFrame-SgYLc6Ud.js → FloatingPanelFrame-3eU9AwPo.js} +2 -2
  2. package/dist/{FloatingPanelFrame-SgYLc6Ud.js.map → FloatingPanelFrame-3eU9AwPo.js.map} +1 -1
  3. package/dist/FloatingWindow-CUXnEtrb.js +827 -0
  4. package/dist/FloatingWindow-CUXnEtrb.js.map +1 -0
  5. package/dist/FloatingWindow-DMwyK0eK.cjs +2 -0
  6. package/dist/FloatingWindow-DMwyK0eK.cjs.map +1 -0
  7. package/dist/GridLayout-DKTg_N61.cjs +2 -0
  8. package/dist/{GridLayout-B4VRsC0r.cjs.map → GridLayout-DKTg_N61.cjs.map} +1 -1
  9. package/dist/{GridLayout-BltqeCPK.js → GridLayout-UWNxXw77.js} +34 -35
  10. package/dist/{GridLayout-BltqeCPK.js.map → GridLayout-UWNxXw77.js.map} +1 -1
  11. package/dist/{HorizontalDivider-WF1k_qND.js → HorizontalDivider-DdxzfV0l.js} +3 -3
  12. package/dist/{HorizontalDivider-WF1k_qND.js.map → HorizontalDivider-DdxzfV0l.js.map} +1 -1
  13. package/dist/{HorizontalDivider-B5Z-KZLk.cjs → HorizontalDivider-_pgV4Mcv.cjs} +2 -2
  14. package/dist/{HorizontalDivider-B5Z-KZLk.cjs.map → HorizontalDivider-_pgV4Mcv.cjs.map} +1 -1
  15. package/dist/{PanelSystem-Dr1TBhxM.js → PanelSystem-BqUzNtf2.js} +5 -5
  16. package/dist/{PanelSystem-Dr1TBhxM.js.map → PanelSystem-BqUzNtf2.js.map} +1 -1
  17. package/dist/{PanelSystem-Bs8bQwQF.cjs → PanelSystem-D603LKKv.cjs} +2 -2
  18. package/dist/{PanelSystem-Bs8bQwQF.cjs.map → PanelSystem-D603LKKv.cjs.map} +1 -1
  19. package/dist/ResizeHandle-CBcAS918.cjs +2 -0
  20. package/dist/{ResizeHandle-CScipO5l.cjs.map → ResizeHandle-CBcAS918.cjs.map} +1 -1
  21. package/dist/{ResizeHandle-CdA_JYfN.js → ResizeHandle-CXjc1meV.js} +28 -29
  22. package/dist/{ResizeHandle-CdA_JYfN.js.map → ResizeHandle-CXjc1meV.js.map} +1 -1
  23. package/dist/SwipePivotTabBar-DWrCuwEI.js +411 -0
  24. package/dist/SwipePivotTabBar-DWrCuwEI.js.map +1 -0
  25. package/dist/SwipePivotTabBar-fjjXkpj7.cjs +2 -0
  26. package/dist/SwipePivotTabBar-fjjXkpj7.cjs.map +1 -0
  27. package/dist/components/gesture/SwipeSafeZone.d.ts +40 -0
  28. package/dist/components/window/Drawer.d.ts +3 -1
  29. package/dist/components/window/DrawerLayers.d.ts +1 -1
  30. package/dist/components/window/drawerStyles.d.ts +69 -0
  31. package/dist/components/window/drawerSwipeConfig.d.ts +29 -0
  32. package/dist/components/window/useDrawerSwipeTransform.d.ts +23 -0
  33. package/dist/config.cjs +1 -1
  34. package/dist/config.js +3 -3
  35. package/dist/constants/styles.d.ts +17 -0
  36. package/dist/dialog/index.d.ts +69 -0
  37. package/dist/floating.js +1 -1
  38. package/dist/grid.cjs +1 -1
  39. package/dist/grid.js +2 -2
  40. package/dist/hooks/gesture/testing/createGestureSimulator.d.ts +7 -0
  41. package/dist/hooks/gesture/types.d.ts +48 -5
  42. package/dist/hooks/gesture/utils.d.ts +19 -0
  43. package/dist/hooks/useAnimationFrame.d.ts +2 -0
  44. package/dist/hooks/useOperationContinuity.d.ts +64 -0
  45. package/dist/hooks/useResizeObserver.d.ts +33 -1
  46. package/dist/hooks/useSharedElementTransition.d.ts +112 -0
  47. package/dist/hooks/useSwipeContentTransform.d.ts +9 -2
  48. package/dist/index.cjs +1 -1
  49. package/dist/index.js +7 -7
  50. package/dist/modules/dialog/AlertDialog.d.ts +9 -0
  51. package/dist/modules/dialog/DialogContainer.d.ts +37 -0
  52. package/dist/modules/dialog/Modal.d.ts +26 -0
  53. package/dist/modules/dialog/SwipeDialogContainer.d.ts +16 -0
  54. package/dist/modules/dialog/dialogAnimationUtils.d.ts +113 -0
  55. package/dist/modules/dialog/types.d.ts +183 -0
  56. package/dist/modules/dialog/useDialog.d.ts +39 -0
  57. package/dist/modules/dialog/useDialogContainer.d.ts +47 -0
  58. package/dist/modules/dialog/useDialogSwipeInput.d.ts +70 -0
  59. package/dist/modules/dialog/useDialogTransform.d.ts +82 -0
  60. package/dist/modules/drawer/types.d.ts +74 -0
  61. package/dist/modules/drawer/useDrawerSwipeInput.d.ts +24 -0
  62. package/dist/modules/pivot/SwipePivotTabBar.d.ts +3 -0
  63. package/dist/modules/stack/SwipeStackContent.d.ts +6 -3
  64. package/dist/modules/stack/SwipeStackOutlet.d.ts +4 -4
  65. package/dist/modules/stack/computeSwipeStackTransform.d.ts +1 -1
  66. package/dist/panels.cjs +1 -1
  67. package/dist/panels.js +1 -1
  68. package/dist/pivot.cjs +1 -1
  69. package/dist/pivot.js +1 -1
  70. package/dist/resizer.cjs +1 -1
  71. package/dist/resizer.js +2 -2
  72. package/dist/stack.cjs +1 -1
  73. package/dist/stack.cjs.map +1 -1
  74. package/dist/stack.js +503 -762
  75. package/dist/stack.js.map +1 -1
  76. package/dist/sticky-header/calculateStickyMetrics.d.ts +28 -0
  77. package/dist/sticky-header.cjs +1 -1
  78. package/dist/sticky-header.cjs.map +1 -1
  79. package/dist/sticky-header.js +59 -51
  80. package/dist/sticky-header.js.map +1 -1
  81. package/dist/{styles-DPPuJ0sf.js → styles-NkjuMOVS.js} +13 -13
  82. package/dist/{styles-DPPuJ0sf.js.map → styles-NkjuMOVS.js.map} +1 -1
  83. package/dist/styles-qf6ptVLD.cjs.map +1 -1
  84. package/dist/types.d.ts +16 -0
  85. package/dist/useDocumentPointerEvents-DXxw3qWj.js +54 -0
  86. package/dist/useDocumentPointerEvents-DXxw3qWj.js.map +1 -0
  87. package/dist/useDocumentPointerEvents-DxDSOtip.cjs +2 -0
  88. package/dist/useDocumentPointerEvents-DxDSOtip.cjs.map +1 -0
  89. package/dist/useNativeGestureGuard-C7TSqEkr.cjs +2 -0
  90. package/dist/useNativeGestureGuard-C7TSqEkr.cjs.map +1 -0
  91. package/dist/useNativeGestureGuard-CGYo6O0r.js +347 -0
  92. package/dist/useNativeGestureGuard-CGYo6O0r.js.map +1 -0
  93. package/dist/window/index.d.ts +2 -0
  94. package/dist/window.cjs +1 -1
  95. package/dist/window.cjs.map +1 -1
  96. package/dist/window.js +114 -103
  97. package/dist/window.js.map +1 -1
  98. package/package.json +6 -1
  99. package/src/components/gesture/SwipeSafeZone.tsx +69 -0
  100. package/src/components/window/Drawer.tsx +249 -162
  101. package/src/components/window/DrawerLayers.tsx +13 -3
  102. package/src/components/window/drawerStyles.spec.ts +263 -0
  103. package/src/components/window/drawerStyles.ts +228 -0
  104. package/src/components/window/drawerSwipeConfig.spec.ts +131 -0
  105. package/src/components/window/drawerSwipeConfig.ts +112 -0
  106. package/src/components/window/useDrawerSwipeTransform.spec.ts +234 -0
  107. package/src/components/window/useDrawerSwipeTransform.ts +129 -0
  108. package/src/constants/styles.ts +19 -0
  109. package/src/demo/pages/Dialog/alerts/index.tsx +22 -0
  110. package/src/demo/pages/Dialog/card/index.tsx +22 -0
  111. package/src/demo/pages/Dialog/components/AlertDialogDemo.tsx +124 -0
  112. package/src/demo/pages/Dialog/components/CardExpandDemo.module.css +243 -0
  113. package/src/demo/pages/Dialog/components/CardExpandDemo.tsx +204 -0
  114. package/src/demo/pages/Dialog/components/CustomAlertDialogDemo.tsx +219 -0
  115. package/src/demo/pages/Dialog/components/DialogDemos.module.css +77 -0
  116. package/src/demo/pages/Dialog/components/ModalBasics.tsx +45 -0
  117. package/src/demo/pages/Dialog/components/SwipeDialogDemo.module.css +77 -0
  118. package/src/demo/pages/Dialog/components/SwipeDialogDemo.tsx +181 -0
  119. package/src/demo/pages/Dialog/custom-alert/index.tsx +22 -0
  120. package/src/demo/pages/Dialog/modal/index.tsx +17 -0
  121. package/src/demo/pages/Dialog/swipe/index.tsx +22 -0
  122. package/src/demo/pages/Drawer/components/DrawerSwipe.module.css +316 -0
  123. package/src/demo/pages/Drawer/components/DrawerSwipe.tsx +178 -0
  124. package/src/demo/pages/Drawer/swipe/index.tsx +17 -0
  125. package/src/demo/pages/Pivot/components/SwipeTabsPivot.tsx +54 -23
  126. package/src/demo/pages/Pivot/swipe-debug/index.tsx +1 -1
  127. package/src/demo/pages/Stack/components/StackBasics.spec.tsx +152 -0
  128. package/src/demo/pages/Stack/components/StackBasics.tsx +179 -95
  129. package/src/demo/pages/Stack/components/StackTablet.spec.tsx +120 -0
  130. package/src/demo/pages/Stack/components/StackTablet.tsx +42 -21
  131. package/src/demo/routes.tsx +22 -1
  132. package/src/dialog/index.ts +85 -0
  133. package/src/hooks/gesture/testing/createGestureSimulator.spec.ts +68 -64
  134. package/src/hooks/gesture/testing/createGestureSimulator.ts +112 -37
  135. package/src/hooks/gesture/types.ts +83 -6
  136. package/src/hooks/gesture/useEdgeSwipeInput.spec.ts +22 -14
  137. package/src/hooks/gesture/useNativeGestureGuard.spec.ts +91 -31
  138. package/src/hooks/gesture/useNativeGestureGuard.ts +3 -1
  139. package/src/hooks/gesture/utils.ts +91 -0
  140. package/src/hooks/useAnimatedVisibility.spec.ts +44 -24
  141. package/src/hooks/useAnimatedVisibility.ts +28 -2
  142. package/src/hooks/useAnimationFrame.ts +8 -0
  143. package/src/hooks/useOperationContinuity.spec.ts +387 -0
  144. package/src/hooks/useOperationContinuity.ts +135 -0
  145. package/src/hooks/useResizeObserver.spec.tsx +277 -0
  146. package/src/hooks/useResizeObserver.tsx +108 -39
  147. package/src/hooks/useScrollContainer.ts +4 -10
  148. package/src/hooks/useSharedElementTransition.ts +333 -0
  149. package/src/hooks/useSwipeContentTransform.spec.ts +18 -18
  150. package/src/hooks/useSwipeContentTransform.ts +166 -28
  151. package/src/modules/dialog/AlertDialog.spec.tsx +387 -0
  152. package/src/modules/dialog/AlertDialog.tsx +221 -0
  153. package/src/modules/dialog/DialogContainer.spec.tsx +228 -0
  154. package/src/modules/dialog/DialogContainer.tsx +188 -0
  155. package/src/modules/dialog/Modal.spec.tsx +220 -0
  156. package/src/modules/dialog/Modal.tsx +182 -0
  157. package/src/modules/dialog/SwipeDialogContainer.tsx +208 -0
  158. package/src/modules/dialog/dialogAnimationUtils.spec.ts +253 -0
  159. package/src/modules/dialog/dialogAnimationUtils.ts +297 -0
  160. package/src/modules/dialog/types.ts +186 -0
  161. package/src/modules/dialog/useDialog.spec.tsx +447 -0
  162. package/src/modules/dialog/useDialog.ts +214 -0
  163. package/src/modules/dialog/useDialogContainer.spec.ts +331 -0
  164. package/src/modules/dialog/useDialogContainer.ts +150 -0
  165. package/src/modules/dialog/useDialogSwipeInput.spec.ts +157 -0
  166. package/src/modules/dialog/useDialogSwipeInput.ts +319 -0
  167. package/src/modules/dialog/useDialogTransform.spec.ts +370 -0
  168. package/src/modules/dialog/useDialogTransform.ts +407 -0
  169. package/src/modules/drawer/types.ts +102 -0
  170. package/src/modules/drawer/useDrawerSwipeInput.spec.ts +566 -0
  171. package/src/modules/drawer/useDrawerSwipeInput.ts +399 -0
  172. package/src/modules/panels/rendering/ContentRegistry.spec.tsx +21 -14
  173. package/src/modules/pivot/SwipePivotContent.position.spec.tsx +12 -8
  174. package/src/modules/pivot/SwipePivotContent.spec.tsx +55 -25
  175. package/src/modules/pivot/SwipePivotContent.tsx +2 -2
  176. package/src/modules/pivot/SwipePivotTabBar.spec.tsx +85 -68
  177. package/src/modules/pivot/SwipePivotTabBar.tsx +75 -15
  178. package/src/modules/pivot/scaleInputState.spec.ts +11 -2
  179. package/src/modules/pivot/usePivot.spec.ts +17 -3
  180. package/src/modules/pivot/usePivotSwipeInput.spec.ts +182 -123
  181. package/src/modules/stack/SwipeStackContent.spec.tsx +387 -100
  182. package/src/modules/stack/SwipeStackContent.tsx +43 -33
  183. package/src/modules/stack/SwipeStackOutlet.spec.tsx +14 -16
  184. package/src/modules/stack/SwipeStackOutlet.tsx +6 -6
  185. package/src/modules/stack/computeSwipeStackTransform.spec.ts +5 -5
  186. package/src/modules/stack/computeSwipeStackTransform.ts +3 -3
  187. package/src/modules/stack/swipeTransitionContinuity.spec.tsx +1133 -0
  188. package/src/modules/stack/useStackAnimationState.spec.ts +3 -1
  189. package/src/modules/stack/useStackAnimationState.ts +18 -13
  190. package/src/modules/stack/useStackNavigation.spec.ts +198 -3
  191. package/src/modules/stack/useStackNavigation.tsx +113 -56
  192. package/src/modules/stack/useStackSwipeInput.spec.ts +65 -32
  193. package/src/modules/stack/useStackSwipeInput.ts +1 -1
  194. package/src/sticky-header/StickyArea.tsx +29 -57
  195. package/src/sticky-header/calculateStickyMetrics.spec.ts +105 -0
  196. package/src/sticky-header/calculateStickyMetrics.ts +50 -0
  197. package/src/types.ts +18 -0
  198. package/src/window/index.ts +2 -0
  199. package/dist/FloatingWindow-BpdOpg_L.js +0 -400
  200. package/dist/FloatingWindow-BpdOpg_L.js.map +0 -1
  201. package/dist/FloatingWindow-TCDNY5gE.cjs +0 -2
  202. package/dist/FloatingWindow-TCDNY5gE.cjs.map +0 -1
  203. package/dist/GridLayout-B4VRsC0r.cjs +0 -2
  204. package/dist/ResizeHandle-CScipO5l.cjs +0 -2
  205. package/dist/SwipePivotTabBar-BGO9X94m.js +0 -407
  206. package/dist/SwipePivotTabBar-BGO9X94m.js.map +0 -1
  207. package/dist/SwipePivotTabBar-BrQismcZ.cjs +0 -2
  208. package/dist/SwipePivotTabBar-BrQismcZ.cjs.map +0 -1
  209. package/dist/useDocumentPointerEvents-CKdhGXd0.js +0 -46
  210. package/dist/useDocumentPointerEvents-CKdhGXd0.js.map +0 -1
  211. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs +0 -2
  212. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs.map +0 -1
  213. package/dist/useEffectEvent-Dp7HLCf0.js +0 -13
  214. package/dist/useEffectEvent-Dp7HLCf0.js.map +0 -1
  215. package/dist/useEffectEvent-huSsGUnl.cjs +0 -2
  216. package/dist/useEffectEvent-huSsGUnl.cjs.map +0 -1
@@ -23,7 +23,7 @@ export type UseSwipeContentTransformOptions = {
23
23
  /** Current swipe displacement in pixels */
24
24
  displacement: number;
25
25
  /** Whether swipe gesture is active */
26
- isSwiping: boolean;
26
+ isOperating: boolean;
27
27
  /** Axis of transformation */
28
28
  axis?: GestureAxis;
29
29
  /** Duration of snap animation in ms */
@@ -42,6 +42,13 @@ export type UseSwipeContentTransformOptions = {
42
42
  * Use this for push animations where new panel comes from off-screen.
43
43
  */
44
44
  initialPx?: number;
45
+ /**
46
+ * Skip animation when targetPx changes.
47
+ * Use this when the target changed during an operation (from useOperationContinuity).
48
+ * When true, target changes will snap instead of animate.
49
+ * @default false
50
+ */
51
+ skipTargetChangeAnimation?: boolean;
45
52
  };
46
53
 
47
54
  /**
@@ -73,6 +80,119 @@ const getTransformFn = (axis: GestureAxis): "translateX" | "translateY" => {
73
80
  return axis === "horizontal" ? "translateX" : "translateY";
74
81
  };
75
82
 
83
+ /**
84
+ * Check if initial mount animation should be scheduled.
85
+ * Returns animation info if conditions are met, null otherwise.
86
+ *
87
+ * Conditions for scheduling:
88
+ * 1. Animation not already consumed
89
+ * 2. containerSize is valid (> 0) - handles React effect execution order
90
+ * 3. initialPx is provided
91
+ * 4. initialPx differs from targetPx
92
+ */
93
+ const computeInitialMountAnimation = (
94
+ hasConsumed: boolean,
95
+ containerSize: number | undefined,
96
+ initialPx: number | undefined,
97
+ targetPx: number,
98
+ ): { from: number; to: number } | null => {
99
+ if (hasConsumed) {
100
+ return null;
101
+ }
102
+ if (containerSize === undefined) {
103
+ return null;
104
+ }
105
+ if (containerSize <= 0) {
106
+ return null;
107
+ }
108
+ if (initialPx === undefined) {
109
+ return null;
110
+ }
111
+ if (initialPx === targetPx) {
112
+ return null;
113
+ }
114
+ return { from: initialPx, to: targetPx };
115
+ };
116
+
117
+ /**
118
+ * Result type for target change handling.
119
+ */
120
+ type TargetChangeResult =
121
+ | { type: "animate"; animation: { from: number; to: number } }
122
+ | { type: "snap"; position: number }
123
+ | { type: "none" };
124
+
125
+ /**
126
+ * Compute action for target position change when not swiping.
127
+ * Returns the appropriate action: animate, snap, or none.
128
+ *
129
+ * @param skipAnimation - If true, skip animation and snap directly.
130
+ * Use this when the target changed during an operation (from useOperationContinuity).
131
+ */
132
+ const computeTargetChangeAction = (
133
+ targetPx: number,
134
+ prevTargetPx: number,
135
+ currentPx: number,
136
+ isOperating: boolean,
137
+ isAnimating: boolean,
138
+ animateOnTargetChange: boolean,
139
+ skipAnimation: boolean,
140
+ ): TargetChangeResult => {
141
+ if (targetPx === prevTargetPx) {
142
+ return { type: "none" };
143
+ }
144
+ if (isOperating) {
145
+ return { type: "none" };
146
+ }
147
+ if (isAnimating) {
148
+ return { type: "none" };
149
+ }
150
+ if (!animateOnTargetChange) {
151
+ return { type: "snap", position: targetPx };
152
+ }
153
+
154
+ const distance = Math.abs(currentPx - targetPx);
155
+ if (distance <= 1) {
156
+ return { type: "snap", position: targetPx };
157
+ }
158
+
159
+ // Skip animation if requested (e.g., role changed during operation)
160
+ // This prevents unwanted animations after an operation ends.
161
+ // However, allow forward animations (currentPx < targetPx) for normal swipe-to-complete.
162
+ // Only skip backward animations (currentPx > targetPx) which occur during over-swipe.
163
+ if (skipAnimation) {
164
+ // Backward direction (over-swipe): snap, don't animate backward
165
+ if (currentPx > targetPx) {
166
+ return { type: "snap", position: targetPx };
167
+ }
168
+ // Forward direction (normal swipe-to-complete): animate from current position
169
+ return { type: "animate", animation: { from: currentPx, to: targetPx } };
170
+ }
171
+
172
+ return { type: "animate", animation: { from: currentPx, to: targetPx } };
173
+ };
174
+
175
+ /**
176
+ * Check if container size change requires position snap.
177
+ * Returns the new position to snap to, or null if no snap needed.
178
+ */
179
+ const computeContainerResizeSnap = (
180
+ containerSize: number | undefined,
181
+ prevContainerSize: number | undefined,
182
+ targetPx: number,
183
+ ): number | null => {
184
+ if (containerSize === undefined) {
185
+ return null;
186
+ }
187
+ if (containerSize === prevContainerSize) {
188
+ return null;
189
+ }
190
+ if (containerSize <= 0) {
191
+ return null;
192
+ }
193
+ return targetPx;
194
+ };
195
+
76
196
  /**
77
197
  * Hook for DOM-based swipe content transform.
78
198
  *
@@ -86,7 +206,7 @@ const getTransformFn = (axis: GestureAxis): "translateX" | "translateY" => {
86
206
  * elementRef: containerRef,
87
207
  * targetPx: 0,
88
208
  * displacement: inputState.displacement.x,
89
- * isSwiping: inputState.phase === "swiping",
209
+ * isOperating: inputState.phase === "swiping",
90
210
  * });
91
211
  * ```
92
212
  */
@@ -97,12 +217,13 @@ export function useSwipeContentTransform(
97
217
  elementRef,
98
218
  targetPx,
99
219
  displacement,
100
- isSwiping,
220
+ isOperating,
101
221
  axis = "horizontal",
102
222
  animationDuration = DEFAULT_ANIMATION_DURATION,
103
223
  containerSize,
104
224
  animateOnTargetChange = false,
105
225
  initialPx,
226
+ skipTargetChangeAnimation = false,
106
227
  } = options;
107
228
 
108
229
  // Use initialPx if provided, otherwise use targetPx
@@ -112,36 +233,47 @@ export function useSwipeContentTransform(
112
233
  const prevTargetPxRef = React.useRef<number>(targetPx);
113
234
  const prevContainerSizeRef = React.useRef<number | undefined>(containerSize);
114
235
  const pendingAnimationRef = React.useRef<{ from: number; to: number } | null>(null);
115
- const isFirstMountRef = React.useRef<boolean>(true);
236
+ // Track if initial mount animation has been consumed
237
+ const hasConsumedInitialMountRef = React.useRef<boolean>(false);
116
238
 
117
- // Schedule animation on first mount if initialPx differs from targetPx
118
- if (isFirstMountRef.current && initialPx !== undefined && initialPx !== targetPx) {
119
- pendingAnimationRef.current = { from: initialPx, to: targetPx };
120
- isFirstMountRef.current = false;
121
- } else if (isFirstMountRef.current) {
122
- isFirstMountRef.current = false;
239
+ // Schedule animation on first mount if initialPx differs from targetPx.
240
+ const initialMountAnimation = computeInitialMountAnimation(
241
+ hasConsumedInitialMountRef.current,
242
+ containerSize,
243
+ initialPx,
244
+ targetPx,
245
+ );
246
+ if (initialMountAnimation !== null) {
247
+ pendingAnimationRef.current = initialMountAnimation;
248
+ hasConsumedInitialMountRef.current = true;
123
249
  }
124
250
 
125
251
  // Handle target changes when not swiping
126
- if (targetPx !== prevTargetPxRef.current && !isSwiping && animRef.current === null) {
127
- if (animateOnTargetChange) {
128
- // Schedule animation from current position to new target
129
- const distance = Math.abs(currentPxRef.current - targetPx);
130
- if (distance > 1) {
131
- pendingAnimationRef.current = { from: currentPxRef.current, to: targetPx };
132
- } else {
133
- currentPxRef.current = targetPx;
134
- }
135
- } else {
136
- // Snap immediately (default behavior for resize, etc.)
137
- currentPxRef.current = targetPx;
138
- }
252
+ const targetChangeAction = computeTargetChangeAction(
253
+ targetPx,
254
+ prevTargetPxRef.current,
255
+ currentPxRef.current,
256
+ isOperating,
257
+ animRef.current !== null,
258
+ animateOnTargetChange,
259
+ skipTargetChangeAnimation,
260
+ );
261
+ if (targetChangeAction.type === "animate") {
262
+ pendingAnimationRef.current = targetChangeAction.animation;
263
+ prevTargetPxRef.current = targetPx;
264
+ } else if (targetChangeAction.type === "snap") {
265
+ currentPxRef.current = targetChangeAction.position;
139
266
  prevTargetPxRef.current = targetPx;
140
267
  }
141
268
 
142
269
  // Snap when container size changes (resize)
143
- if (containerSize !== undefined && containerSize !== prevContainerSizeRef.current && containerSize > 0) {
144
- currentPxRef.current = targetPx;
270
+ const resizeSnapPosition = computeContainerResizeSnap(
271
+ containerSize,
272
+ prevContainerSizeRef.current,
273
+ targetPx,
274
+ );
275
+ if (resizeSnapPosition !== null) {
276
+ currentPxRef.current = resizeSnapPosition;
145
277
  prevContainerSizeRef.current = containerSize;
146
278
  }
147
279
 
@@ -175,7 +307,7 @@ export function useSwipeContentTransform(
175
307
 
176
308
  // When swipe ends or target changes with animateOnTargetChange, animate to target
177
309
  React.useLayoutEffect(() => {
178
- if (isSwiping) {
310
+ if (isOperating) {
179
311
  cancel();
180
312
  animRef.current = null;
181
313
  pendingAnimationRef.current = null;
@@ -208,7 +340,7 @@ export function useSwipeContentTransform(
208
340
  currentPxRef.current = targetPx;
209
341
  prevTargetPxRef.current = targetPx;
210
342
  }
211
- }, [isSwiping, targetPx, start, cancel]);
343
+ }, [isOperating, targetPx, start, cancel]);
212
344
 
213
345
  // Direct DOM update during swipe
214
346
  React.useLayoutEffect(() => {
@@ -218,7 +350,13 @@ export function useSwipeContentTransform(
218
350
  }
219
351
 
220
352
  // Skip if animation is running, about to start, or pending
221
- if (isAnimating || animRef.current !== null || pendingAnimationRef.current !== null) {
353
+ if (isAnimating) {
354
+ return;
355
+ }
356
+ if (animRef.current !== null) {
357
+ return;
358
+ }
359
+ if (pendingAnimationRef.current !== null) {
222
360
  return;
223
361
  }
224
362
 
@@ -0,0 +1,387 @@
1
+ /**
2
+ * @file Tests for AlertDialog component
3
+ */
4
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react";
5
+ import { AlertDialog } from "./AlertDialog";
6
+
7
+ type CallTracker = {
8
+ calls: ReadonlyArray<ReadonlyArray<unknown>>;
9
+ fn: (...args: ReadonlyArray<unknown>) => void;
10
+ };
11
+
12
+ const createCallTracker = (): CallTracker => {
13
+ const calls: Array<ReadonlyArray<unknown>> = [];
14
+ const fn = (...args: ReadonlyArray<unknown>): void => {
15
+ calls.push(args);
16
+ };
17
+ return { calls, fn };
18
+ };
19
+
20
+ describe("AlertDialog", () => {
21
+ const originalShowModal = HTMLDialogElement.prototype.showModal;
22
+ const originalClose = HTMLDialogElement.prototype.close;
23
+
24
+ beforeEach(() => {
25
+ HTMLDialogElement.prototype.showModal = function (this: HTMLDialogElement) {
26
+ this.setAttribute("open", "");
27
+ };
28
+ HTMLDialogElement.prototype.close = function (this: HTMLDialogElement) {
29
+ this.removeAttribute("open");
30
+ };
31
+ });
32
+
33
+ afterEach(() => {
34
+ HTMLDialogElement.prototype.showModal = originalShowModal;
35
+ HTMLDialogElement.prototype.close = originalClose;
36
+ document.body.style.overflow = "";
37
+ document.body.style.paddingRight = "";
38
+ });
39
+
40
+ describe("alert type", () => {
41
+ it("should render message", () => {
42
+ render(
43
+ <AlertDialog
44
+ type="alert"
45
+ visible={true}
46
+ message="This is an alert message"
47
+ onConfirm={() => {}}
48
+ onCancel={() => {}}
49
+ />,
50
+ );
51
+
52
+ expect(screen.getByText("This is an alert message")).toBeInTheDocument();
53
+ });
54
+
55
+ it("should render title when provided", () => {
56
+ render(
57
+ <AlertDialog
58
+ type="alert"
59
+ visible={true}
60
+ title="Alert Title"
61
+ message="Message"
62
+ onConfirm={() => {}}
63
+ onCancel={() => {}}
64
+ />,
65
+ );
66
+
67
+ expect(screen.getByText("Alert Title")).toBeInTheDocument();
68
+ });
69
+
70
+ it("should only show OK button", () => {
71
+ render(
72
+ <AlertDialog type="alert" visible={true} message="Message" onConfirm={() => {}} onCancel={() => {}} />,
73
+ );
74
+
75
+ expect(screen.getByRole("button", { name: "OK" })).toBeInTheDocument();
76
+ expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument();
77
+ });
78
+
79
+ it("should call onConfirm when OK is clicked", () => {
80
+ const onConfirm = createCallTracker();
81
+ render(
82
+ <AlertDialog type="alert" visible={true} message="Message" onConfirm={onConfirm.fn} onCancel={() => {}} />,
83
+ );
84
+
85
+ fireEvent.click(screen.getByRole("button", { name: "OK" }));
86
+
87
+ expect(onConfirm.calls).toHaveLength(1);
88
+ });
89
+
90
+ it("should use custom confirmLabel", () => {
91
+ render(
92
+ <AlertDialog
93
+ type="alert"
94
+ visible={true}
95
+ message="Message"
96
+ confirmLabel="Got it"
97
+ onConfirm={() => {}}
98
+ onCancel={() => {}}
99
+ />,
100
+ );
101
+
102
+ expect(screen.getByRole("button", { name: "Got it" })).toBeInTheDocument();
103
+ });
104
+ });
105
+
106
+ describe("confirm type", () => {
107
+ it("should show OK and Cancel buttons", () => {
108
+ render(
109
+ <AlertDialog type="confirm" visible={true} message="Are you sure?" onConfirm={() => {}} onCancel={() => {}} />,
110
+ );
111
+
112
+ expect(screen.getByRole("button", { name: "OK" })).toBeInTheDocument();
113
+ expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
114
+ });
115
+
116
+ it("should call onConfirm when OK is clicked", () => {
117
+ const onConfirm = createCallTracker();
118
+ render(
119
+ <AlertDialog type="confirm" visible={true} message="Are you sure?" onConfirm={onConfirm.fn} onCancel={() => {}} />,
120
+ );
121
+
122
+ fireEvent.click(screen.getByRole("button", { name: "OK" }));
123
+
124
+ expect(onConfirm.calls).toHaveLength(1);
125
+ });
126
+
127
+ it("should call onCancel when Cancel is clicked", () => {
128
+ const onCancel = createCallTracker();
129
+ render(
130
+ <AlertDialog type="confirm" visible={true} message="Are you sure?" onConfirm={() => {}} onCancel={onCancel.fn} />,
131
+ );
132
+
133
+ fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
134
+
135
+ expect(onCancel.calls).toHaveLength(1);
136
+ });
137
+
138
+ it("should call onCancel when backdrop is clicked", () => {
139
+ const onCancel = createCallTracker();
140
+ render(
141
+ <AlertDialog
142
+ type="confirm"
143
+ visible={true}
144
+ message="Are you sure?"
145
+ onConfirm={() => {}}
146
+ onCancel={onCancel.fn}
147
+ />,
148
+ );
149
+
150
+ const dialog = document.querySelector("dialog");
151
+ fireEvent.click(dialog!);
152
+
153
+ expect(onCancel.calls).toHaveLength(1);
154
+ });
155
+
156
+ it("should use custom button labels", () => {
157
+ render(
158
+ <AlertDialog
159
+ type="confirm"
160
+ visible={true}
161
+ message="Delete?"
162
+ confirmLabel="Delete"
163
+ cancelLabel="Keep"
164
+ onConfirm={() => {}}
165
+ onCancel={() => {}}
166
+ />,
167
+ );
168
+
169
+ expect(screen.getByRole("button", { name: "Delete" })).toBeInTheDocument();
170
+ expect(screen.getByRole("button", { name: "Keep" })).toBeInTheDocument();
171
+ });
172
+ });
173
+
174
+ describe("prompt type", () => {
175
+ it("should show input field", () => {
176
+ render(
177
+ <AlertDialog
178
+ type="prompt"
179
+ visible={true}
180
+ message="Enter your name:"
181
+ onConfirm={() => {}}
182
+ onCancel={() => {}}
183
+ />,
184
+ );
185
+
186
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
187
+ });
188
+
189
+ it("should show placeholder", () => {
190
+ render(
191
+ <AlertDialog
192
+ type="prompt"
193
+ visible={true}
194
+ message="Enter your name:"
195
+ placeholder="John Doe"
196
+ onConfirm={() => {}}
197
+ onCancel={() => {}}
198
+ />,
199
+ );
200
+
201
+ expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "John Doe");
202
+ });
203
+
204
+ it("should show default value", () => {
205
+ render(
206
+ <AlertDialog
207
+ type="prompt"
208
+ visible={true}
209
+ message="Enter your name:"
210
+ defaultValue="Initial Value"
211
+ onConfirm={() => {}}
212
+ onCancel={() => {}}
213
+ />,
214
+ );
215
+
216
+ expect(screen.getByRole("textbox")).toHaveValue("Initial Value");
217
+ });
218
+
219
+ it("should update input value on change", () => {
220
+ render(
221
+ <AlertDialog
222
+ type="prompt"
223
+ visible={true}
224
+ message="Enter your name:"
225
+ onConfirm={() => {}}
226
+ onCancel={() => {}}
227
+ />,
228
+ );
229
+
230
+ const input = screen.getByRole("textbox") as HTMLInputElement;
231
+ fireEvent.change(input, { target: { value: "Test Value" } });
232
+
233
+ expect(input.value).toBe("Test Value");
234
+ });
235
+
236
+ it("should call onConfirm with input value when OK is clicked", () => {
237
+ const onConfirm = createCallTracker();
238
+ render(
239
+ <AlertDialog
240
+ type="prompt"
241
+ visible={true}
242
+ message="Enter your name:"
243
+ defaultValue="Hello"
244
+ onConfirm={onConfirm.fn}
245
+ onCancel={() => {}}
246
+ />,
247
+ );
248
+
249
+ fireEvent.click(screen.getByRole("button", { name: "OK" }));
250
+
251
+ expect(onConfirm.calls).toHaveLength(1);
252
+ expect(onConfirm.calls[0]?.[0]).toBe("Hello");
253
+ });
254
+
255
+ it("should call onConfirm with changed input value", () => {
256
+ const onConfirm = createCallTracker();
257
+ render(
258
+ <AlertDialog
259
+ type="prompt"
260
+ visible={true}
261
+ message="Enter your name:"
262
+ onConfirm={onConfirm.fn}
263
+ onCancel={() => {}}
264
+ />,
265
+ );
266
+
267
+ const input = screen.getByRole("textbox");
268
+ fireEvent.change(input, { target: { value: "Changed Value" } });
269
+ fireEvent.click(screen.getByRole("button", { name: "OK" }));
270
+
271
+ expect(onConfirm.calls).toHaveLength(1);
272
+ expect(onConfirm.calls[0]?.[0]).toBe("Changed Value");
273
+ });
274
+
275
+ it("should call onConfirm when Enter is pressed in input", () => {
276
+ const onConfirm = createCallTracker();
277
+ render(
278
+ <AlertDialog
279
+ type="prompt"
280
+ visible={true}
281
+ message="Enter your name:"
282
+ defaultValue="Test"
283
+ onConfirm={onConfirm.fn}
284
+ onCancel={() => {}}
285
+ />,
286
+ );
287
+
288
+ const input = screen.getByRole("textbox");
289
+ fireEvent.keyDown(input, { key: "Enter" });
290
+
291
+ expect(onConfirm.calls).toHaveLength(1);
292
+ expect(onConfirm.calls[0]?.[0]).toBe("Test");
293
+ });
294
+
295
+ it("should call onCancel when Cancel is clicked", () => {
296
+ const onCancel = createCallTracker();
297
+ render(
298
+ <AlertDialog
299
+ type="prompt"
300
+ visible={true}
301
+ message="Enter your name:"
302
+ onConfirm={() => {}}
303
+ onCancel={onCancel.fn}
304
+ />,
305
+ );
306
+
307
+ fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
308
+
309
+ expect(onCancel.calls).toHaveLength(1);
310
+ });
311
+
312
+ it("should support password input type", () => {
313
+ render(
314
+ <AlertDialog
315
+ type="prompt"
316
+ visible={true}
317
+ message="Enter password:"
318
+ inputType="password"
319
+ onConfirm={() => {}}
320
+ onCancel={() => {}}
321
+ />,
322
+ );
323
+
324
+ const input = screen.getByLabelText("Input");
325
+ expect(input).toHaveAttribute("type", "password");
326
+ });
327
+
328
+ it("should focus input when dialog opens", async () => {
329
+ render(
330
+ <AlertDialog
331
+ type="prompt"
332
+ visible={true}
333
+ message="Enter your name:"
334
+ onConfirm={() => {}}
335
+ onCancel={() => {}}
336
+ />,
337
+ );
338
+
339
+ const input = screen.getByRole("textbox");
340
+ await waitFor(() => {
341
+ expect(document.activeElement).toBe(input);
342
+ });
343
+ });
344
+
345
+ it("should reset input value when dialog reopens", () => {
346
+ const { rerender } = render(
347
+ <AlertDialog
348
+ type="prompt"
349
+ visible={true}
350
+ message="Enter your name:"
351
+ defaultValue="Initial"
352
+ onConfirm={() => {}}
353
+ onCancel={() => {}}
354
+ />,
355
+ );
356
+
357
+ const input = screen.getByRole("textbox") as HTMLInputElement;
358
+ fireEvent.change(input, { target: { value: "Changed" } });
359
+ expect(input.value).toBe("Changed");
360
+
361
+ // Close and reopen
362
+ rerender(
363
+ <AlertDialog
364
+ type="prompt"
365
+ visible={false}
366
+ message="Enter your name:"
367
+ defaultValue="Initial"
368
+ onConfirm={() => {}}
369
+ onCancel={() => {}}
370
+ />,
371
+ );
372
+
373
+ rerender(
374
+ <AlertDialog
375
+ type="prompt"
376
+ visible={true}
377
+ message="Enter your name:"
378
+ defaultValue="Initial"
379
+ onConfirm={() => {}}
380
+ onCancel={() => {}}
381
+ />,
382
+ );
383
+
384
+ expect(input.value).toBe("Initial");
385
+ });
386
+ });
387
+ });