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,157 @@
1
+ /**
2
+ * @file Tests for useDialogSwipeInput hook
3
+ */
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
5
+ import { renderHook, act } from "@testing-library/react";
6
+ import * as React from "react";
7
+ import { useDialogSwipeInput } from "./useDialogSwipeInput.js";
8
+
9
+ // Mock ResizeObserver
10
+ vi.stubGlobal(
11
+ "ResizeObserver",
12
+ class {
13
+ observe = vi.fn();
14
+ unobserve = vi.fn();
15
+ disconnect = vi.fn();
16
+ },
17
+ );
18
+
19
+ describe("useDialogSwipeInput", () => {
20
+ const createMockContainer = (dimensions: { width: number; height: number }) => {
21
+ const container = document.createElement("div");
22
+ Object.defineProperty(container, "clientWidth", { value: dimensions.width });
23
+ Object.defineProperty(container, "clientHeight", { value: dimensions.height });
24
+ return container;
25
+ };
26
+
27
+ beforeEach(() => {
28
+ vi.useFakeTimers();
29
+ });
30
+
31
+ afterEach(() => {
32
+ vi.useRealTimers();
33
+ vi.clearAllMocks();
34
+ });
35
+
36
+ describe("initialization", () => {
37
+ it("should return idle state initially", () => {
38
+ const container = createMockContainer({ width: 400, height: 300 });
39
+ const containerRef = { current: container };
40
+
41
+ const { result } = renderHook(() =>
42
+ useDialogSwipeInput({
43
+ containerRef,
44
+ openDirection: "bottom",
45
+ enabled: true,
46
+ onSwipeDismiss: vi.fn(),
47
+ }),
48
+ );
49
+
50
+ expect(result.current.state.phase).toBe("idle");
51
+ expect(result.current.isOperating).toBe(false);
52
+ expect(result.current.displacement).toBe(0);
53
+ });
54
+
55
+ it("should provide containerProps with touch-action style", () => {
56
+ const container = createMockContainer({ width: 400, height: 300 });
57
+ const containerRef = { current: container };
58
+
59
+ const { result } = renderHook(() =>
60
+ useDialogSwipeInput({
61
+ containerRef,
62
+ openDirection: "bottom",
63
+ enabled: true,
64
+ onSwipeDismiss: vi.fn(),
65
+ }),
66
+ );
67
+
68
+ expect(result.current.containerProps.style).toBeDefined();
69
+ expect(result.current.containerProps.style.touchAction).toBeDefined();
70
+ });
71
+ });
72
+
73
+ describe("free 2D movement", () => {
74
+ it("should use touch-action none for free movement", () => {
75
+ const container = createMockContainer({ width: 400, height: 300 });
76
+ const containerRef = { current: container };
77
+
78
+ const { result } = renderHook(() =>
79
+ useDialogSwipeInput({
80
+ containerRef,
81
+ openDirection: "bottom",
82
+ enabled: true,
83
+ onSwipeDismiss: vi.fn(),
84
+ }),
85
+ );
86
+
87
+ // Free 2D movement requires touch-action: none
88
+ expect(result.current.containerProps.style.touchAction).toBe("none");
89
+ });
90
+
91
+ it("should provide displacement2D for 2D transform", () => {
92
+ const container = createMockContainer({ width: 400, height: 300 });
93
+ const containerRef = { current: container };
94
+
95
+ const { result } = renderHook(() =>
96
+ useDialogSwipeInput({
97
+ containerRef,
98
+ openDirection: "left",
99
+ enabled: true,
100
+ onSwipeDismiss: vi.fn(),
101
+ }),
102
+ );
103
+
104
+ // Should provide 2D displacement
105
+ expect(result.current.displacement2D).toEqual({ x: 0, y: 0 });
106
+ });
107
+ });
108
+
109
+ describe("enabled state", () => {
110
+ it("should not respond to pointer events when disabled", () => {
111
+ const container = createMockContainer({ width: 400, height: 300 });
112
+ const containerRef = { current: container };
113
+
114
+ const { result } = renderHook(() =>
115
+ useDialogSwipeInput({
116
+ containerRef,
117
+ openDirection: "bottom",
118
+ enabled: false,
119
+ onSwipeDismiss: vi.fn(),
120
+ }),
121
+ );
122
+
123
+ // Try to trigger pointer down
124
+ const pointerEvent = new PointerEvent("pointerdown", {
125
+ pointerId: 1,
126
+ clientX: 100,
127
+ clientY: 100,
128
+ });
129
+
130
+ act(() => {
131
+ result.current.containerProps.onPointerDown?.(
132
+ pointerEvent as unknown as React.PointerEvent,
133
+ );
134
+ });
135
+
136
+ expect(result.current.state.phase).toBe("idle");
137
+ });
138
+ });
139
+
140
+ describe("progress calculation", () => {
141
+ it("should return 0 progress when not swiping", () => {
142
+ const container = createMockContainer({ width: 400, height: 300 });
143
+ const containerRef = { current: container };
144
+
145
+ const { result } = renderHook(() =>
146
+ useDialogSwipeInput({
147
+ containerRef,
148
+ openDirection: "bottom",
149
+ enabled: true,
150
+ onSwipeDismiss: vi.fn(),
151
+ }),
152
+ );
153
+
154
+ expect(result.current.progress).toBe(0);
155
+ });
156
+ });
157
+ });
@@ -0,0 +1,319 @@
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
+ * Default dismiss threshold (30% of container size).
22
+ */
23
+ const DEFAULT_DISMISS_THRESHOLD = 0.3;
24
+
25
+ /**
26
+ * Velocity threshold for quick flick dismissal (px/ms).
27
+ */
28
+ const VELOCITY_THRESHOLD = 0.5;
29
+
30
+ /**
31
+ * Options for useDialogSwipeInput hook.
32
+ */
33
+ export type UseDialogSwipeInputOptions = {
34
+ /** Ref to the dialog container element */
35
+ containerRef: React.RefObject<HTMLElement | null>;
36
+ /** Direction the dialog opened from (swipe closes in same direction) */
37
+ openDirection: DialogOpenDirection;
38
+ /** Whether swipe dismiss is enabled */
39
+ enabled: boolean;
40
+ /** Callback when swipe exceeds threshold and dialog should dismiss */
41
+ onSwipeDismiss: () => void;
42
+ /** Threshold ratio (0-1) of container size to trigger dismiss. @default 0.3 */
43
+ dismissThreshold?: number;
44
+ };
45
+
46
+ /**
47
+ * Result from useDialogSwipeInput hook.
48
+ */
49
+ export type UseDialogSwipeInputResult = {
50
+ /** Current operation state (idle, operating, or ended) */
51
+ state: ContinuousOperationState;
52
+ /** Props to spread on the container element */
53
+ containerProps: React.HTMLAttributes<HTMLElement> & {
54
+ style: React.CSSProperties;
55
+ };
56
+ /** Swipe progress (0-1) towards dismiss threshold */
57
+ progress: number;
58
+ /** Whether user is currently swiping */
59
+ isOperating: boolean;
60
+ /** Current displacement in pixels (primary axis based on openDirection) */
61
+ displacement: number;
62
+ /** Full 2D displacement for free movement transform */
63
+ displacement2D: Vector2;
64
+ };
65
+
66
+ /**
67
+ * Check if an element or its ancestors are scrollable in any direction.
68
+ */
69
+ function isScrollableElement(element: HTMLElement): boolean {
70
+ const style = getComputedStyle(element);
71
+
72
+ const isScrollableX =
73
+ (style.overflowX === "scroll" || style.overflowX === "auto") &&
74
+ element.scrollWidth > element.clientWidth;
75
+
76
+ const isScrollableY =
77
+ (style.overflowY === "scroll" || style.overflowY === "auto") &&
78
+ element.scrollHeight > element.clientHeight;
79
+
80
+ return isScrollableX || isScrollableY;
81
+ }
82
+
83
+ /**
84
+ * Check if we should start tracking based on scroll state.
85
+ * Returns false if the target is inside a scrollable element that can scroll in the drag direction.
86
+ */
87
+ function shouldStartDrag(event: React.PointerEvent, container: HTMLElement): boolean {
88
+ // eslint-disable-next-line no-restricted-syntax -- loop variable requires let
89
+ let current = event.target as HTMLElement | null;
90
+
91
+ while (current !== null && current !== container) {
92
+ if (isScrollableElement(current)) {
93
+ return false;
94
+ }
95
+ current = current.parentElement;
96
+ }
97
+
98
+ return true;
99
+ }
100
+
101
+ /**
102
+ * Hook for detecting swipe gestures to dismiss a dialog.
103
+ *
104
+ * Allows free 2D movement - user can drag in any direction.
105
+ * On release, detects if the movement matches the close direction:
106
+ * - "center" or "bottom": close if moved down significantly
107
+ * - "top": close if moved up significantly
108
+ * - "left": close if moved left significantly
109
+ * - "right": close if moved right significantly
110
+ *
111
+ * @example
112
+ * ```tsx
113
+ * const { state, containerProps, displacement2D } = useDialogSwipeInput({
114
+ * containerRef,
115
+ * openDirection: "bottom",
116
+ * enabled: true,
117
+ * onSwipeDismiss: () => setVisible(false),
118
+ * });
119
+ *
120
+ * // Apply 2D transform for free movement
121
+ * style={{ transform: `translate(${displacement2D.x}px, ${displacement2D.y}px)` }}
122
+ * ```
123
+ */
124
+ export function useDialogSwipeInput(
125
+ options: UseDialogSwipeInputOptions,
126
+ ): UseDialogSwipeInputResult {
127
+ const {
128
+ containerRef,
129
+ openDirection,
130
+ enabled,
131
+ onSwipeDismiss,
132
+ dismissThreshold = DEFAULT_DISMISS_THRESHOLD,
133
+ } = options;
134
+
135
+ const axis = getAnimationAxis(openDirection);
136
+ const expectedSign = getDirectionSign(openDirection);
137
+
138
+ // Track container size for progress calculation
139
+ const containerSizeRef = React.useRef<{ width: number; height: number }>({ width: 0, height: 0 });
140
+
141
+ React.useLayoutEffect(() => {
142
+ const container = containerRef.current;
143
+ if (!container) {
144
+ return;
145
+ }
146
+
147
+ const updateSize = () => {
148
+ containerSizeRef.current = {
149
+ width: container.clientWidth,
150
+ height: container.clientHeight,
151
+ };
152
+ };
153
+
154
+ updateSize();
155
+
156
+ const observer = new ResizeObserver(updateSize);
157
+ observer.observe(container);
158
+
159
+ return () => observer.disconnect();
160
+ }, [containerRef]);
161
+
162
+ // Use pointer tracking for free 2D movement
163
+ const { state: tracking, onPointerDown: baseOnPointerDown } = usePointerTracking({
164
+ enabled,
165
+ });
166
+
167
+ // Track displacement for snapback and release detection
168
+ const lastDisplacementRef = React.useRef<Vector2>({ x: 0, y: 0 });
169
+
170
+ // Wrap pointer down with scrollable check
171
+ const onPointerDown = React.useCallback(
172
+ (event: React.PointerEvent) => {
173
+ if (!enabled) {
174
+ return;
175
+ }
176
+ const container = containerRef.current;
177
+ if (!container) {
178
+ return;
179
+ }
180
+ if (!shouldStartDrag(event, container)) {
181
+ return;
182
+ }
183
+ baseOnPointerDown(event);
184
+ },
185
+ [enabled, containerRef, baseOnPointerDown],
186
+ );
187
+
188
+ // Calculate 2D displacement - preserve last value on release for snapback
189
+ const displacement2D = React.useMemo<Vector2>(() => {
190
+ if (!tracking.isDown || !tracking.start || !tracking.current) {
191
+ // Return last known displacement for snapback animation
192
+ return lastDisplacementRef.current;
193
+ }
194
+ return {
195
+ x: tracking.current.x - tracking.start.x,
196
+ y: tracking.current.y - tracking.start.y,
197
+ };
198
+ }, [tracking.isDown, tracking.start, tracking.current]);
199
+
200
+ // Calculate primary axis displacement for progress
201
+ const primaryDisplacement = axis === "x" ? displacement2D.x : displacement2D.y;
202
+
203
+ // Calculate progress towards dismiss threshold (only for correct direction)
204
+ const progress = React.useMemo(() => {
205
+ const containerSize = axis === "x" ? containerSizeRef.current.width : containerSizeRef.current.height;
206
+ if (containerSize <= 0) {
207
+ return 0;
208
+ }
209
+ const sign = primaryDisplacement > 0 ? 1 : primaryDisplacement < 0 ? -1 : 0;
210
+ if (sign !== expectedSign) {
211
+ return 0; // Wrong direction
212
+ }
213
+ return Math.min(Math.abs(primaryDisplacement) / containerSize, 1);
214
+ }, [axis, primaryDisplacement, expectedSign]);
215
+
216
+ // State machine for operation phase
217
+ const [operationPhase, setOperationPhase] = React.useState<"idle" | "operating" | "ended">("idle");
218
+
219
+ // Store displacement while dragging
220
+ React.useEffect(() => {
221
+ if (tracking.isDown && tracking.current) {
222
+ lastDisplacementRef.current = displacement2D;
223
+ }
224
+ }, [tracking.isDown, tracking.current, displacement2D]);
225
+
226
+ // Handle drag start
227
+ React.useEffect(() => {
228
+ if (tracking.isDown && operationPhase === "idle") {
229
+ setOperationPhase("operating");
230
+ }
231
+ }, [tracking.isDown, operationPhase]);
232
+
233
+ // Handle release - transition to "ended" then check dismiss
234
+ React.useEffect(() => {
235
+ if (!tracking.isDown && operationPhase === "operating") {
236
+ const hasMovement = Math.abs(lastDisplacementRef.current.x) > 1 || Math.abs(lastDisplacementRef.current.y) > 1;
237
+
238
+ if (hasMovement) {
239
+ // Transition to ended phase for snapback detection
240
+ setOperationPhase("ended");
241
+
242
+ // Check if should dismiss
243
+ const containerSize = axis === "x" ? containerSizeRef.current.width : containerSizeRef.current.height;
244
+ if (containerSize > 0) {
245
+ const finalDisplacement = lastDisplacementRef.current;
246
+ const primaryValue = axis === "x" ? finalDisplacement.x : finalDisplacement.y;
247
+ const sign = primaryValue > 0 ? 1 : primaryValue < 0 ? -1 : 0;
248
+
249
+ if (sign === expectedSign) {
250
+ const ratio = Math.abs(primaryValue) / containerSize;
251
+ const velocity = tracking.start && tracking.current
252
+ ? Math.abs(primaryValue) / Math.max(1, tracking.current.timestamp - tracking.start.timestamp)
253
+ : 0;
254
+
255
+ if (ratio >= dismissThreshold || velocity >= VELOCITY_THRESHOLD) {
256
+ onSwipeDismiss();
257
+ }
258
+ }
259
+ }
260
+ } else {
261
+ // No significant movement, go directly to idle
262
+ setOperationPhase("idle");
263
+ lastDisplacementRef.current = { x: 0, y: 0 };
264
+ }
265
+ }
266
+ }, [tracking.isDown, operationPhase, axis, expectedSign, dismissThreshold, onSwipeDismiss, tracking.start, tracking.current]);
267
+
268
+ // Transition from ended to idle after one render (to allow snapback detection)
269
+ React.useEffect(() => {
270
+ if (operationPhase === "ended") {
271
+ // Use microtask to ensure the "ended" state is seen by consumers
272
+ queueMicrotask(() => {
273
+ setOperationPhase("idle");
274
+ lastDisplacementRef.current = { x: 0, y: 0 };
275
+ });
276
+ }
277
+ }, [operationPhase]);
278
+
279
+ // Build continuous operation state
280
+ const state = React.useMemo<ContinuousOperationState>(() => {
281
+ if (operationPhase === "idle") {
282
+ return IDLE_CONTINUOUS_OPERATION_STATE;
283
+ }
284
+ if (operationPhase === "ended") {
285
+ return {
286
+ phase: "ended",
287
+ displacement: lastDisplacementRef.current,
288
+ velocity: { x: 0, y: 0 },
289
+ };
290
+ }
291
+ return {
292
+ phase: "operating",
293
+ displacement: displacement2D,
294
+ velocity: { x: 0, y: 0 },
295
+ };
296
+ }, [operationPhase, displacement2D]);
297
+
298
+ const containerProps = React.useMemo(() => {
299
+ return {
300
+ onPointerDown,
301
+ style: {
302
+ touchAction: "none" as const, // Allow free 2D movement
303
+ userSelect: "none" as const,
304
+ WebkitUserSelect: "none" as const,
305
+ },
306
+ };
307
+ }, [onPointerDown]);
308
+
309
+ const isOperating = state.phase === "operating";
310
+
311
+ return {
312
+ state,
313
+ containerProps,
314
+ progress,
315
+ isOperating,
316
+ displacement: primaryDisplacement,
317
+ displacement2D,
318
+ };
319
+ }