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,214 @@
1
+ /**
2
+ * @file useDialog hook for imperative alert/confirm/prompt dialogs
3
+ */
4
+ import * as React from "react";
5
+ import type {
6
+ AlertOptions,
7
+ ConfirmOptions,
8
+ PromptOptions,
9
+ DialogQueueItem,
10
+ UseDialogReturn,
11
+ UseDialogProps,
12
+ } from "./types";
13
+ import { AlertDialog } from "./AlertDialog";
14
+
15
+ type DialogState = {
16
+ queue: DialogQueueItem[];
17
+ current: DialogQueueItem | null;
18
+ };
19
+
20
+ const normalizeAlertOptions = (options: AlertOptions | string): AlertOptions => {
21
+ if (typeof options === "string") {
22
+ return { message: options };
23
+ }
24
+ return options;
25
+ };
26
+
27
+ const normalizeConfirmOptions = (options: ConfirmOptions | string): ConfirmOptions => {
28
+ if (typeof options === "string") {
29
+ return { message: options };
30
+ }
31
+ return options;
32
+ };
33
+
34
+ const normalizePromptOptions = (options: PromptOptions | string): PromptOptions => {
35
+ if (typeof options === "string") {
36
+ return { message: options };
37
+ }
38
+ return options;
39
+ };
40
+
41
+ /**
42
+ * Hook for imperative alert/confirm/prompt dialogs.
43
+ *
44
+ * Returns functions to show dialogs and an Outlet component that must be rendered.
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * function MyComponent() {
49
+ * const { alert, confirm, prompt, Outlet } = useDialog();
50
+ *
51
+ * const handleClick = async () => {
52
+ * await alert("Hello!");
53
+ *
54
+ * const confirmed = await confirm({
55
+ * message: "Are you sure?",
56
+ * confirmLabel: "Yes",
57
+ * cancelLabel: "No",
58
+ * });
59
+ *
60
+ * if (confirmed) {
61
+ * const name = await prompt({
62
+ * message: "Enter your name:",
63
+ * defaultValue: "Anonymous",
64
+ * });
65
+ * console.log("Name:", name);
66
+ * }
67
+ * };
68
+ *
69
+ * return (
70
+ * <>
71
+ * <button onClick={handleClick}>Show dialogs</button>
72
+ * <Outlet />
73
+ * </>
74
+ * );
75
+ * }
76
+ * ```
77
+ */
78
+ export const useDialog = (props?: UseDialogProps): UseDialogReturn => {
79
+ const DialogComponent = props?.alertDialogComponent ?? AlertDialog;
80
+ const [state, setState] = React.useState<DialogState>({
81
+ queue: [],
82
+ current: null,
83
+ });
84
+
85
+ // Process queue when current dialog is closed
86
+ const processQueue = React.useCallback(() => {
87
+ setState((prev) => {
88
+ if (prev.queue.length === 0) {
89
+ return { ...prev, current: null };
90
+ }
91
+ const [next, ...rest] = prev.queue;
92
+ return { queue: rest, current: next };
93
+ });
94
+ }, []);
95
+
96
+ // Add to queue or show immediately if no current dialog
97
+ const enqueue = React.useCallback((item: DialogQueueItem) => {
98
+ setState((prev) => {
99
+ if (prev.current === null) {
100
+ return { ...prev, current: item };
101
+ }
102
+ return { ...prev, queue: [...prev.queue, item] };
103
+ });
104
+ }, []);
105
+
106
+ const alert = React.useCallback(
107
+ (options: AlertOptions | string): Promise<void> => {
108
+ return new Promise<void>((resolve) => {
109
+ const normalized = normalizeAlertOptions(options);
110
+ enqueue({
111
+ type: "alert",
112
+ options: normalized,
113
+ resolve,
114
+ });
115
+ });
116
+ },
117
+ [enqueue],
118
+ );
119
+
120
+ const confirm = React.useCallback(
121
+ (options: ConfirmOptions | string): Promise<boolean> => {
122
+ return new Promise<boolean>((resolve) => {
123
+ const normalized = normalizeConfirmOptions(options);
124
+ enqueue({
125
+ type: "confirm",
126
+ options: normalized,
127
+ resolve,
128
+ });
129
+ });
130
+ },
131
+ [enqueue],
132
+ );
133
+
134
+ const prompt = React.useCallback(
135
+ (options: PromptOptions | string): Promise<string | null> => {
136
+ return new Promise<string | null>((resolve) => {
137
+ const normalized = normalizePromptOptions(options);
138
+ enqueue({
139
+ type: "prompt",
140
+ options: normalized,
141
+ resolve,
142
+ });
143
+ });
144
+ },
145
+ [enqueue],
146
+ );
147
+
148
+ const handleConfirm = React.useCallback(
149
+ (value?: string) => {
150
+ const current = state.current;
151
+ if (!current) {
152
+ return;
153
+ }
154
+
155
+ if (current.type === "alert") {
156
+ current.resolve();
157
+ } else if (current.type === "confirm") {
158
+ current.resolve(true);
159
+ } else if (current.type === "prompt") {
160
+ current.resolve(value ?? "");
161
+ }
162
+
163
+ processQueue();
164
+ },
165
+ [state.current, processQueue],
166
+ );
167
+
168
+ const handleCancel = React.useCallback(() => {
169
+ const current = state.current;
170
+ if (!current) {
171
+ return;
172
+ }
173
+
174
+ if (current.type === "alert") {
175
+ current.resolve();
176
+ } else if (current.type === "confirm") {
177
+ current.resolve(false);
178
+ } else if (current.type === "prompt") {
179
+ current.resolve(null);
180
+ }
181
+
182
+ processQueue();
183
+ }, [state.current, processQueue]);
184
+
185
+ const Outlet: React.FC = React.useCallback(() => {
186
+ const current = state.current;
187
+ if (!current) {
188
+ return null;
189
+ }
190
+
191
+ const { type, options } = current;
192
+
193
+ return React.createElement(DialogComponent, {
194
+ type,
195
+ visible: true,
196
+ title: options.title,
197
+ message: options.message,
198
+ confirmLabel: type === "alert" ? (options as AlertOptions).okLabel : (options as ConfirmOptions).confirmLabel,
199
+ cancelLabel: (options as ConfirmOptions).cancelLabel,
200
+ placeholder: (options as PromptOptions).placeholder,
201
+ defaultValue: (options as PromptOptions).defaultValue,
202
+ inputType: (options as PromptOptions).inputType,
203
+ onConfirm: handleConfirm,
204
+ onCancel: handleCancel,
205
+ });
206
+ }, [state.current, handleConfirm, handleCancel, DialogComponent]);
207
+
208
+ return {
209
+ alert,
210
+ confirm,
211
+ prompt,
212
+ Outlet,
213
+ };
214
+ };
@@ -0,0 +1,339 @@
1
+ /**
2
+ * @file Tests for useDialogContainer hook
3
+ */
4
+ import * as React from "react";
5
+ import { renderHook, act } from "@testing-library/react";
6
+ import { useDialogContainer } from "./useDialogContainer";
7
+
8
+ type CallTracker = {
9
+ calls: ReadonlyArray<ReadonlyArray<unknown>>;
10
+ fn: (...args: ReadonlyArray<unknown>) => void;
11
+ };
12
+
13
+ const createCallTracker = (): CallTracker => {
14
+ const calls: Array<ReadonlyArray<unknown>> = [];
15
+ const fn = (...args: ReadonlyArray<unknown>): void => {
16
+ calls.push(args);
17
+ };
18
+ return { calls, fn };
19
+ };
20
+
21
+ /**
22
+ * Create a mock view for test events.
23
+ */
24
+ function createMockView(): React.MouseEvent<HTMLDialogElement>["view"] {
25
+ // eslint-disable-next-line custom/no-as-outside-guard -- test helper for view casting
26
+ return window as unknown as React.MouseEvent<HTMLDialogElement>["view"];
27
+ }
28
+
29
+ /**
30
+ * Mock SyntheticEvent with call tracking.
31
+ */
32
+ type MockSyntheticEvent = React.SyntheticEvent & {
33
+ preventDefault: (() => void) & { calls: number };
34
+ };
35
+
36
+ /**
37
+ * Creates a mock React.SyntheticEvent with preventDefault tracking.
38
+ */
39
+ function createMockSyntheticEvent(): MockSyntheticEvent {
40
+ const noop = (): void => {};
41
+ const noopBool = (): boolean => false;
42
+ const element = document.createElement("div");
43
+
44
+ const preventDefaultFn = (): void => {
45
+ preventDefaultFn.calls += 1;
46
+ };
47
+ preventDefaultFn.calls = 0;
48
+
49
+ return {
50
+ preventDefault: preventDefaultFn,
51
+ target: element,
52
+ currentTarget: element,
53
+ nativeEvent: new Event("test"),
54
+ bubbles: true,
55
+ cancelable: true,
56
+ defaultPrevented: false,
57
+ eventPhase: 0,
58
+ isTrusted: true,
59
+ isDefaultPrevented: noopBool,
60
+ stopPropagation: noop,
61
+ isPropagationStopped: noopBool,
62
+ persist: noop,
63
+ timeStamp: Date.now(),
64
+ type: "test",
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Creates a mock React.MouseEvent for dialog interactions.
70
+ */
71
+ function createMockMouseEvent(
72
+ target: HTMLElement,
73
+ currentTarget: HTMLDialogElement,
74
+ ): React.MouseEvent<HTMLDialogElement> {
75
+ const noop = (): void => {};
76
+ const noopBool = (): boolean => false;
77
+ const nativeEvent = new MouseEvent("click");
78
+
79
+ return {
80
+ target,
81
+ currentTarget,
82
+ nativeEvent,
83
+ bubbles: true,
84
+ cancelable: true,
85
+ defaultPrevented: false,
86
+ eventPhase: 0,
87
+ isTrusted: true,
88
+ preventDefault: noop,
89
+ isDefaultPrevented: noopBool,
90
+ stopPropagation: noop,
91
+ isPropagationStopped: noopBool,
92
+ persist: noop,
93
+ timeStamp: Date.now(),
94
+ type: "click",
95
+ // MouseEvent properties
96
+ altKey: false,
97
+ button: 0,
98
+ buttons: 1,
99
+ clientX: 0,
100
+ clientY: 0,
101
+ ctrlKey: false,
102
+ metaKey: false,
103
+ shiftKey: false,
104
+ getModifierState: noopBool,
105
+ movementX: 0,
106
+ movementY: 0,
107
+ pageX: 0,
108
+ pageY: 0,
109
+ relatedTarget: null,
110
+ screenX: 0,
111
+ screenY: 0,
112
+ // UIEvent properties
113
+ detail: 0,
114
+ view: createMockView(),
115
+ };
116
+ }
117
+
118
+ describe("useDialogContainer", () => {
119
+ const dialogState = {
120
+ dialog: document.createElement("dialog") as HTMLDialogElement,
121
+ showModal: createCallTracker(),
122
+ close: createCallTracker(),
123
+ };
124
+
125
+ beforeEach(() => {
126
+ // Create mock dialog element
127
+ dialogState.dialog = document.createElement("dialog");
128
+ dialogState.showModal = createCallTracker();
129
+ dialogState.close = createCallTracker();
130
+ dialogState.dialog.showModal = () => {
131
+ dialogState.showModal.fn();
132
+ };
133
+ dialogState.dialog.close = () => {
134
+ dialogState.close.fn();
135
+ };
136
+ Object.defineProperty(dialogState.dialog, "open", {
137
+ value: false,
138
+ writable: true,
139
+ configurable: true,
140
+ });
141
+ });
142
+
143
+ afterEach(() => {
144
+ document.body.style.overflow = "";
145
+ document.body.style.paddingRight = "";
146
+ });
147
+
148
+ it("should return dialogRef and dialogProps", () => {
149
+ const onClose = createCallTracker();
150
+ const { result } = renderHook(() =>
151
+ useDialogContainer({
152
+ visible: false,
153
+ onClose: onClose.fn,
154
+ }),
155
+ );
156
+
157
+ expect(result.current.dialogRef).toBeDefined();
158
+ expect(result.current.dialogProps).toBeDefined();
159
+ expect(result.current.dialogProps.onCancel).toBeInstanceOf(Function);
160
+ expect(result.current.dialogProps.onClick).toBeInstanceOf(Function);
161
+ });
162
+
163
+ it("should call showModal when visible becomes true", () => {
164
+ const onClose = createCallTracker();
165
+ const { result, rerender } = renderHook(
166
+ ({ visible }) =>
167
+ useDialogContainer({
168
+ visible,
169
+ onClose: onClose.fn,
170
+ }),
171
+ { initialProps: { visible: false } },
172
+ );
173
+
174
+ // Assign mock dialog to ref
175
+ act(() => {
176
+ (result.current.dialogRef as React.MutableRefObject<HTMLDialogElement | null>).current = dialogState.dialog;
177
+ });
178
+
179
+ // Make visible
180
+ rerender({ visible: true });
181
+
182
+ expect(dialogState.showModal.calls).toHaveLength(1);
183
+ });
184
+
185
+ it("should call close when visible becomes false", () => {
186
+ const onClose = createCallTracker();
187
+ const { result, rerender } = renderHook(
188
+ ({ visible }) =>
189
+ useDialogContainer({
190
+ visible,
191
+ onClose: onClose.fn,
192
+ }),
193
+ { initialProps: { visible: true } },
194
+ );
195
+
196
+ // Assign mock dialog to ref and set as open
197
+ act(() => {
198
+ (result.current.dialogRef as React.MutableRefObject<HTMLDialogElement | null>).current = dialogState.dialog;
199
+ Object.defineProperty(dialogState.dialog, "open", { value: true, configurable: true });
200
+ });
201
+
202
+ // Make invisible
203
+ rerender({ visible: false });
204
+
205
+ expect(dialogState.close.calls).toHaveLength(1);
206
+ });
207
+
208
+ it("should prevent default on cancel event and call onClose when closeOnEscape is true", () => {
209
+ const onClose = createCallTracker();
210
+ const { result } = renderHook(() =>
211
+ useDialogContainer({
212
+ visible: true,
213
+ onClose: onClose.fn,
214
+ closeOnEscape: true,
215
+ }),
216
+ );
217
+
218
+ const mockEvent = createMockSyntheticEvent();
219
+
220
+ act(() => {
221
+ result.current.dialogProps.onCancel(mockEvent);
222
+ });
223
+
224
+ expect(mockEvent.preventDefault.calls).toBeGreaterThan(0);
225
+ expect(onClose.calls).toHaveLength(1);
226
+ });
227
+
228
+ it("should prevent default on cancel event but NOT call onClose when closeOnEscape is false", () => {
229
+ const onClose = createCallTracker();
230
+ const { result } = renderHook(() =>
231
+ useDialogContainer({
232
+ visible: true,
233
+ onClose: onClose.fn,
234
+ closeOnEscape: false,
235
+ }),
236
+ );
237
+
238
+ const mockEvent = createMockSyntheticEvent();
239
+
240
+ act(() => {
241
+ result.current.dialogProps.onCancel(mockEvent);
242
+ });
243
+
244
+ expect(mockEvent.preventDefault.calls).toBeGreaterThan(0);
245
+ expect(onClose.calls).toHaveLength(0);
246
+ });
247
+
248
+ it("should call onClose on backdrop click when dismissible is true", () => {
249
+ const onClose = createCallTracker();
250
+ const { result } = renderHook(() =>
251
+ useDialogContainer({
252
+ visible: true,
253
+ onClose: onClose.fn,
254
+ dismissible: true,
255
+ }),
256
+ );
257
+
258
+ // Simulate click on dialog element itself (backdrop area)
259
+ const mockEvent = createMockMouseEvent(dialogState.dialog, dialogState.dialog);
260
+
261
+ act(() => {
262
+ result.current.dialogProps.onClick(mockEvent);
263
+ });
264
+
265
+ expect(onClose.calls).toHaveLength(1);
266
+ });
267
+
268
+ it("should NOT call onClose on backdrop click when dismissible is false", () => {
269
+ const onClose = createCallTracker();
270
+ const { result } = renderHook(() =>
271
+ useDialogContainer({
272
+ visible: true,
273
+ onClose: onClose.fn,
274
+ dismissible: false,
275
+ }),
276
+ );
277
+
278
+ const mockEvent = createMockMouseEvent(dialogState.dialog, dialogState.dialog);
279
+
280
+ act(() => {
281
+ result.current.dialogProps.onClick(mockEvent);
282
+ });
283
+
284
+ expect(onClose.calls).toHaveLength(0);
285
+ });
286
+
287
+ it("should NOT call onClose when click is on content (not backdrop)", () => {
288
+ const onClose = createCallTracker();
289
+ const { result } = renderHook(() =>
290
+ useDialogContainer({
291
+ visible: true,
292
+ onClose: onClose.fn,
293
+ dismissible: true,
294
+ }),
295
+ );
296
+
297
+ const contentElement = document.createElement("div");
298
+
299
+ // Click on content inside dialog
300
+ const mockEvent = createMockMouseEvent(contentElement, dialogState.dialog);
301
+
302
+ act(() => {
303
+ result.current.dialogProps.onClick(mockEvent);
304
+ });
305
+
306
+ expect(onClose.calls).toHaveLength(0);
307
+ });
308
+
309
+ it("should set body overflow to hidden when visible and preventBodyScroll is true", () => {
310
+ const onClose = createCallTracker();
311
+ const { unmount } = renderHook(() =>
312
+ useDialogContainer({
313
+ visible: true,
314
+ onClose: onClose.fn,
315
+ preventBodyScroll: true,
316
+ }),
317
+ );
318
+
319
+ expect(document.body.style.overflow).toBe("hidden");
320
+
321
+ unmount();
322
+
323
+ // Should restore
324
+ expect(document.body.style.overflow).toBe("");
325
+ });
326
+
327
+ it("should NOT set body overflow when preventBodyScroll is false", () => {
328
+ const onClose = createCallTracker();
329
+ renderHook(() =>
330
+ useDialogContainer({
331
+ visible: true,
332
+ onClose: onClose.fn,
333
+ preventBodyScroll: false,
334
+ }),
335
+ );
336
+
337
+ expect(document.body.style.overflow).toBe("");
338
+ });
339
+ });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @file Hook for managing dialog element lifecycle
3
+ */
4
+ import * as React from "react";
5
+ import { useEffectEvent } from "../../hooks/useEffectEvent";
6
+ import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect";
7
+
8
+ export type UseDialogContainerOptions = {
9
+ /** Whether the dialog is visible */
10
+ visible: boolean;
11
+ /** Callback when dialog should close */
12
+ onClose: () => void;
13
+ /** Whether clicking backdrop closes the dialog (default: true) */
14
+ dismissible?: boolean;
15
+ /** Whether pressing Escape closes the dialog (default: true) */
16
+ closeOnEscape?: boolean;
17
+ /** Whether to return focus to previous element on close (default: true) */
18
+ returnFocus?: boolean;
19
+ /** Whether to prevent body scroll when open (default: true) */
20
+ preventBodyScroll?: boolean;
21
+ };
22
+
23
+ export type UseDialogContainerReturn = {
24
+ /** Ref to attach to the dialog element */
25
+ dialogRef: React.RefObject<HTMLDialogElement | null>;
26
+ /** Props to spread onto the dialog element */
27
+ dialogProps: {
28
+ onCancel: (event: React.SyntheticEvent) => void;
29
+ onClick: (event: React.MouseEvent<HTMLDialogElement>) => void;
30
+ };
31
+ };
32
+
33
+ const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
34
+
35
+ /**
36
+ * Hook for managing native dialog element lifecycle
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * function MyDialog({ visible, onClose }) {
41
+ * const { dialogRef, dialogProps } = useDialogContainer({
42
+ * visible,
43
+ * onClose,
44
+ * });
45
+ *
46
+ * return (
47
+ * <dialog ref={dialogRef} {...dialogProps}>
48
+ * <div>Dialog content</div>
49
+ * </dialog>
50
+ * );
51
+ * }
52
+ * ```
53
+ */
54
+ export const useDialogContainer = (options: UseDialogContainerOptions): UseDialogContainerReturn => {
55
+ const {
56
+ visible,
57
+ onClose,
58
+ dismissible = true,
59
+ closeOnEscape = true,
60
+ returnFocus = true,
61
+ preventBodyScroll = true,
62
+ } = options;
63
+
64
+ const dialogRef = React.useRef<HTMLDialogElement | null>(null);
65
+ const previousActiveElementRef = React.useRef<Element | null>(null);
66
+
67
+ const handleClose = useEffectEvent(onClose);
68
+
69
+ // Open/close dialog based on visibility
70
+ useIsomorphicLayoutEffect(() => {
71
+ const dialog = dialogRef.current;
72
+ if (!dialog || !isBrowser) {
73
+ return;
74
+ }
75
+
76
+ if (visible) {
77
+ // Store currently focused element before opening
78
+ if (returnFocus) {
79
+ previousActiveElementRef.current = document.activeElement;
80
+ }
81
+
82
+ // Open as modal (this puts it in the top layer, above all other content)
83
+ if (!dialog.open) {
84
+ dialog.showModal();
85
+ }
86
+ } else if (dialog.open) {
87
+ dialog.close();
88
+
89
+ // Return focus to previously focused element
90
+ if (returnFocus && previousActiveElementRef.current instanceof HTMLElement) {
91
+ previousActiveElementRef.current.focus();
92
+ previousActiveElementRef.current = null;
93
+ }
94
+ }
95
+ }, [visible, returnFocus]);
96
+
97
+ // Prevent body scroll when dialog is open
98
+ React.useEffect(() => {
99
+ if (!isBrowser || !preventBodyScroll || !visible) {
100
+ return;
101
+ }
102
+
103
+ const originalOverflow = document.body.style.overflow;
104
+ const originalPaddingRight = document.body.style.paddingRight;
105
+
106
+ // Calculate scrollbar width to prevent layout shift
107
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
108
+
109
+ document.body.style.overflow = "hidden";
110
+ if (scrollbarWidth > 0) {
111
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
112
+ }
113
+
114
+ return () => {
115
+ document.body.style.overflow = originalOverflow;
116
+ document.body.style.paddingRight = originalPaddingRight;
117
+ };
118
+ }, [visible, preventBodyScroll]);
119
+
120
+ // Handle cancel event (Escape key)
121
+ const handleCancel = React.useCallback(
122
+ (event: React.SyntheticEvent) => {
123
+ event.preventDefault();
124
+ if (closeOnEscape) {
125
+ handleClose();
126
+ }
127
+ },
128
+ [closeOnEscape, handleClose],
129
+ );
130
+
131
+ // Handle click on dialog (for backdrop click detection)
132
+ const handleClick = React.useCallback(
133
+ (event: React.MouseEvent<HTMLDialogElement>) => {
134
+ // Check if click was on the dialog backdrop (::backdrop pseudo-element area)
135
+ // The dialog element itself is the backdrop; clicking on it directly means backdrop click
136
+ if (dismissible && event.target === event.currentTarget) {
137
+ handleClose();
138
+ }
139
+ },
140
+ [dismissible, handleClose],
141
+ );
142
+
143
+ return {
144
+ dialogRef,
145
+ dialogProps: {
146
+ onCancel: handleCancel,
147
+ onClick: handleClick,
148
+ },
149
+ };
150
+ };