react-panel-layout 0.6.0 → 0.7.0

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 (258) 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-Bw2djgpz.js +1542 -0
  4. package/dist/FloatingWindow-Bw2djgpz.js.map +1 -0
  5. package/dist/FloatingWindow-Cvyokf0m.cjs +2 -0
  6. package/dist/FloatingWindow-Cvyokf0m.cjs.map +1 -0
  7. package/dist/GridLayout-B4aCqSyd.js +947 -0
  8. package/dist/{GridLayout-BltqeCPK.js.map → GridLayout-B4aCqSyd.js.map} +1 -1
  9. package/dist/GridLayout-DNOClFzz.cjs +2 -0
  10. package/dist/{GridLayout-B4VRsC0r.cjs.map → GridLayout-DNOClFzz.cjs.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-B8Igvnb2.cjs +3 -0
  16. package/dist/PanelSystem-B8Igvnb2.cjs.map +1 -0
  17. package/dist/{PanelSystem-Dr1TBhxM.js → PanelSystem-DDUSFjXD.js} +209 -248
  18. package/dist/PanelSystem-DDUSFjXD.js.map +1 -0
  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 +4 -1
  29. package/dist/components/window/DrawerLayers.d.ts +1 -1
  30. package/dist/components/window/DrawerRevealContext.d.ts +61 -0
  31. package/dist/components/window/drawerRevealAnimationUtils.d.ts +212 -0
  32. package/dist/components/window/drawerStyles.d.ts +74 -0
  33. package/dist/components/window/drawerSwipeConfig.d.ts +29 -0
  34. package/dist/components/window/useDrawerSwipeTransform.d.ts +29 -0
  35. package/dist/components/window/useDrawerTransform.d.ts +68 -0
  36. package/dist/components/window/useRevealDrawerTransform.d.ts +56 -0
  37. package/dist/config.cjs +1 -1
  38. package/dist/config.cjs.map +1 -1
  39. package/dist/config.js +9 -8
  40. package/dist/config.js.map +1 -1
  41. package/dist/constants/styles.d.ts +17 -0
  42. package/dist/dialog/index.d.ts +69 -0
  43. package/dist/floating.js +1 -1
  44. package/dist/grid.cjs +1 -1
  45. package/dist/grid.js +2 -2
  46. package/dist/hooks/gesture/testing/createGestureSimulator.d.ts +7 -0
  47. package/dist/hooks/gesture/types.d.ts +48 -5
  48. package/dist/hooks/gesture/utils.d.ts +19 -0
  49. package/dist/hooks/useAnimationFrame.d.ts +2 -0
  50. package/dist/hooks/useOperationContinuity.d.ts +64 -0
  51. package/dist/hooks/useResizeObserver.d.ts +33 -1
  52. package/dist/hooks/useSharedElementTransition.d.ts +112 -0
  53. package/dist/hooks/useSwipeContentTransform.d.ts +9 -2
  54. package/dist/index.cjs +1 -1
  55. package/dist/index.js +7 -7
  56. package/dist/modules/dialog/AlertDialog.d.ts +9 -0
  57. package/dist/modules/dialog/DialogContainer.d.ts +37 -0
  58. package/dist/modules/dialog/Modal.d.ts +26 -0
  59. package/dist/modules/dialog/SwipeDialogContainer.d.ts +16 -0
  60. package/dist/modules/dialog/dialogAnimationUtils.d.ts +113 -0
  61. package/dist/modules/dialog/types.d.ts +183 -0
  62. package/dist/modules/dialog/useDialog.d.ts +39 -0
  63. package/dist/modules/dialog/useDialogContainer.d.ts +47 -0
  64. package/dist/modules/dialog/useDialogSwipeInput.d.ts +70 -0
  65. package/dist/modules/dialog/useDialogTransform.d.ts +82 -0
  66. package/dist/modules/drawer/drawerStateMachine.d.ts +168 -0
  67. package/dist/modules/drawer/revealDrawerConstants.d.ts +33 -0
  68. package/dist/modules/drawer/revealDrawerStateMachine.d.ts +146 -0
  69. package/dist/modules/drawer/strategies/index.d.ts +8 -0
  70. package/dist/modules/drawer/strategies/overlayStrategy.d.ts +12 -0
  71. package/dist/modules/drawer/strategies/revealStrategy.d.ts +12 -0
  72. package/dist/modules/drawer/strategies/types.d.ts +116 -0
  73. package/dist/modules/drawer/types.d.ts +74 -0
  74. package/dist/modules/drawer/useDrawerSwipeInput.d.ts +24 -0
  75. package/dist/modules/pivot/SwipePivotTabBar.d.ts +3 -0
  76. package/dist/modules/stack/SwipeStackContent.d.ts +6 -3
  77. package/dist/modules/stack/SwipeStackOutlet.d.ts +4 -4
  78. package/dist/modules/stack/computeSwipeStackTransform.d.ts +1 -1
  79. package/dist/panels.cjs +1 -1
  80. package/dist/panels.js +1 -1
  81. package/dist/pivot.cjs +1 -1
  82. package/dist/pivot.js +1 -1
  83. package/dist/resizer.cjs +1 -1
  84. package/dist/resizer.js +2 -2
  85. package/dist/stack.cjs +1 -1
  86. package/dist/stack.cjs.map +1 -1
  87. package/dist/stack.js +480 -780
  88. package/dist/stack.js.map +1 -1
  89. package/dist/sticky-header/calculateStickyMetrics.d.ts +28 -0
  90. package/dist/sticky-header.cjs +1 -1
  91. package/dist/sticky-header.cjs.map +1 -1
  92. package/dist/sticky-header.js +59 -51
  93. package/dist/sticky-header.js.map +1 -1
  94. package/dist/{styles-DPPuJ0sf.js → styles-NkjuMOVS.js} +13 -13
  95. package/dist/{styles-DPPuJ0sf.js.map → styles-NkjuMOVS.js.map} +1 -1
  96. package/dist/styles-qf6ptVLD.cjs.map +1 -1
  97. package/dist/types.d.ts +30 -0
  98. package/dist/useAnimationFrame-BZ6D2lMq.cjs +2 -0
  99. package/dist/useAnimationFrame-BZ6D2lMq.cjs.map +1 -0
  100. package/dist/useAnimationFrame-Bg4e-H8O.js +394 -0
  101. package/dist/useAnimationFrame-Bg4e-H8O.js.map +1 -0
  102. package/dist/useDocumentPointerEvents-DXxw3qWj.js +54 -0
  103. package/dist/useDocumentPointerEvents-DXxw3qWj.js.map +1 -0
  104. package/dist/useDocumentPointerEvents-DxDSOtip.cjs +2 -0
  105. package/dist/useDocumentPointerEvents-DxDSOtip.cjs.map +1 -0
  106. package/dist/window/index.d.ts +2 -0
  107. package/dist/window.cjs +1 -1
  108. package/dist/window.cjs.map +1 -1
  109. package/dist/window.js +114 -103
  110. package/dist/window.js.map +1 -1
  111. package/package.json +6 -1
  112. package/src/components/gesture/SwipeSafeZone.tsx +70 -0
  113. package/src/components/grid/GridLayout.tsx +110 -38
  114. package/src/components/window/Drawer.tsx +353 -162
  115. package/src/components/window/DrawerLayers.tsx +54 -11
  116. package/src/components/window/DrawerRevealContext.spec.ts +20 -0
  117. package/src/components/window/DrawerRevealContext.tsx +99 -0
  118. package/src/components/window/drawerRevealAnimationUtils.spec.ts +375 -0
  119. package/src/components/window/drawerRevealAnimationUtils.ts +415 -0
  120. package/src/components/window/drawerStyles.spec.ts +302 -0
  121. package/src/components/window/drawerStyles.ts +252 -0
  122. package/src/components/window/drawerSwipeConfig.spec.ts +131 -0
  123. package/src/components/window/drawerSwipeConfig.ts +112 -0
  124. package/src/components/window/useDrawerSwipeTransform.ts +67 -0
  125. package/src/components/window/useDrawerTransform.ts +505 -0
  126. package/src/components/window/useRevealDrawerTransform.spec.ts +1936 -0
  127. package/src/components/window/useRevealDrawerTransform.ts +105 -0
  128. package/src/constants/styles.ts +19 -0
  129. package/src/demo/components/FullscreenDemoPage.tsx +47 -0
  130. package/src/demo/fullscreenRoutes.tsx +32 -0
  131. package/src/demo/index.tsx +5 -0
  132. package/src/demo/pages/Dialog/alerts/index.tsx +22 -0
  133. package/src/demo/pages/Dialog/card/index.tsx +22 -0
  134. package/src/demo/pages/Dialog/components/AlertDialogDemo.tsx +124 -0
  135. package/src/demo/pages/Dialog/components/CardExpandDemo.module.css +243 -0
  136. package/src/demo/pages/Dialog/components/CardExpandDemo.tsx +219 -0
  137. package/src/demo/pages/Dialog/components/CustomAlertDialogDemo.tsx +219 -0
  138. package/src/demo/pages/Dialog/components/DialogDemos.module.css +77 -0
  139. package/src/demo/pages/Dialog/components/ModalBasics.tsx +45 -0
  140. package/src/demo/pages/Dialog/components/SwipeDialogDemo.module.css +77 -0
  141. package/src/demo/pages/Dialog/components/SwipeDialogDemo.tsx +181 -0
  142. package/src/demo/pages/Dialog/custom-alert/index.tsx +22 -0
  143. package/src/demo/pages/Dialog/modal/index.tsx +17 -0
  144. package/src/demo/pages/Dialog/swipe/index.tsx +22 -0
  145. package/src/demo/pages/Drawer/components/DrawerBasics.module.css +6 -1
  146. package/src/demo/pages/Drawer/components/DrawerBasics.tsx +14 -4
  147. package/src/demo/pages/Drawer/components/DrawerReveal.module.css +157 -0
  148. package/src/demo/pages/Drawer/components/DrawerReveal.tsx +128 -0
  149. package/src/demo/pages/Drawer/components/DrawerSwipe.module.css +316 -0
  150. package/src/demo/pages/Drawer/components/DrawerSwipe.tsx +178 -0
  151. package/src/demo/pages/Drawer/reveal/index.tsx +17 -0
  152. package/src/demo/pages/Drawer/reveal-fullscreen/index.tsx +135 -0
  153. package/src/demo/pages/Drawer/reveal-fullscreen/styles.module.css +233 -0
  154. package/src/demo/pages/Drawer/swipe/index.tsx +17 -0
  155. package/src/demo/pages/Pivot/components/SwipeTabsPivot.tsx +54 -23
  156. package/src/demo/pages/Pivot/swipe-debug/index.tsx +1 -1
  157. package/src/demo/pages/Stack/components/StackBasics.spec.tsx +156 -0
  158. package/src/demo/pages/Stack/components/StackBasics.tsx +179 -95
  159. package/src/demo/pages/Stack/components/StackTablet.spec.tsx +110 -0
  160. package/src/demo/pages/Stack/components/StackTablet.tsx +42 -21
  161. package/src/demo/routes.tsx +24 -1
  162. package/src/dialog/index.ts +85 -0
  163. package/src/hooks/gesture/testing/createGestureSimulator.spec.ts +68 -64
  164. package/src/hooks/gesture/testing/createGestureSimulator.ts +113 -37
  165. package/src/hooks/gesture/types.ts +83 -6
  166. package/src/hooks/gesture/useEdgeSwipeInput.spec.ts +22 -14
  167. package/src/hooks/gesture/useNativeGestureGuard.spec.ts +99 -31
  168. package/src/hooks/gesture/useNativeGestureGuard.ts +3 -1
  169. package/src/hooks/gesture/utils.ts +102 -0
  170. package/src/hooks/useAnimatedVisibility.spec.ts +44 -24
  171. package/src/hooks/useAnimatedVisibility.ts +28 -2
  172. package/src/hooks/useAnimationFrame.ts +8 -0
  173. package/src/hooks/useOperationContinuity.spec.ts +394 -0
  174. package/src/hooks/useOperationContinuity.ts +135 -0
  175. package/src/hooks/useResizeObserver.spec.tsx +277 -0
  176. package/src/hooks/useResizeObserver.tsx +108 -39
  177. package/src/hooks/useScrollContainer.ts +4 -10
  178. package/src/hooks/useSharedElementTransition.ts +354 -0
  179. package/src/hooks/useSwipeContentTransform.spec.ts +18 -18
  180. package/src/hooks/useSwipeContentTransform.ts +166 -28
  181. package/src/modules/dialog/AlertDialog.spec.tsx +387 -0
  182. package/src/modules/dialog/AlertDialog.tsx +221 -0
  183. package/src/modules/dialog/DialogContainer.spec.tsx +228 -0
  184. package/src/modules/dialog/DialogContainer.tsx +188 -0
  185. package/src/modules/dialog/Modal.spec.tsx +220 -0
  186. package/src/modules/dialog/Modal.tsx +182 -0
  187. package/src/modules/dialog/SwipeDialogContainer.tsx +208 -0
  188. package/src/modules/dialog/dialogAnimationUtils.spec.ts +252 -0
  189. package/src/modules/dialog/dialogAnimationUtils.ts +297 -0
  190. package/src/modules/dialog/types.ts +186 -0
  191. package/src/modules/dialog/useDialog.spec.tsx +447 -0
  192. package/src/modules/dialog/useDialog.ts +214 -0
  193. package/src/modules/dialog/useDialogContainer.spec.ts +339 -0
  194. package/src/modules/dialog/useDialogContainer.ts +150 -0
  195. package/src/modules/dialog/useDialogSwipeInput.spec.ts +178 -0
  196. package/src/modules/dialog/useDialogSwipeInput.ts +350 -0
  197. package/src/modules/dialog/useDialogTransform.spec.ts +403 -0
  198. package/src/modules/dialog/useDialogTransform.ts +407 -0
  199. package/src/modules/drawer/drawerStateMachine.ts +500 -0
  200. package/src/modules/drawer/revealDrawerConstants.ts +38 -0
  201. package/src/modules/drawer/revealDrawerStateMachine.spec.ts +558 -0
  202. package/src/modules/drawer/revealDrawerStateMachine.ts +197 -0
  203. package/src/modules/drawer/strategies/index.ts +9 -0
  204. package/src/modules/drawer/strategies/overlayStrategy.ts +133 -0
  205. package/src/modules/drawer/strategies/revealStrategy.ts +111 -0
  206. package/src/modules/drawer/strategies/types.ts +160 -0
  207. package/src/modules/drawer/types.ts +102 -0
  208. package/src/modules/drawer/useDrawerSwipeInput.spec.ts +566 -0
  209. package/src/modules/drawer/useDrawerSwipeInput.ts +402 -0
  210. package/src/modules/panels/rendering/ContentRegistry.spec.tsx +21 -14
  211. package/src/modules/pivot/SwipePivotContent.position.spec.tsx +12 -8
  212. package/src/modules/pivot/SwipePivotContent.spec.tsx +66 -25
  213. package/src/modules/pivot/SwipePivotContent.tsx +2 -2
  214. package/src/modules/pivot/SwipePivotTabBar.spec.tsx +85 -68
  215. package/src/modules/pivot/SwipePivotTabBar.tsx +75 -15
  216. package/src/modules/pivot/scaleInputState.spec.ts +11 -2
  217. package/src/modules/pivot/usePivot.spec.ts +17 -3
  218. package/src/modules/pivot/usePivotSwipeInput.spec.ts +182 -123
  219. package/src/modules/stack/SwipeStackContent.spec.tsx +387 -100
  220. package/src/modules/stack/SwipeStackContent.tsx +43 -33
  221. package/src/modules/stack/SwipeStackOutlet.spec.tsx +14 -16
  222. package/src/modules/stack/SwipeStackOutlet.tsx +6 -6
  223. package/src/modules/stack/computeSwipeStackTransform.spec.ts +5 -5
  224. package/src/modules/stack/computeSwipeStackTransform.ts +3 -3
  225. package/src/modules/stack/swipeTransitionContinuity.spec.tsx +1133 -0
  226. package/src/modules/stack/useStackAnimationState.spec.ts +3 -1
  227. package/src/modules/stack/useStackAnimationState.ts +18 -13
  228. package/src/modules/stack/useStackNavigation.spec.ts +198 -3
  229. package/src/modules/stack/useStackNavigation.tsx +113 -56
  230. package/src/modules/stack/useStackSwipeInput.spec.ts +65 -32
  231. package/src/modules/stack/useStackSwipeInput.ts +1 -1
  232. package/src/sticky-header/StickyArea.tsx +29 -57
  233. package/src/sticky-header/calculateStickyMetrics.spec.ts +105 -0
  234. package/src/sticky-header/calculateStickyMetrics.ts +50 -0
  235. package/src/types.ts +33 -0
  236. package/src/window/index.ts +2 -0
  237. package/dist/FloatingWindow-BpdOpg_L.js +0 -400
  238. package/dist/FloatingWindow-BpdOpg_L.js.map +0 -1
  239. package/dist/FloatingWindow-TCDNY5gE.cjs +0 -2
  240. package/dist/FloatingWindow-TCDNY5gE.cjs.map +0 -1
  241. package/dist/GridLayout-B4VRsC0r.cjs +0 -2
  242. package/dist/GridLayout-BltqeCPK.js +0 -927
  243. package/dist/PanelSystem-Bs8bQwQF.cjs +0 -3
  244. package/dist/PanelSystem-Bs8bQwQF.cjs.map +0 -1
  245. package/dist/PanelSystem-Dr1TBhxM.js.map +0 -1
  246. package/dist/ResizeHandle-CScipO5l.cjs +0 -2
  247. package/dist/SwipePivotTabBar-BGO9X94m.js +0 -407
  248. package/dist/SwipePivotTabBar-BGO9X94m.js.map +0 -1
  249. package/dist/SwipePivotTabBar-BrQismcZ.cjs +0 -2
  250. package/dist/SwipePivotTabBar-BrQismcZ.cjs.map +0 -1
  251. package/dist/useDocumentPointerEvents-CKdhGXd0.js +0 -46
  252. package/dist/useDocumentPointerEvents-CKdhGXd0.js.map +0 -1
  253. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs +0 -2
  254. package/dist/useDocumentPointerEvents-ChqrKXDk.cjs.map +0 -1
  255. package/dist/useEffectEvent-Dp7HLCf0.js +0 -13
  256. package/dist/useEffectEvent-Dp7HLCf0.js.map +0 -1
  257. package/dist/useEffectEvent-huSsGUnl.cjs +0 -2
  258. package/dist/useEffectEvent-huSsGUnl.cjs.map +0 -1
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @file Tests for useDialogSwipeInput hook
3
+ */
4
+ import { renderHook, act } from "@testing-library/react";
5
+ import * as React from "react";
6
+ import { useDialogSwipeInput } from "./useDialogSwipeInput.js";
7
+
8
+ /**
9
+ * Call tracker for testing callbacks.
10
+ */
11
+ type CallTracker = {
12
+ calls: ReadonlyArray<ReadonlyArray<unknown>>;
13
+ fn: (...args: ReadonlyArray<unknown>) => void;
14
+ };
15
+
16
+ const createCallTracker = (): CallTracker => {
17
+ const calls: Array<ReadonlyArray<unknown>> = [];
18
+ const fn = (...args: ReadonlyArray<unknown>): void => {
19
+ calls.push(args);
20
+ };
21
+ return { calls, fn };
22
+ };
23
+
24
+ /**
25
+ * Cast native PointerEvent to React.PointerEvent for testing.
26
+ */
27
+ function asReactPointerEvent(e: PointerEvent): React.PointerEvent<HTMLElement> {
28
+ // eslint-disable-next-line custom/no-as-outside-guard -- test helper for event casting
29
+ return e as unknown as React.PointerEvent<HTMLElement>;
30
+ }
31
+
32
+ // Simple ResizeObserver mock
33
+ const originalResizeObserver = globalThis.ResizeObserver;
34
+
35
+ // eslint-disable-next-line no-restricted-syntax -- mock class needed for ResizeObserver
36
+ class MockResizeObserver {
37
+ observe(): void {}
38
+ unobserve(): void {}
39
+ disconnect(): void {}
40
+ }
41
+
42
+ beforeEach(() => {
43
+ // eslint-disable-next-line custom/no-as-outside-guard -- test mock assignment
44
+ globalThis.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver;
45
+ });
46
+
47
+ afterEach(() => {
48
+ globalThis.ResizeObserver = originalResizeObserver;
49
+ });
50
+
51
+ describe("useDialogSwipeInput", () => {
52
+ const createMockContainer = (dimensions: { width: number; height: number }) => {
53
+ const container = document.createElement("div");
54
+ Object.defineProperty(container, "clientWidth", { value: dimensions.width });
55
+ Object.defineProperty(container, "clientHeight", { value: dimensions.height });
56
+ return container;
57
+ };
58
+
59
+ describe("initialization", () => {
60
+ it("should return idle state initially", () => {
61
+ const container = createMockContainer({ width: 400, height: 300 });
62
+ const containerRef = { current: container };
63
+
64
+ const { result } = renderHook(() =>
65
+ useDialogSwipeInput({
66
+ containerRef,
67
+ openDirection: "bottom",
68
+ enabled: true,
69
+ onSwipeDismiss: createCallTracker().fn,
70
+ }),
71
+ );
72
+
73
+ expect(result.current.state.phase).toBe("idle");
74
+ expect(result.current.isOperating).toBe(false);
75
+ expect(result.current.displacement).toBe(0);
76
+ });
77
+
78
+ it("should provide containerProps with touch-action style", () => {
79
+ const container = createMockContainer({ width: 400, height: 300 });
80
+ const containerRef = { current: container };
81
+
82
+ const { result } = renderHook(() =>
83
+ useDialogSwipeInput({
84
+ containerRef,
85
+ openDirection: "bottom",
86
+ enabled: true,
87
+ onSwipeDismiss: createCallTracker().fn,
88
+ }),
89
+ );
90
+
91
+ expect(result.current.containerProps.style).toBeDefined();
92
+ expect(result.current.containerProps.style.touchAction).toBeDefined();
93
+ });
94
+ });
95
+
96
+ describe("free 2D movement", () => {
97
+ it("should use touch-action none for free movement", () => {
98
+ const container = createMockContainer({ width: 400, height: 300 });
99
+ const containerRef = { current: container };
100
+
101
+ const { result } = renderHook(() =>
102
+ useDialogSwipeInput({
103
+ containerRef,
104
+ openDirection: "bottom",
105
+ enabled: true,
106
+ onSwipeDismiss: createCallTracker().fn,
107
+ }),
108
+ );
109
+
110
+ // Free 2D movement requires touch-action: none
111
+ expect(result.current.containerProps.style.touchAction).toBe("none");
112
+ });
113
+
114
+ it("should provide displacement2D for 2D transform", () => {
115
+ const container = createMockContainer({ width: 400, height: 300 });
116
+ const containerRef = { current: container };
117
+
118
+ const { result } = renderHook(() =>
119
+ useDialogSwipeInput({
120
+ containerRef,
121
+ openDirection: "left",
122
+ enabled: true,
123
+ onSwipeDismiss: createCallTracker().fn,
124
+ }),
125
+ );
126
+
127
+ // Should provide 2D displacement
128
+ expect(result.current.displacement2D).toEqual({ x: 0, y: 0 });
129
+ });
130
+ });
131
+
132
+ describe("enabled state", () => {
133
+ it("should not respond to pointer events when disabled", () => {
134
+ const container = createMockContainer({ width: 400, height: 300 });
135
+ const containerRef = { current: container };
136
+
137
+ const { result } = renderHook(() =>
138
+ useDialogSwipeInput({
139
+ containerRef,
140
+ openDirection: "bottom",
141
+ enabled: false,
142
+ onSwipeDismiss: createCallTracker().fn,
143
+ }),
144
+ );
145
+
146
+ // Try to trigger pointer down
147
+ const pointerEvent = new PointerEvent("pointerdown", {
148
+ pointerId: 1,
149
+ clientX: 100,
150
+ clientY: 100,
151
+ });
152
+
153
+ act(() => {
154
+ result.current.containerProps.onPointerDown?.(asReactPointerEvent(pointerEvent));
155
+ });
156
+
157
+ expect(result.current.state.phase).toBe("idle");
158
+ });
159
+ });
160
+
161
+ describe("progress calculation", () => {
162
+ it("should return 0 progress when not swiping", () => {
163
+ const container = createMockContainer({ width: 400, height: 300 });
164
+ const containerRef = { current: container };
165
+
166
+ const { result } = renderHook(() =>
167
+ useDialogSwipeInput({
168
+ containerRef,
169
+ openDirection: "bottom",
170
+ enabled: true,
171
+ onSwipeDismiss: createCallTracker().fn,
172
+ }),
173
+ );
174
+
175
+ expect(result.current.progress).toBe(0);
176
+ });
177
+ });
178
+ });
@@ -0,0 +1,350 @@
1
+ /**
2
+ * @file Hook for detecting swipe gestures to dismiss a dialog.
3
+ *
4
+ * This hook provides free 2D movement during swipe:
5
+ * - User can drag in any direction freely
6
+ * - Close intent is detected on release based on displacement direction
7
+ * - If movement matches close direction and exceeds threshold, dismiss
8
+ * - Otherwise, snap back to original position
9
+ */
10
+ import * as React from "react";
11
+ import { usePointerTracking } from "../../hooks/gesture/usePointerTracking.js";
12
+ import {
13
+ type ContinuousOperationState,
14
+ type Vector2,
15
+ IDLE_CONTINUOUS_OPERATION_STATE,
16
+ } from "../../hooks/gesture/types.js";
17
+ import type { DialogOpenDirection } from "./dialogAnimationUtils.js";
18
+ import { getAnimationAxis, getDirectionSign } from "./dialogAnimationUtils.js";
19
+
20
+ /**
21
+ * Get sign from displacement value.
22
+ */
23
+ function getSignFromDisplacement(value: number): -1 | 0 | 1 {
24
+ if (value > 0) {
25
+ return 1;
26
+ }
27
+ if (value < 0) {
28
+ return -1;
29
+ }
30
+ return 0;
31
+ }
32
+
33
+ type TrackingPoint = { timestamp: number } | null;
34
+
35
+ /**
36
+ * Compute velocity from tracking points.
37
+ */
38
+ function computeSwipeVelocity(
39
+ start: TrackingPoint,
40
+ current: TrackingPoint,
41
+ primaryValue: number,
42
+ ): number {
43
+ if (start === null || current === null) {
44
+ return 0;
45
+ }
46
+ const duration = Math.max(1, current.timestamp - start.timestamp);
47
+ return Math.abs(primaryValue) / duration;
48
+ }
49
+
50
+ /**
51
+ * Default dismiss threshold (30% of container size).
52
+ */
53
+ const DEFAULT_DISMISS_THRESHOLD = 0.3;
54
+
55
+ /**
56
+ * Velocity threshold for quick flick dismissal (px/ms).
57
+ */
58
+ const VELOCITY_THRESHOLD = 0.5;
59
+
60
+ /**
61
+ * Options for useDialogSwipeInput hook.
62
+ */
63
+ export type UseDialogSwipeInputOptions = {
64
+ /** Ref to the dialog container element */
65
+ containerRef: React.RefObject<HTMLElement | null>;
66
+ /** Direction the dialog opened from (swipe closes in same direction) */
67
+ openDirection: DialogOpenDirection;
68
+ /** Whether swipe dismiss is enabled */
69
+ enabled: boolean;
70
+ /** Callback when swipe exceeds threshold and dialog should dismiss */
71
+ onSwipeDismiss: () => void;
72
+ /** Threshold ratio (0-1) of container size to trigger dismiss. @default 0.3 */
73
+ dismissThreshold?: number;
74
+ };
75
+
76
+ /**
77
+ * Result from useDialogSwipeInput hook.
78
+ */
79
+ export type UseDialogSwipeInputResult = {
80
+ /** Current operation state (idle, operating, or ended) */
81
+ state: ContinuousOperationState;
82
+ /** Props to spread on the container element */
83
+ containerProps: React.HTMLAttributes<HTMLElement> & {
84
+ style: React.CSSProperties;
85
+ };
86
+ /** Swipe progress (0-1) towards dismiss threshold */
87
+ progress: number;
88
+ /** Whether user is currently swiping */
89
+ isOperating: boolean;
90
+ /** Current displacement in pixels (primary axis based on openDirection) */
91
+ displacement: number;
92
+ /** Full 2D displacement for free movement transform */
93
+ displacement2D: Vector2;
94
+ };
95
+
96
+ /**
97
+ * Check if an element or its ancestors are scrollable in any direction.
98
+ */
99
+ function isScrollableElement(element: HTMLElement): boolean {
100
+ const style = getComputedStyle(element);
101
+
102
+ const isScrollableX =
103
+ (style.overflowX === "scroll" || style.overflowX === "auto") &&
104
+ element.scrollWidth > element.clientWidth;
105
+
106
+ const isScrollableY =
107
+ (style.overflowY === "scroll" || style.overflowY === "auto") &&
108
+ element.scrollHeight > element.clientHeight;
109
+
110
+ if (isScrollableX) {
111
+ return true;
112
+ }
113
+ return isScrollableY;
114
+ }
115
+
116
+ /**
117
+ * Check if we should start tracking based on scroll state.
118
+ * Returns false if the target is inside a scrollable element that can scroll in the drag direction.
119
+ */
120
+ function shouldStartDrag(event: React.PointerEvent, container: HTMLElement): boolean {
121
+ // eslint-disable-next-line no-restricted-syntax -- loop variable requires let
122
+ let current = event.target as HTMLElement | null;
123
+
124
+ while (current !== null && current !== container) {
125
+ if (isScrollableElement(current)) {
126
+ return false;
127
+ }
128
+ current = current.parentElement;
129
+ }
130
+
131
+ return true;
132
+ }
133
+
134
+ /**
135
+ * Hook for detecting swipe gestures to dismiss a dialog.
136
+ *
137
+ * Allows free 2D movement - user can drag in any direction.
138
+ * On release, detects if the movement matches the close direction:
139
+ * - "center" or "bottom": close if moved down significantly
140
+ * - "top": close if moved up significantly
141
+ * - "left": close if moved left significantly
142
+ * - "right": close if moved right significantly
143
+ *
144
+ * @example
145
+ * ```tsx
146
+ * const { state, containerProps, displacement2D } = useDialogSwipeInput({
147
+ * containerRef,
148
+ * openDirection: "bottom",
149
+ * enabled: true,
150
+ * onSwipeDismiss: () => setVisible(false),
151
+ * });
152
+ *
153
+ * // Apply 2D transform for free movement
154
+ * style={{ transform: `translate(${displacement2D.x}px, ${displacement2D.y}px)` }}
155
+ * ```
156
+ */
157
+ export function useDialogSwipeInput(
158
+ options: UseDialogSwipeInputOptions,
159
+ ): UseDialogSwipeInputResult {
160
+ const {
161
+ containerRef,
162
+ openDirection,
163
+ enabled,
164
+ onSwipeDismiss,
165
+ dismissThreshold = DEFAULT_DISMISS_THRESHOLD,
166
+ } = options;
167
+
168
+ const axis = getAnimationAxis(openDirection);
169
+ const expectedSign = getDirectionSign(openDirection);
170
+
171
+ // Track container size for progress calculation
172
+ const containerSizeRef = React.useRef<{ width: number; height: number }>({ width: 0, height: 0 });
173
+
174
+ React.useLayoutEffect(() => {
175
+ const container = containerRef.current;
176
+ if (!container) {
177
+ return;
178
+ }
179
+
180
+ const updateSize = () => {
181
+ containerSizeRef.current = {
182
+ width: container.clientWidth,
183
+ height: container.clientHeight,
184
+ };
185
+ };
186
+
187
+ updateSize();
188
+
189
+ const observer = new ResizeObserver(updateSize);
190
+ observer.observe(container);
191
+
192
+ return () => observer.disconnect();
193
+ }, [containerRef]);
194
+
195
+ // Use pointer tracking for free 2D movement
196
+ const { state: tracking, onPointerDown: baseOnPointerDown } = usePointerTracking({
197
+ enabled,
198
+ });
199
+
200
+ // Track displacement for snapback and release detection
201
+ const lastDisplacementRef = React.useRef<Vector2>({ x: 0, y: 0 });
202
+
203
+ // Wrap pointer down with scrollable check
204
+ const onPointerDown = React.useCallback(
205
+ (event: React.PointerEvent) => {
206
+ if (!enabled) {
207
+ return;
208
+ }
209
+ const container = containerRef.current;
210
+ if (!container) {
211
+ return;
212
+ }
213
+ if (!shouldStartDrag(event, container)) {
214
+ return;
215
+ }
216
+ baseOnPointerDown(event);
217
+ },
218
+ [enabled, containerRef, baseOnPointerDown],
219
+ );
220
+
221
+ // Calculate 2D displacement - preserve last value on release for snapback
222
+ const displacement2D = React.useMemo<Vector2>(() => {
223
+ if (!tracking.isDown || !tracking.start || !tracking.current) {
224
+ // Return last known displacement for snapback animation
225
+ return lastDisplacementRef.current;
226
+ }
227
+ return {
228
+ x: tracking.current.x - tracking.start.x,
229
+ y: tracking.current.y - tracking.start.y,
230
+ };
231
+ }, [tracking.isDown, tracking.start, tracking.current]);
232
+
233
+ // Calculate primary axis displacement for progress
234
+ const primaryDisplacement = axis === "x" ? displacement2D.x : displacement2D.y;
235
+
236
+ // Calculate progress towards dismiss threshold (only for correct direction)
237
+ const progress = React.useMemo(() => {
238
+ const containerSize = axis === "x" ? containerSizeRef.current.width : containerSizeRef.current.height;
239
+ if (containerSize <= 0) {
240
+ return 0;
241
+ }
242
+ const sign = getSignFromDisplacement(primaryDisplacement);
243
+ if (sign !== expectedSign) {
244
+ return 0; // Wrong direction
245
+ }
246
+ return Math.min(Math.abs(primaryDisplacement) / containerSize, 1);
247
+ }, [axis, primaryDisplacement, expectedSign]);
248
+
249
+ // State machine for operation phase
250
+ const [operationPhase, setOperationPhase] = React.useState<"idle" | "operating" | "ended">("idle");
251
+
252
+ // Store displacement while dragging
253
+ React.useEffect(() => {
254
+ if (tracking.isDown && tracking.current) {
255
+ lastDisplacementRef.current = displacement2D;
256
+ }
257
+ }, [tracking.isDown, tracking.current, displacement2D]);
258
+
259
+ // Handle drag start
260
+ React.useEffect(() => {
261
+ if (tracking.isDown && operationPhase === "idle") {
262
+ setOperationPhase("operating");
263
+ }
264
+ }, [tracking.isDown, operationPhase]);
265
+
266
+ // Handle release - transition to "ended" then check dismiss
267
+ React.useEffect(() => {
268
+ if (!tracking.isDown && operationPhase === "operating") {
269
+ const hasMovement = Math.abs(lastDisplacementRef.current.x) > 1 || Math.abs(lastDisplacementRef.current.y) > 1;
270
+
271
+ if (hasMovement) {
272
+ // Transition to ended phase for snapback detection
273
+ setOperationPhase("ended");
274
+
275
+ // Check if should dismiss
276
+ const containerSize = axis === "x" ? containerSizeRef.current.width : containerSizeRef.current.height;
277
+ if (containerSize > 0) {
278
+ const finalDisplacement = lastDisplacementRef.current;
279
+ const primaryValue = axis === "x" ? finalDisplacement.x : finalDisplacement.y;
280
+ const sign = getSignFromDisplacement(primaryValue);
281
+
282
+ if (sign === expectedSign) {
283
+ const ratio = Math.abs(primaryValue) / containerSize;
284
+ const velocity = computeSwipeVelocity(tracking.start, tracking.current, primaryValue);
285
+
286
+ if (ratio >= dismissThreshold || velocity >= VELOCITY_THRESHOLD) {
287
+ onSwipeDismiss();
288
+ }
289
+ }
290
+ }
291
+ } else {
292
+ // No significant movement, go directly to idle
293
+ setOperationPhase("idle");
294
+ lastDisplacementRef.current = { x: 0, y: 0 };
295
+ }
296
+ }
297
+ }, [tracking.isDown, operationPhase, axis, expectedSign, dismissThreshold, onSwipeDismiss, tracking.start, tracking.current]);
298
+
299
+ // Transition from ended to idle after one render (to allow snapback detection)
300
+ React.useEffect(() => {
301
+ if (operationPhase === "ended") {
302
+ // Use microtask to ensure the "ended" state is seen by consumers
303
+ queueMicrotask(() => {
304
+ setOperationPhase("idle");
305
+ lastDisplacementRef.current = { x: 0, y: 0 };
306
+ });
307
+ }
308
+ }, [operationPhase]);
309
+
310
+ // Build continuous operation state
311
+ const state = React.useMemo<ContinuousOperationState>(() => {
312
+ if (operationPhase === "idle") {
313
+ return IDLE_CONTINUOUS_OPERATION_STATE;
314
+ }
315
+ if (operationPhase === "ended") {
316
+ return {
317
+ phase: "ended",
318
+ displacement: lastDisplacementRef.current,
319
+ velocity: { x: 0, y: 0 },
320
+ };
321
+ }
322
+ return {
323
+ phase: "operating",
324
+ displacement: displacement2D,
325
+ velocity: { x: 0, y: 0 },
326
+ };
327
+ }, [operationPhase, displacement2D]);
328
+
329
+ const containerProps = React.useMemo(() => {
330
+ return {
331
+ onPointerDown,
332
+ style: {
333
+ touchAction: "none" as const, // Allow free 2D movement
334
+ userSelect: "none" as const,
335
+ WebkitUserSelect: "none" as const,
336
+ },
337
+ };
338
+ }, [onPointerDown]);
339
+
340
+ const isOperating = state.phase === "operating";
341
+
342
+ return {
343
+ state,
344
+ containerProps,
345
+ progress,
346
+ isOperating,
347
+ displacement: primaryDisplacement,
348
+ displacement2D,
349
+ };
350
+ }