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,186 @@
1
+ /**
2
+ * @file Dialog module type definitions
3
+ */
4
+ import type * as React from "react";
5
+ import type { Position } from "../../types";
6
+
7
+ /**
8
+ * Transition mode for dialog animations
9
+ * - "none": No animation
10
+ * - "css": CSS-based fade/scale animation
11
+ * - "swipe": Swipeable with multi-phase animation
12
+ */
13
+ export type DialogTransitionMode = "none" | "css" | "swipe";
14
+
15
+ /**
16
+ * Direction from which the dialog opens.
17
+ * The close animation uses the same direction (swipe down to close if opened from bottom).
18
+ */
19
+ export type DialogOpenDirection = "center" | "top" | "bottom" | "left" | "right";
20
+
21
+ /**
22
+ * Base props for DialogContainer component
23
+ */
24
+ export type DialogContainerProps = {
25
+ /** Whether the dialog is visible */
26
+ visible: boolean;
27
+ /** Callback when dialog should close */
28
+ onClose: () => void;
29
+ /** Dialog content */
30
+ children: React.ReactNode;
31
+ /** Position: 'center' for screen center, or Position for absolute coordinates */
32
+ position?: "center" | Position;
33
+ /** Whether clicking backdrop closes the dialog */
34
+ dismissible?: boolean;
35
+ /** Whether pressing Escape closes the dialog */
36
+ closeOnEscape?: boolean;
37
+ /** Whether to prevent body scroll when open */
38
+ preventBodyScroll?: boolean;
39
+ /** Whether to return focus to previous element on close */
40
+ returnFocus?: boolean;
41
+ /** Aria label for the dialog */
42
+ ariaLabel?: string;
43
+ /** ID of element that labels the dialog */
44
+ ariaLabelledBy?: string;
45
+ /** ID of element that describes the dialog */
46
+ ariaDescribedBy?: string;
47
+ /** Transition mode */
48
+ transitionMode?: DialogTransitionMode;
49
+ /** Transition duration (CSS value, e.g. '200ms') */
50
+ transitionDuration?: string;
51
+ /** Transition easing (CSS value, e.g. 'ease-out') */
52
+ transitionEasing?: string;
53
+ /** Whether the dialog can be dismissed by swiping. @default false for "css", true for "swipe" */
54
+ swipeDismissible?: boolean;
55
+ /** Direction the dialog opens from (used for swipe mode). @default "center" */
56
+ openDirection?: DialogOpenDirection;
57
+ /** Whether to use viewTransition API for close animation (when available). @default false */
58
+ useViewTransition?: boolean;
59
+ };
60
+
61
+ /**
62
+ * Header configuration for Modal
63
+ */
64
+ export type ModalHeader = {
65
+ /** Title displayed in the header */
66
+ title: string;
67
+ /** Whether to show close button (default: true) */
68
+ showCloseButton?: boolean;
69
+ };
70
+
71
+ /**
72
+ * Props for Modal component
73
+ */
74
+ export type ModalProps = Omit<DialogContainerProps, "position"> & {
75
+ /** Header configuration */
76
+ header?: ModalHeader;
77
+ /** Modal width */
78
+ width?: string | number;
79
+ /** Modal height */
80
+ height?: string | number;
81
+ /** Max width (default: 90vw) */
82
+ maxWidth?: string | number;
83
+ /** Max height (default: 85vh) */
84
+ maxHeight?: string | number;
85
+ /** Whether to use FloatingPanelFrame chrome (default: true) */
86
+ chrome?: boolean;
87
+ /** Custom content style */
88
+ contentStyle?: React.CSSProperties;
89
+ };
90
+
91
+ /**
92
+ * Options for alert dialog
93
+ */
94
+ export type AlertOptions = {
95
+ /** Optional title */
96
+ title?: string;
97
+ /** Message to display */
98
+ message: string;
99
+ /** OK button label (default: 'OK') */
100
+ okLabel?: string;
101
+ };
102
+
103
+ /**
104
+ * Options for confirm dialog
105
+ */
106
+ export type ConfirmOptions = AlertOptions & {
107
+ /** Confirm button label (default: 'OK') */
108
+ confirmLabel?: string;
109
+ /** Cancel button label (default: 'Cancel') */
110
+ cancelLabel?: string;
111
+ };
112
+
113
+ /**
114
+ * Options for prompt dialog
115
+ */
116
+ export type PromptOptions = ConfirmOptions & {
117
+ /** Default input value */
118
+ defaultValue?: string;
119
+ /** Placeholder text for input */
120
+ placeholder?: string;
121
+ /** Input type (default: 'text') */
122
+ inputType?: "text" | "password" | "email" | "number";
123
+ };
124
+
125
+ /**
126
+ * Internal state for dialog queue
127
+ */
128
+ export type DialogQueueItem =
129
+ | { type: "alert"; options: AlertOptions; resolve: (value: void) => void }
130
+ | { type: "confirm"; options: ConfirmOptions; resolve: (value: boolean) => void }
131
+ | { type: "prompt"; options: PromptOptions; resolve: (value: string | null) => void };
132
+
133
+ /**
134
+ * Return type of useDialog hook
135
+ */
136
+ export type UseDialogReturn = {
137
+ /** Show an alert dialog */
138
+ alert: (options: AlertOptions | string) => Promise<void>;
139
+ /** Show a confirm dialog */
140
+ confirm: (options: ConfirmOptions | string) => Promise<boolean>;
141
+ /** Show a prompt dialog */
142
+ prompt: (options: PromptOptions | string) => Promise<string | null>;
143
+ /** Outlet component to render dialogs */
144
+ Outlet: React.FC;
145
+ };
146
+
147
+ /**
148
+ * Props for useDialog hook
149
+ */
150
+ export type UseDialogProps = {
151
+ /**
152
+ * Custom component to render alert/confirm/prompt dialogs.
153
+ * When provided, replaces the default AlertDialog component.
154
+ */
155
+ alertDialogComponent?: React.ComponentType<AlertDialogProps>;
156
+ };
157
+
158
+ /**
159
+ * Props for AlertDialog component (internal)
160
+ */
161
+ export type AlertDialogProps = {
162
+ /** Dialog type */
163
+ type: "alert" | "confirm" | "prompt";
164
+ /** Whether the dialog is visible */
165
+ visible: boolean;
166
+ /** Title */
167
+ title?: string;
168
+ /** Message */
169
+ message: string;
170
+ /** OK/Confirm button label */
171
+ confirmLabel?: string;
172
+ /** Cancel button label */
173
+ cancelLabel?: string;
174
+ /** Placeholder for prompt input */
175
+ placeholder?: string;
176
+ /** Default value for prompt input */
177
+ defaultValue?: string;
178
+ /** Input type for prompt */
179
+ inputType?: "text" | "password" | "email" | "number";
180
+ /** Callback for confirm/OK action */
181
+ onConfirm: (value?: string) => void;
182
+ /** Callback for cancel action */
183
+ onCancel: () => void;
184
+ /** Whether the dialog can be dismissed by swiping. @default false for alert, true for confirm/prompt */
185
+ swipeDismissible?: boolean;
186
+ };
@@ -0,0 +1,447 @@
1
+ /**
2
+ * @file Tests for useDialog hook
3
+ */
4
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react";
5
+ import * as React from "react";
6
+ import { useDialog } from "./useDialog";
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
+ describe("useDialog", () => {
22
+ const originalShowModal = HTMLDialogElement.prototype.showModal;
23
+ const originalClose = HTMLDialogElement.prototype.close;
24
+
25
+ beforeEach(() => {
26
+ HTMLDialogElement.prototype.showModal = function (this: HTMLDialogElement) {
27
+ this.setAttribute("open", "");
28
+ };
29
+ HTMLDialogElement.prototype.close = function (this: HTMLDialogElement) {
30
+ this.removeAttribute("open");
31
+ };
32
+ });
33
+
34
+ afterEach(() => {
35
+ HTMLDialogElement.prototype.showModal = originalShowModal;
36
+ HTMLDialogElement.prototype.close = originalClose;
37
+ document.body.style.overflow = "";
38
+ document.body.style.paddingRight = "";
39
+ });
40
+
41
+ const TestComponent: React.FC<{
42
+ onAlert?: () => void;
43
+ onConfirm?: (result: boolean) => void;
44
+ onPrompt?: (result: string | null) => void;
45
+ }> = ({ onAlert, onConfirm, onPrompt }) => {
46
+ const { alert, confirm, prompt, Outlet } = useDialog();
47
+
48
+ const handleAlert = async () => {
49
+ await alert("Test alert message");
50
+ onAlert?.();
51
+ };
52
+
53
+ const handleConfirm = async () => {
54
+ const result = await confirm("Test confirm message");
55
+ onConfirm?.(result);
56
+ };
57
+
58
+ const handlePrompt = async () => {
59
+ const result = await prompt("Test prompt message");
60
+ onPrompt?.(result);
61
+ };
62
+
63
+ return (
64
+ <div>
65
+ <button data-testid="alert-btn" onClick={handleAlert}>
66
+ Alert
67
+ </button>
68
+ <button data-testid="confirm-btn" onClick={handleConfirm}>
69
+ Confirm
70
+ </button>
71
+ <button data-testid="prompt-btn" onClick={handlePrompt}>
72
+ Prompt
73
+ </button>
74
+ <Outlet />
75
+ </div>
76
+ );
77
+ };
78
+
79
+ describe("alert", () => {
80
+ it("should show alert dialog with message", async () => {
81
+ render(<TestComponent />);
82
+
83
+ fireEvent.click(screen.getByTestId("alert-btn"));
84
+
85
+ await waitFor(() => {
86
+ expect(screen.getByText("Test alert message")).toBeInTheDocument();
87
+ });
88
+ });
89
+
90
+ it("should resolve when OK is clicked", async () => {
91
+ const onAlert = createCallTracker();
92
+ render(<TestComponent onAlert={onAlert.fn} />);
93
+
94
+ fireEvent.click(screen.getByTestId("alert-btn"));
95
+
96
+ await waitFor(() => {
97
+ expect(screen.getByRole("button", { name: "OK" })).toBeInTheDocument();
98
+ });
99
+
100
+ fireEvent.click(screen.getByRole("button", { name: "OK" }));
101
+
102
+ await waitFor(() => {
103
+ expect(onAlert.calls).toHaveLength(1);
104
+ });
105
+ });
106
+
107
+ it("should accept string as shorthand", async () => {
108
+ const TestStringAlert: React.FC = () => {
109
+ const { alert, Outlet } = useDialog();
110
+ return (
111
+ <div>
112
+ <button onClick={() => alert("Simple message")}>Alert</button>
113
+ <Outlet />
114
+ </div>
115
+ );
116
+ };
117
+
118
+ render(<TestStringAlert />);
119
+ fireEvent.click(screen.getByRole("button", { name: "Alert" }));
120
+
121
+ await waitFor(() => {
122
+ expect(screen.getByText("Simple message")).toBeInTheDocument();
123
+ });
124
+ });
125
+ });
126
+
127
+ describe("confirm", () => {
128
+ it("should show confirm dialog with message", async () => {
129
+ render(<TestComponent />);
130
+
131
+ fireEvent.click(screen.getByTestId("confirm-btn"));
132
+
133
+ await waitFor(() => {
134
+ expect(screen.getByText("Test confirm message")).toBeInTheDocument();
135
+ });
136
+ });
137
+
138
+ it("should resolve with true when OK is clicked", async () => {
139
+ const onConfirm = createCallTracker();
140
+ render(<TestComponent onConfirm={onConfirm.fn} />);
141
+
142
+ fireEvent.click(screen.getByTestId("confirm-btn"));
143
+
144
+ await waitFor(() => {
145
+ expect(screen.getByRole("button", { name: "OK" })).toBeInTheDocument();
146
+ });
147
+
148
+ fireEvent.click(screen.getByRole("button", { name: "OK" }));
149
+
150
+ await waitFor(() => {
151
+ expect(onConfirm.calls).toHaveLength(1);
152
+ expect(onConfirm.calls[0]?.[0]).toBe(true);
153
+ });
154
+ });
155
+
156
+ it("should resolve with false when Cancel is clicked", async () => {
157
+ const onConfirm = createCallTracker();
158
+ render(<TestComponent onConfirm={onConfirm.fn} />);
159
+
160
+ fireEvent.click(screen.getByTestId("confirm-btn"));
161
+
162
+ await waitFor(() => {
163
+ expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
164
+ });
165
+
166
+ fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
167
+
168
+ await waitFor(() => {
169
+ expect(onConfirm.calls).toHaveLength(1);
170
+ expect(onConfirm.calls[0]?.[0]).toBe(false);
171
+ });
172
+ });
173
+
174
+ it("should resolve with false when backdrop is clicked", async () => {
175
+ const onConfirm = createCallTracker();
176
+ render(<TestComponent onConfirm={onConfirm.fn} />);
177
+
178
+ fireEvent.click(screen.getByTestId("confirm-btn"));
179
+
180
+ await waitFor(() => {
181
+ expect(screen.getByText("Test confirm message")).toBeInTheDocument();
182
+ });
183
+
184
+ const dialog = document.querySelector("dialog");
185
+ fireEvent.click(dialog!);
186
+
187
+ await waitFor(() => {
188
+ expect(onConfirm.calls).toHaveLength(1);
189
+ expect(onConfirm.calls[0]?.[0]).toBe(false);
190
+ });
191
+ });
192
+ });
193
+
194
+ describe("prompt", () => {
195
+ it("should show prompt dialog with message and input", async () => {
196
+ render(<TestComponent />);
197
+
198
+ fireEvent.click(screen.getByTestId("prompt-btn"));
199
+
200
+ await waitFor(() => {
201
+ expect(screen.getByText("Test prompt message")).toBeInTheDocument();
202
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
203
+ });
204
+ });
205
+
206
+ it("should resolve with input value when OK is clicked", async () => {
207
+ const onPrompt = createCallTracker();
208
+ render(<TestComponent onPrompt={onPrompt.fn} />);
209
+
210
+ fireEvent.click(screen.getByTestId("prompt-btn"));
211
+
212
+ await waitFor(() => {
213
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
214
+ });
215
+
216
+ const input = screen.getByRole("textbox");
217
+ fireEvent.change(input, { target: { value: "Test input" } });
218
+ fireEvent.click(screen.getByRole("button", { name: "OK" }));
219
+
220
+ await waitFor(() => {
221
+ expect(onPrompt.calls).toHaveLength(1);
222
+ expect(onPrompt.calls[0]?.[0]).toBe("Test input");
223
+ });
224
+ });
225
+
226
+ it("should resolve with null when Cancel is clicked", async () => {
227
+ const onPrompt = createCallTracker();
228
+ render(<TestComponent onPrompt={onPrompt.fn} />);
229
+
230
+ fireEvent.click(screen.getByTestId("prompt-btn"));
231
+
232
+ await waitFor(() => {
233
+ expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
234
+ });
235
+
236
+ fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
237
+
238
+ await waitFor(() => {
239
+ expect(onPrompt.calls).toHaveLength(1);
240
+ expect(onPrompt.calls[0]?.[0]).toBe(null);
241
+ });
242
+ });
243
+
244
+ it("should resolve with null when backdrop is clicked", async () => {
245
+ const onPrompt = createCallTracker();
246
+ render(<TestComponent onPrompt={onPrompt.fn} />);
247
+
248
+ fireEvent.click(screen.getByTestId("prompt-btn"));
249
+
250
+ await waitFor(() => {
251
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
252
+ });
253
+
254
+ const dialog = document.querySelector("dialog");
255
+ fireEvent.click(dialog!);
256
+
257
+ await waitFor(() => {
258
+ expect(onPrompt.calls).toHaveLength(1);
259
+ expect(onPrompt.calls[0]?.[0]).toBe(null);
260
+ });
261
+ });
262
+ });
263
+
264
+ describe("queue behavior", () => {
265
+ it("should queue multiple dialogs and show one at a time", async () => {
266
+ const results: string[] = [];
267
+
268
+ const QueueTest: React.FC = () => {
269
+ const { alert, Outlet } = useDialog();
270
+
271
+ const handleClick = async () => {
272
+ await alert("First");
273
+ results.push("first done");
274
+ await alert("Second");
275
+ results.push("second done");
276
+ await alert("Third");
277
+ results.push("third done");
278
+ };
279
+
280
+ return (
281
+ <div>
282
+ <button onClick={handleClick}>Show All</button>
283
+ <Outlet />
284
+ </div>
285
+ );
286
+ };
287
+
288
+ render(<QueueTest />);
289
+
290
+ fireEvent.click(screen.getByRole("button", { name: "Show All" }));
291
+
292
+ // First dialog
293
+ await waitFor(() => {
294
+ expect(screen.getByText("First")).toBeInTheDocument();
295
+ });
296
+
297
+ fireEvent.click(screen.getByRole("button", { name: "OK" }));
298
+
299
+ // Second dialog
300
+ await waitFor(() => {
301
+ expect(screen.getByText("Second")).toBeInTheDocument();
302
+ });
303
+ expect(results).toContain("first done");
304
+
305
+ fireEvent.click(screen.getByRole("button", { name: "OK" }));
306
+
307
+ // Third dialog
308
+ await waitFor(() => {
309
+ expect(screen.getByText("Third")).toBeInTheDocument();
310
+ });
311
+ expect(results).toContain("second done");
312
+
313
+ fireEvent.click(screen.getByRole("button", { name: "OK" }));
314
+
315
+ await waitFor(() => {
316
+ expect(results).toContain("third done");
317
+ });
318
+ });
319
+
320
+ it("should handle multiple dialogs triggered simultaneously", async () => {
321
+ const results: (boolean | string | null)[] = [];
322
+
323
+ const SimultaneousTest: React.FC = () => {
324
+ const { confirm, prompt, Outlet } = useDialog();
325
+
326
+ const handleClick = () => {
327
+ confirm("First?").then((r) => results.push(r));
328
+ prompt("Second?").then((r) => results.push(r));
329
+ };
330
+
331
+ return (
332
+ <div>
333
+ <button onClick={handleClick}>Show All</button>
334
+ <Outlet />
335
+ </div>
336
+ );
337
+ };
338
+
339
+ render(<SimultaneousTest />);
340
+
341
+ fireEvent.click(screen.getByRole("button", { name: "Show All" }));
342
+
343
+ // First dialog (confirm)
344
+ await waitFor(() => {
345
+ expect(screen.getByText("First?")).toBeInTheDocument();
346
+ });
347
+
348
+ fireEvent.click(screen.getByRole("button", { name: "OK" }));
349
+
350
+ // Second dialog (prompt)
351
+ await waitFor(() => {
352
+ expect(screen.getByText("Second?")).toBeInTheDocument();
353
+ });
354
+
355
+ expect(results[0]).toBe(true);
356
+
357
+ fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
358
+
359
+ await waitFor(() => {
360
+ expect(results[1]).toBeNull();
361
+ });
362
+ });
363
+ });
364
+
365
+ describe("options", () => {
366
+ it("should use custom title", async () => {
367
+ const TestTitle: React.FC = () => {
368
+ const { alert, Outlet } = useDialog();
369
+ return (
370
+ <div>
371
+ <button onClick={() => alert({ title: "Custom Title", message: "Message" })}>Alert</button>
372
+ <Outlet />
373
+ </div>
374
+ );
375
+ };
376
+
377
+ render(<TestTitle />);
378
+ fireEvent.click(screen.getByRole("button", { name: "Alert" }));
379
+
380
+ await waitFor(() => {
381
+ expect(screen.getByText("Custom Title")).toBeInTheDocument();
382
+ });
383
+ });
384
+
385
+ it("should use custom button labels", async () => {
386
+ const TestLabels: React.FC = () => {
387
+ const { confirm, Outlet } = useDialog();
388
+ return (
389
+ <div>
390
+ <button
391
+ onClick={() =>
392
+ confirm({
393
+ message: "Confirm?",
394
+ confirmLabel: "Yes",
395
+ cancelLabel: "No",
396
+ })
397
+ }
398
+ >
399
+ Confirm
400
+ </button>
401
+ <Outlet />
402
+ </div>
403
+ );
404
+ };
405
+
406
+ render(<TestLabels />);
407
+ fireEvent.click(screen.getByRole("button", { name: "Confirm" }));
408
+
409
+ await waitFor(() => {
410
+ expect(screen.getByRole("button", { name: "Yes" })).toBeInTheDocument();
411
+ expect(screen.getByRole("button", { name: "No" })).toBeInTheDocument();
412
+ });
413
+ });
414
+
415
+ it("should use prompt options", async () => {
416
+ const TestPromptOptions: React.FC = () => {
417
+ const { prompt, Outlet } = useDialog();
418
+ return (
419
+ <div>
420
+ <button
421
+ onClick={() =>
422
+ prompt({
423
+ message: "Enter:",
424
+ defaultValue: "default",
425
+ placeholder: "placeholder",
426
+ inputType: "password",
427
+ })
428
+ }
429
+ >
430
+ Prompt
431
+ </button>
432
+ <Outlet />
433
+ </div>
434
+ );
435
+ };
436
+
437
+ render(<TestPromptOptions />);
438
+ fireEvent.click(screen.getByRole("button", { name: "Prompt" }));
439
+
440
+ await waitFor(() => {
441
+ const input = screen.getByPlaceholderText("placeholder") as HTMLInputElement;
442
+ expect(input.value).toBe("default");
443
+ expect(input.type).toBe("password");
444
+ });
445
+ });
446
+ });
447
+ });