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
@@ -0,0 +1,399 @@
1
+ /**
2
+ * @file Hook for detecting swipe gestures to open/close a drawer.
3
+ *
4
+ * Combines:
5
+ * - Edge swipe detection for opening (Stack pattern)
6
+ * - Drag-to-close within drawer (Dialog pattern)
7
+ * - Native gesture guard for browser back prevention
8
+ */
9
+ import * as React from "react";
10
+ import { useEdgeSwipeInput } from "../../hooks/gesture/useEdgeSwipeInput.js";
11
+ import { useNativeGestureGuard } from "../../hooks/gesture/useNativeGestureGuard.js";
12
+ import { usePointerTracking } from "../../hooks/gesture/usePointerTracking.js";
13
+ import {
14
+ mergeGestureContainerProps,
15
+ isScrollableInDirection,
16
+ } from "../../hooks/gesture/utils.js";
17
+ import { isInSwipeSafeZone } from "../../components/gesture/SwipeSafeZone.js";
18
+ import {
19
+ type ContinuousOperationState,
20
+ IDLE_CONTINUOUS_OPERATION_STATE,
21
+ } from "../../hooks/gesture/types.js";
22
+ import type { UseDrawerSwipeInputOptions, UseDrawerSwipeInputResult } from "./types.js";
23
+ import { getDrawerAnimationAxis, getDrawerCloseSwipeSign, getDrawerOpenSwipeSign } from "./types.js";
24
+
25
+ /**
26
+ * Default dismiss threshold (30% of container size).
27
+ */
28
+ const DEFAULT_DISMISS_THRESHOLD = 0.3;
29
+
30
+ /**
31
+ * Velocity threshold for quick flick dismissal (px/ms).
32
+ */
33
+ const VELOCITY_THRESHOLD = 0.5;
34
+
35
+ // ============================================================================
36
+ // Helper functions (extracted to avoid ternary violations)
37
+ // ============================================================================
38
+
39
+ function getContainerSize(container: HTMLElement, axis: "x" | "y"): number {
40
+ if (axis === "x") {
41
+ return container.clientWidth;
42
+ }
43
+ return container.clientHeight;
44
+ }
45
+
46
+ function getAxisDelta(
47
+ start: { x: number; y: number },
48
+ current: { x: number; y: number },
49
+ axis: "x" | "y",
50
+ ): number {
51
+ if (axis === "x") {
52
+ return current.x - start.x;
53
+ }
54
+ return current.y - start.y;
55
+ }
56
+
57
+ const PHASE_MAP: Record<string, "idle" | "operating" | "ended"> = {
58
+ idle: "idle",
59
+ ended: "ended",
60
+ };
61
+
62
+ function normalizePhase(phase: string): "idle" | "operating" | "ended" {
63
+ return PHASE_MAP[phase] ?? "operating";
64
+ }
65
+
66
+ function computeDisplacementValue(
67
+ closeSwipeSign: 1 | -1,
68
+ axis: "x" | "y",
69
+ closeDisplacement: number,
70
+ ): { x: number; y: number } {
71
+ const signedDisplacement = closeSwipeSign * closeDisplacement;
72
+ if (axis === "x") {
73
+ return { x: signedDisplacement, y: 0 };
74
+ }
75
+ return { x: 0, y: signedDisplacement };
76
+ }
77
+
78
+ function computeAxisDisplacement(
79
+ displacement: { x: number; y: number },
80
+ axis: "x" | "y",
81
+ ): number {
82
+ if (axis === "x") {
83
+ return Math.abs(displacement.x);
84
+ }
85
+ return Math.abs(displacement.y);
86
+ }
87
+
88
+ function isEdgeSwipeEnabled(enableEdgeSwipeOpen: boolean, isOpen: boolean): boolean {
89
+ if (!enableEdgeSwipeOpen) {
90
+ return false;
91
+ }
92
+ return !isOpen;
93
+ }
94
+
95
+ function isCloseSwipeEnabled(enableSwipeClose: boolean, isOpen: boolean): boolean {
96
+ if (!enableSwipeClose) {
97
+ return false;
98
+ }
99
+ return isOpen;
100
+ }
101
+
102
+ function isDrawerOpening(isEdgeGesture: boolean, isOpen: boolean): boolean {
103
+ if (!isEdgeGesture) {
104
+ return false;
105
+ }
106
+ return !isOpen;
107
+ }
108
+
109
+ function isDrawerClosing(closePhase: "idle" | "operating" | "ended", isOpen: boolean): boolean {
110
+ if (closePhase === "idle") {
111
+ return false;
112
+ }
113
+ return isOpen;
114
+ }
115
+
116
+ function computeVelocity(
117
+ start: { x: number; y: number; timestamp: number } | null,
118
+ current: { x: number; y: number; timestamp: number } | null,
119
+ displacement: number,
120
+ ): number {
121
+ if (!start || !current) {
122
+ return 0;
123
+ }
124
+ const timeDelta = Math.max(1, current.timestamp - start.timestamp);
125
+ return displacement / timeDelta;
126
+ }
127
+
128
+ /**
129
+ * Hook for detecting swipe gestures to open/close a drawer.
130
+ *
131
+ * When drawer is closed:
132
+ * - Detects edge swipe from the anchor edge to trigger open
133
+ *
134
+ * When drawer is open:
135
+ * - Detects drag gesture within drawer to trigger close
136
+ * - Respects scrollable content boundaries
137
+ *
138
+ * @example
139
+ * ```tsx
140
+ * const { state, edgeContainerProps, drawerContentProps } = useDrawerSwipeInput({
141
+ * edgeContainerRef: gridLayoutRef,
142
+ * drawerContentRef: drawerRef,
143
+ * direction: "left",
144
+ * isOpen,
145
+ * onSwipeOpen: () => setOpen(true),
146
+ * onSwipeClose: () => setOpen(false),
147
+ * });
148
+ * ```
149
+ */
150
+ export function useDrawerSwipeInput(
151
+ options: UseDrawerSwipeInputOptions,
152
+ ): UseDrawerSwipeInputResult {
153
+ const {
154
+ edgeContainerRef,
155
+ drawerContentRef,
156
+ direction,
157
+ isOpen,
158
+ onSwipeOpen,
159
+ onSwipeClose,
160
+ enableEdgeSwipeOpen = true,
161
+ enableSwipeClose = true,
162
+ edgeWidth = 20,
163
+ dismissThreshold = DEFAULT_DISMISS_THRESHOLD,
164
+ } = options;
165
+
166
+ const axis = getDrawerAnimationAxis(direction);
167
+ const closeSwipeSign = getDrawerCloseSwipeSign(direction);
168
+ const openSwipeSign = getDrawerOpenSwipeSign(direction);
169
+
170
+ // Track container size for progress calculation
171
+ const containerSizeRef = React.useRef(0);
172
+
173
+ // Measure drawer content size
174
+ React.useLayoutEffect(() => {
175
+ const container = drawerContentRef.current;
176
+ if (!container) {
177
+ return;
178
+ }
179
+
180
+ const updateSize = () => {
181
+ containerSizeRef.current = getContainerSize(container, axis);
182
+ };
183
+
184
+ updateSize();
185
+
186
+ const observer = new ResizeObserver(updateSize);
187
+ observer.observe(container);
188
+
189
+ return () => observer.disconnect();
190
+ }, [drawerContentRef, axis]);
191
+
192
+ // =========== Edge swipe to OPEN ===========
193
+ const handleOpenSwipeEnd = React.useCallback(
194
+ (state: { direction: 1 | -1 | 0 }) => {
195
+ // Open when swiping in the correct direction (away from edge)
196
+ if (state.direction === openSwipeSign) {
197
+ onSwipeOpen();
198
+ }
199
+ },
200
+ [openSwipeSign, onSwipeOpen],
201
+ );
202
+
203
+ const {
204
+ isEdgeGesture,
205
+ state: edgeSwipeState,
206
+ containerProps: edgeSwipeProps,
207
+ } = useEdgeSwipeInput({
208
+ containerRef: edgeContainerRef,
209
+ edge: direction,
210
+ edgeWidth,
211
+ enabled: isEdgeSwipeEnabled(enableEdgeSwipeOpen, isOpen),
212
+ onSwipeEnd: handleOpenSwipeEnd,
213
+ });
214
+
215
+ // Native gesture guard for edge swipe
216
+ const { containerProps: guardProps } = useNativeGestureGuard({
217
+ containerRef: edgeContainerRef,
218
+ active: isEdgeGesture,
219
+ preventEdgeBack: true,
220
+ preventOverscroll: true,
221
+ edgeWidth,
222
+ });
223
+
224
+ // =========== Drag to CLOSE (Dialog pattern) ===========
225
+ const { state: closeTracking, onPointerDown: baseClosePointerDown } = usePointerTracking({
226
+ enabled: isCloseSwipeEnabled(enableSwipeClose, isOpen),
227
+ });
228
+
229
+ const [closePhase, setClosePhase] = React.useState<"idle" | "operating" | "ended">("idle");
230
+ const lastCloseDisplacementRef = React.useRef(0);
231
+
232
+ // Wrap pointer down with scroll check and safe zone check
233
+ const onClosePointerDown = React.useCallback(
234
+ (event: React.PointerEvent) => {
235
+ if (!enableSwipeClose || !isOpen) {
236
+ return;
237
+ }
238
+
239
+ const container = drawerContentRef.current;
240
+ if (!container) {
241
+ return;
242
+ }
243
+
244
+ const target = event.target as HTMLElement;
245
+
246
+ // Check if target is in a SwipeSafeZone
247
+ if (isInSwipeSafeZone(target, container)) {
248
+ return; // Don't start close swipe if inside safe zone
249
+ }
250
+
251
+ // Check if target is in a scrollable area that would block swipe
252
+ if (isScrollableInDirection(target, container, axis, closeSwipeSign)) {
253
+ return; // Don't start close swipe if inside scrollable content
254
+ }
255
+
256
+ baseClosePointerDown(event);
257
+ },
258
+ [enableSwipeClose, isOpen, drawerContentRef, axis, closeSwipeSign, baseClosePointerDown],
259
+ );
260
+
261
+ // Calculate close displacement
262
+ const closeDisplacement = React.useMemo(() => {
263
+ if (!closeTracking.isDown || !closeTracking.start || !closeTracking.current) {
264
+ return lastCloseDisplacementRef.current;
265
+ }
266
+
267
+ const delta = getAxisDelta(closeTracking.start, closeTracking.current, axis);
268
+
269
+ // Only count movement in close direction
270
+ const signedDelta = delta * closeSwipeSign;
271
+ return Math.max(0, signedDelta);
272
+ }, [closeTracking.isDown, closeTracking.start, closeTracking.current, axis, closeSwipeSign]);
273
+
274
+ // Track displacement while dragging
275
+ React.useEffect(() => {
276
+ if (closeTracking.isDown && closeTracking.current) {
277
+ lastCloseDisplacementRef.current = closeDisplacement;
278
+ }
279
+ }, [closeTracking.isDown, closeTracking.current, closeDisplacement]);
280
+
281
+ // Handle close drag start
282
+ React.useEffect(() => {
283
+ if (closeTracking.isDown && closePhase === "idle") {
284
+ setClosePhase("operating");
285
+ }
286
+ }, [closeTracking.isDown, closePhase]);
287
+
288
+ // Handle close drag end
289
+ React.useEffect(() => {
290
+ if (!closeTracking.isDown && closePhase === "operating") {
291
+ const displacement = lastCloseDisplacementRef.current;
292
+ const hasMovement = displacement > 1;
293
+
294
+ if (hasMovement) {
295
+ setClosePhase("ended");
296
+
297
+ // Check if should close
298
+ const containerSize = containerSizeRef.current;
299
+ if (containerSize > 0) {
300
+ const ratio = displacement / containerSize;
301
+ const velocity = computeVelocity(closeTracking.start, closeTracking.current, displacement);
302
+
303
+ if (ratio >= dismissThreshold || velocity >= VELOCITY_THRESHOLD) {
304
+ onSwipeClose();
305
+ }
306
+ }
307
+ } else {
308
+ setClosePhase("idle");
309
+ lastCloseDisplacementRef.current = 0;
310
+ }
311
+ }
312
+ }, [closeTracking.isDown, closePhase, dismissThreshold, onSwipeClose, closeTracking.start, closeTracking.current]);
313
+
314
+ // Transition from ended to idle
315
+ React.useEffect(() => {
316
+ if (closePhase === "ended") {
317
+ queueMicrotask(() => {
318
+ setClosePhase("idle");
319
+ lastCloseDisplacementRef.current = 0;
320
+ });
321
+ }
322
+ }, [closePhase]);
323
+
324
+ // Reset close state when drawer closes
325
+ React.useEffect(() => {
326
+ if (!isOpen) {
327
+ setClosePhase("idle");
328
+ lastCloseDisplacementRef.current = 0;
329
+ }
330
+ }, [isOpen]);
331
+
332
+ // =========== Combined state ===========
333
+ const isOpening = isDrawerOpening(isEdgeGesture, isOpen);
334
+ const isClosing = isDrawerClosing(closePhase, isOpen);
335
+
336
+ // Determine primary displacement based on current operation
337
+ const displacement = React.useMemo(() => {
338
+ if (isOpening) {
339
+ return computeAxisDisplacement(edgeSwipeState.displacement, axis);
340
+ }
341
+ if (isClosing) {
342
+ return closeDisplacement;
343
+ }
344
+ return 0;
345
+ }, [isOpening, isClosing, axis, edgeSwipeState.displacement, closeDisplacement]);
346
+
347
+ // Progress calculation
348
+ const progress = React.useMemo(() => {
349
+ const containerSize = containerSizeRef.current;
350
+ if (containerSize <= 0) {
351
+ return 0;
352
+ }
353
+ return Math.min(displacement / containerSize, 1);
354
+ }, [displacement]);
355
+
356
+ // Combined operation state
357
+ const state = React.useMemo<ContinuousOperationState>(() => {
358
+ if (isOpening) {
359
+ return {
360
+ phase: normalizePhase(edgeSwipeState.phase),
361
+ displacement: edgeSwipeState.displacement,
362
+ velocity: edgeSwipeState.velocity,
363
+ };
364
+ }
365
+ if (isClosing) {
366
+ return {
367
+ phase: closePhase,
368
+ displacement: computeDisplacementValue(closeSwipeSign, axis, closeDisplacement),
369
+ velocity: { x: 0, y: 0 },
370
+ };
371
+ }
372
+ return IDLE_CONTINUOUS_OPERATION_STATE;
373
+ }, [isOpening, isClosing, edgeSwipeState, closePhase, closeDisplacement, axis, closeSwipeSign]);
374
+
375
+ // Container props
376
+ const edgeContainerProps = React.useMemo(
377
+ () => mergeGestureContainerProps(edgeSwipeProps, guardProps),
378
+ [edgeSwipeProps, guardProps],
379
+ );
380
+
381
+ const drawerContentProps = React.useMemo(() => ({
382
+ onPointerDown: onClosePointerDown,
383
+ style: {
384
+ touchAction: "none" as const,
385
+ userSelect: "none" as const,
386
+ WebkitUserSelect: "none" as const,
387
+ },
388
+ }), [onClosePointerDown]);
389
+
390
+ return {
391
+ state,
392
+ isOpening,
393
+ isClosing,
394
+ progress,
395
+ displacement,
396
+ edgeContainerProps,
397
+ drawerContentProps,
398
+ };
399
+ }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * @file ContentRegistry tests - state persistence across tab switch, panel move, and split
3
3
  */
4
- /* eslint-disable no-restricted-imports, no-restricted-properties, no-restricted-syntax -- integration test */
5
4
  import { render, screen, fireEvent } from "@testing-library/react";
6
5
  import * as React from "react";
7
6
  import { ContentRegistryProvider, useContentRegistry } from "./ContentRegistry";
@@ -49,6 +48,21 @@ const TestHarness: React.FC<{ state: TestState }> = ({ state }) => {
49
48
  };
50
49
 
51
50
  describe("ContentRegistry", () => {
51
+ const defaultRect = {
52
+ top: 0,
53
+ left: 0,
54
+ width: 100,
55
+ height: 100,
56
+ right: 100,
57
+ bottom: 100,
58
+ x: 0,
59
+ y: 0,
60
+ toJSON: () => ({}),
61
+ } as DOMRect;
62
+ const originalPointerCapture = Element.prototype.setPointerCapture;
63
+ const originalReleasePointerCapture = Element.prototype.releasePointerCapture;
64
+ const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
65
+
52
66
  const createPanel = (id: string): TabDefinition => ({
53
67
  id,
54
68
  title: `Panel ${id}`,
@@ -59,25 +73,18 @@ describe("ContentRegistry", () => {
59
73
  // Mock ResizeObserver (polyfill provided in vitest.setup.ts)
60
74
 
61
75
  // Mock pointer capture methods
62
- Element.prototype.setPointerCapture = vi.fn();
63
- Element.prototype.releasePointerCapture = vi.fn();
76
+ Element.prototype.setPointerCapture = () => {};
77
+ Element.prototype.releasePointerCapture = () => {};
64
78
  // Mock getBoundingClientRect
65
- Element.prototype.getBoundingClientRect = vi.fn().mockReturnValue({
66
- top: 0,
67
- left: 0,
68
- width: 100,
69
- height: 100,
70
- right: 100,
71
- bottom: 100,
72
- x: 0,
73
- y: 0,
74
- toJSON: () => ({}),
75
- });
79
+ Element.prototype.getBoundingClientRect = () => defaultRect;
76
80
  });
77
81
 
78
82
  afterEach(() => {
79
83
  // Clean up any portal containers
80
84
  document.querySelectorAll("[data-panel-content-root]").forEach((el) => el.remove());
85
+ Element.prototype.setPointerCapture = originalPointerCapture;
86
+ Element.prototype.releasePointerCapture = originalReleasePointerCapture;
87
+ Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
81
88
  });
82
89
 
83
90
  it("should render content inside the registered container element", () => {
@@ -31,8 +31,12 @@ const shouldRenderItem = (offset: number): boolean => {
31
31
  * Helper to convert offset to display position
32
32
  */
33
33
  const toDisplayPosition = (offset: number): -1 | 0 | 1 => {
34
- if (offset < 0) return -1;
35
- if (offset > 0) return 1;
34
+ if (offset < 0) {
35
+ return -1;
36
+ }
37
+ if (offset > 0) {
38
+ return 1;
39
+ }
36
40
  return 0;
37
41
  };
38
42
 
@@ -76,9 +80,9 @@ describe("SwipePivotContent position handling", () => {
76
80
  const activeIndex = 0;
77
81
 
78
82
  // Filter to only adjacent items BEFORE rendering
79
- const itemsToRender = items.filter(item =>
80
- shouldRenderItem(getPositionOffset(item.index, activeIndex))
81
- );
83
+ const itemsToRender = items.filter((item) => {
84
+ return shouldRenderItem(getPositionOffset(item.index, activeIndex));
85
+ });
82
86
 
83
87
  render(
84
88
  <>
@@ -114,9 +118,9 @@ describe("SwipePivotContent position handling", () => {
114
118
  ];
115
119
  const activeIndex = 1; // On Page 2
116
120
 
117
- const itemsToRender = items.filter(item =>
118
- shouldRenderItem(getPositionOffset(item.index, activeIndex))
119
- );
121
+ const itemsToRender = items.filter((item) => {
122
+ return shouldRenderItem(getPositionOffset(item.index, activeIndex));
123
+ });
120
124
 
121
125
  render(
122
126
  <>
@@ -6,41 +6,71 @@ import * as React from "react";
6
6
  import { SwipePivotContent } from "./SwipePivotContent.js";
7
7
  import type { SwipeInputState } from "../../hooks/gesture/types.js";
8
8
 
9
- // Mock Web Animations API for JSDOM
10
- class MockAnimation {
11
- finished: Promise<Animation>;
12
- private resolveFinished!: () => void;
13
- playState: AnimationPlayState = "running";
14
-
15
- constructor() {
16
- this.finished = new Promise((resolve) => {
17
- this.resolveFinished = () => resolve(this as unknown as Animation);
18
- });
19
- }
9
+ /**
10
+ * Mock Animation that implements the full Animation interface.
11
+ * Used to polyfill Web Animations API for JSDOM testing.
12
+ */
13
+ const createMockAnimation = (): Animation => {
14
+ const animation = {} as Animation;
15
+ const animationState = {
16
+ resolveFinished: (value: Animation): void => {
17
+ void value;
18
+ },
19
+ };
20
20
 
21
- cancel() {
22
- this.playState = "idle";
23
- }
21
+ animation.currentTime = 0;
22
+ animation.effect = null;
23
+ animation.id = "";
24
+ animation.oncancel = null;
25
+ animation.onfinish = null;
26
+ animation.onremove = null;
27
+ animation.pending = false;
28
+ animation.playState = "running";
29
+ animation.playbackRate = 1;
30
+ animation.replaceState = "active";
31
+ animation.startTime = 0;
32
+ animation.timeline = null;
33
+ animation.finished = new Promise((resolve) => {
34
+ animationState.resolveFinished = resolve;
35
+ });
36
+ animation.ready = Promise.resolve(animation);
37
+ animation.cancel = () => {
38
+ animation.playState = "idle";
39
+ };
40
+ animation.finish = () => {
41
+ animation.playState = "finished";
42
+ animationState.resolveFinished(animation);
43
+ };
44
+ animation.commitStyles = () => {};
45
+ animation.pause = () => {};
46
+ animation.persist = () => {};
47
+ animation.play = () => {};
48
+ animation.reverse = () => {};
49
+ animation.updatePlaybackRate = () => {};
24
50
 
25
- finish() {
26
- this.playState = "finished";
27
- this.resolveFinished();
28
- }
29
- }
51
+ // EventTarget methods
52
+ animation.addEventListener = () => {};
53
+ animation.removeEventListener = () => {};
54
+ animation.dispatchEvent = () => true;
55
+
56
+ return animation;
57
+ };
30
58
 
31
59
  describe("SwipePivotContent", () => {
32
- let originalAnimate: typeof Element.prototype.animate | undefined;
60
+ const animationState = {
61
+ originalAnimate: Element.prototype.animate as typeof Element.prototype.animate | undefined,
62
+ };
33
63
 
34
64
  beforeAll(() => {
35
- originalAnimate = Element.prototype.animate;
36
- Element.prototype.animate = function () {
37
- return new MockAnimation() as unknown as Animation;
65
+ animationState.originalAnimate = Element.prototype.animate;
66
+ Element.prototype.animate = (): Animation => {
67
+ return createMockAnimation();
38
68
  };
39
69
  });
40
70
 
41
71
  afterAll(() => {
42
- if (originalAnimate) {
43
- Element.prototype.animate = originalAnimate;
72
+ if (animationState.originalAnimate) {
73
+ Element.prototype.animate = animationState.originalAnimate;
44
74
  }
45
75
  });
46
76
  const idleState: SwipeInputState = {
@@ -140,14 +140,14 @@ export const SwipePivotContent: React.FC<SwipePivotContentProps> = React.memo(({
140
140
 
141
141
  const targetPx = position * containerSize;
142
142
  const displacement = getAxisDisplacement(inputState, axis);
143
- const isSwiping = inputState.phase === "swiping" || inputState.phase === "tracking";
143
+ const isOperating = inputState.phase === "swiping" || inputState.phase === "tracking";
144
144
 
145
145
  // Use shared transform hook for DOM manipulation
146
146
  const { animationDirection } = useSwipeContentTransform({
147
147
  elementRef,
148
148
  targetPx,
149
149
  displacement,
150
- isSwiping,
150
+ isOperating,
151
151
  axis,
152
152
  animationDuration,
153
153
  containerSize,