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,220 @@
1
+ /**
2
+ * @file Tests for Modal component
3
+ */
4
+ import { render, screen, fireEvent } from "@testing-library/react";
5
+ import { Modal } from "./Modal";
6
+
7
+ type CallTracker = {
8
+ calls: ReadonlyArray<ReadonlyArray<unknown>>;
9
+ fn: (...args: ReadonlyArray<unknown>) => void;
10
+ };
11
+
12
+ const createCallTracker = (): CallTracker => {
13
+ const calls: Array<ReadonlyArray<unknown>> = [];
14
+ const fn = (...args: ReadonlyArray<unknown>): void => {
15
+ calls.push(args);
16
+ };
17
+ return { calls, fn };
18
+ };
19
+
20
+ describe("Modal", () => {
21
+ const originalShowModal = HTMLDialogElement.prototype.showModal;
22
+ const originalClose = HTMLDialogElement.prototype.close;
23
+
24
+ beforeEach(() => {
25
+ HTMLDialogElement.prototype.showModal = function (this: HTMLDialogElement) {
26
+ this.setAttribute("open", "");
27
+ };
28
+ HTMLDialogElement.prototype.close = function (this: HTMLDialogElement) {
29
+ this.removeAttribute("open");
30
+ };
31
+ });
32
+
33
+ afterEach(() => {
34
+ HTMLDialogElement.prototype.showModal = originalShowModal;
35
+ HTMLDialogElement.prototype.close = originalClose;
36
+ document.body.style.overflow = "";
37
+ document.body.style.paddingRight = "";
38
+ });
39
+
40
+ it("should render children when visible", () => {
41
+ render(
42
+ <Modal visible={true} onClose={() => {}}>
43
+ <div data-testid="content">Modal content</div>
44
+ </Modal>,
45
+ );
46
+
47
+ expect(screen.getByTestId("content")).toBeInTheDocument();
48
+ expect(screen.getByTestId("content")).toHaveTextContent("Modal content");
49
+ });
50
+
51
+ it("should render header with title when provided", () => {
52
+ render(
53
+ <Modal visible={true} onClose={() => {}} header={{ title: "Test Title" }}>
54
+ <div>Content</div>
55
+ </Modal>,
56
+ );
57
+
58
+ expect(screen.getByText("Test Title")).toBeInTheDocument();
59
+ });
60
+
61
+ it("should render close button in header when showCloseButton is true", () => {
62
+ render(
63
+ <Modal visible={true} onClose={() => {}} header={{ title: "Title", showCloseButton: true }}>
64
+ <div>Content</div>
65
+ </Modal>,
66
+ );
67
+
68
+ expect(screen.getByRole("button", { name: "Close modal" })).toBeInTheDocument();
69
+ });
70
+
71
+ it("should NOT render close button when showCloseButton is false", () => {
72
+ render(
73
+ <Modal visible={true} onClose={() => {}} header={{ title: "Title", showCloseButton: false }}>
74
+ <div>Content</div>
75
+ </Modal>,
76
+ );
77
+
78
+ expect(screen.queryByRole("button", { name: "Close modal" })).not.toBeInTheDocument();
79
+ });
80
+
81
+ it("should NOT render close button when dismissible is false", () => {
82
+ render(
83
+ <Modal visible={true} onClose={() => {}} dismissible={false} header={{ title: "Title", showCloseButton: true }}>
84
+ <div>Content</div>
85
+ </Modal>,
86
+ );
87
+
88
+ expect(screen.queryByRole("button", { name: "Close modal" })).not.toBeInTheDocument();
89
+ });
90
+
91
+ it("should call onClose when close button is clicked", () => {
92
+ const onClose = createCallTracker();
93
+ render(
94
+ <Modal visible={true} onClose={onClose.fn} header={{ title: "Title" }}>
95
+ <div>Content</div>
96
+ </Modal>,
97
+ );
98
+
99
+ const closeButton = screen.getByRole("button", { name: "Close modal" });
100
+ fireEvent.click(closeButton);
101
+
102
+ expect(onClose.calls).toHaveLength(1);
103
+ });
104
+
105
+ it("should call onClose when Escape is pressed", () => {
106
+ const onClose = createCallTracker();
107
+ render(
108
+ <Modal visible={true} onClose={onClose.fn}>
109
+ <div>Content</div>
110
+ </Modal>,
111
+ );
112
+
113
+ const dialog = document.querySelector("dialog");
114
+ fireEvent(dialog!, new Event("cancel", { bubbles: true, cancelable: true }));
115
+
116
+ expect(onClose.calls).toHaveLength(1);
117
+ });
118
+
119
+ it("should call onClose when backdrop is clicked", () => {
120
+ const onClose = createCallTracker();
121
+ render(
122
+ <Modal visible={true} onClose={onClose.fn}>
123
+ <div data-testid="content">Content</div>
124
+ </Modal>,
125
+ );
126
+
127
+ const dialog = document.querySelector("dialog");
128
+ fireEvent.click(dialog!);
129
+
130
+ expect(onClose.calls).toHaveLength(1);
131
+ });
132
+
133
+ it("should NOT call onClose when content is clicked", () => {
134
+ const onClose = createCallTracker();
135
+ render(
136
+ <Modal visible={true} onClose={onClose.fn}>
137
+ <div data-testid="content">Content</div>
138
+ </Modal>,
139
+ );
140
+
141
+ const content = screen.getByTestId("content");
142
+ fireEvent.click(content);
143
+
144
+ expect(onClose.calls).toHaveLength(0);
145
+ });
146
+
147
+ it("should NOT render chrome when chrome is false", () => {
148
+ render(
149
+ <Modal visible={true} onClose={() => {}} chrome={false} header={{ title: "Title" }}>
150
+ <div data-testid="content">Content</div>
151
+ </Modal>,
152
+ );
153
+
154
+ // Header should not be rendered when chrome is false
155
+ expect(screen.queryByText("Title")).not.toBeInTheDocument();
156
+ expect(screen.getByTestId("content")).toBeInTheDocument();
157
+ });
158
+
159
+ it("should set aria-label from header title", () => {
160
+ render(
161
+ <Modal visible={true} onClose={() => {}} header={{ title: "Settings Dialog" }}>
162
+ <div>Content</div>
163
+ </Modal>,
164
+ );
165
+
166
+ const dialog = document.querySelector("dialog");
167
+ expect(dialog).toHaveAttribute("aria-label", "Settings Dialog");
168
+ });
169
+
170
+ it("should use custom aria-label when provided", () => {
171
+ render(
172
+ <Modal visible={true} onClose={() => {}} ariaLabel="Custom Label" header={{ title: "Title" }}>
173
+ <div>Content</div>
174
+ </Modal>,
175
+ );
176
+
177
+ const dialog = document.querySelector("dialog");
178
+ expect(dialog).toHaveAttribute("aria-label", "Custom Label");
179
+ });
180
+
181
+ it("should handle input focus without conflict", () => {
182
+ render(
183
+ <Modal visible={true} onClose={() => {}}>
184
+ <input data-testid="input" type="text" />
185
+ </Modal>,
186
+ );
187
+
188
+ const input = screen.getByTestId("input") as HTMLInputElement;
189
+ input.focus();
190
+ fireEvent.change(input, { target: { value: "test" } });
191
+
192
+ expect(input.value).toBe("test");
193
+ });
194
+
195
+ it("should render with custom width and height", () => {
196
+ render(
197
+ <Modal visible={true} onClose={() => {}} width={500} height={400}>
198
+ <div data-testid="content">Content</div>
199
+ </Modal>,
200
+ );
201
+
202
+ const content = screen.getByTestId("content");
203
+ // Traverse: FloatingPanelContent > inner > overflow > shadow > computedStyle div
204
+ const styledDiv = content.parentElement?.parentElement?.parentElement?.parentElement?.parentElement;
205
+ expect(styledDiv).toHaveStyle({ width: "500px", height: "400px" });
206
+ });
207
+
208
+ it("should render with string width and height", () => {
209
+ render(
210
+ <Modal visible={true} onClose={() => {}} width="80%" height="auto">
211
+ <div data-testid="content">Content</div>
212
+ </Modal>,
213
+ );
214
+
215
+ const content = screen.getByTestId("content");
216
+ // Traverse: FloatingPanelContent > inner > overflow > shadow > computedStyle div
217
+ const styledDiv = content.parentElement?.parentElement?.parentElement?.parentElement?.parentElement;
218
+ expect(styledDiv).toHaveStyle({ width: "80%", height: "auto" });
219
+ });
220
+ });
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @file Modal component - Centered modal dialog with optional chrome
3
+ */
4
+ import * as React from "react";
5
+ import type { ModalProps } from "./types";
6
+ import { DialogContainer } from "./DialogContainer";
7
+ import {
8
+ FloatingPanelFrame,
9
+ FloatingPanelHeader,
10
+ FloatingPanelTitle,
11
+ FloatingPanelContent,
12
+ FloatingPanelCloseButton,
13
+ } from "../../components/paneling/FloatingPanelFrame";
14
+ import {
15
+ MODAL_MIN_WIDTH,
16
+ MODAL_MAX_WIDTH,
17
+ MODAL_MAX_HEIGHT,
18
+ FLOATING_PANEL_HEADER_PADDING_Y,
19
+ FLOATING_PANEL_HEADER_PADDING_X,
20
+ FLOATING_PANEL_GAP,
21
+ } from "../../constants/styles";
22
+
23
+ const modalContentStyle: React.CSSProperties = {
24
+ minWidth: MODAL_MIN_WIDTH,
25
+ maxWidth: MODAL_MAX_WIDTH,
26
+ maxHeight: MODAL_MAX_HEIGHT,
27
+ display: "flex",
28
+ flexDirection: "column",
29
+ };
30
+
31
+ type ModalContentProps = {
32
+ header?: ModalProps["header"];
33
+ dismissible: boolean;
34
+ onClose: () => void;
35
+ children: React.ReactNode;
36
+ contentStyle?: React.CSSProperties;
37
+ };
38
+
39
+ const ModalContentWithChrome: React.FC<ModalContentProps> = ({
40
+ header,
41
+ dismissible,
42
+ onClose,
43
+ children,
44
+ contentStyle,
45
+ }) => {
46
+ const showCloseButton = header?.showCloseButton ?? true;
47
+ const shouldShowClose = dismissible ? showCloseButton : false;
48
+
49
+ const hasHeader = header !== undefined;
50
+
51
+ return (
52
+ <FloatingPanelFrame>
53
+ <React.Activity mode={hasHeader ? "visible" : "hidden"}>
54
+ <FloatingPanelHeader
55
+ style={{
56
+ padding: `${FLOATING_PANEL_HEADER_PADDING_Y} ${FLOATING_PANEL_HEADER_PADDING_X}`,
57
+ gap: FLOATING_PANEL_GAP,
58
+ }}
59
+ >
60
+ <FloatingPanelTitle>{header?.title}</FloatingPanelTitle>
61
+ <React.Activity mode={shouldShowClose ? "visible" : "hidden"}>
62
+ <FloatingPanelCloseButton onClick={onClose} aria-label="Close modal" style={{ marginLeft: "auto" }} />
63
+ </React.Activity>
64
+ </FloatingPanelHeader>
65
+ </React.Activity>
66
+ <FloatingPanelContent style={{ flex: 1, overflow: "auto", ...contentStyle }}>{children}</FloatingPanelContent>
67
+ </FloatingPanelFrame>
68
+ );
69
+ };
70
+
71
+ type ModalContentRendererProps = ModalContentProps & {
72
+ chrome: boolean;
73
+ };
74
+
75
+ const ModalContentRenderer: React.FC<ModalContentRendererProps> = ({ chrome, children, ...chromeProps }) => {
76
+ if (chrome) {
77
+ return <ModalContentWithChrome {...chromeProps}>{children}</ModalContentWithChrome>;
78
+ }
79
+ return <>{children}</>;
80
+ };
81
+
82
+ /**
83
+ * Modal component for displaying centered dialogs.
84
+ * Uses native <dialog> element for proper accessibility and top-layer rendering.
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * const [isOpen, setIsOpen] = useState(false);
89
+ *
90
+ * <Modal
91
+ * visible={isOpen}
92
+ * onClose={() => setIsOpen(false)}
93
+ * header={{ title: "Settings" }}
94
+ * >
95
+ * <form>
96
+ * <input type="text" placeholder="Name" />
97
+ * <button type="submit">Save</button>
98
+ * </form>
99
+ * </Modal>
100
+ * ```
101
+ */
102
+ export const Modal: React.FC<ModalProps> = ({
103
+ visible,
104
+ onClose,
105
+ children,
106
+ header,
107
+ width,
108
+ height,
109
+ maxWidth,
110
+ maxHeight,
111
+ chrome = true,
112
+ dismissible = true,
113
+ closeOnEscape = true,
114
+ preventBodyScroll = true,
115
+ returnFocus = true,
116
+ ariaLabel,
117
+ ariaLabelledBy,
118
+ ariaDescribedBy,
119
+ transitionMode = "css",
120
+ transitionDuration,
121
+ transitionEasing,
122
+ contentStyle: propContentStyle,
123
+ swipeDismissible,
124
+ openDirection = "center",
125
+ useViewTransition = false,
126
+ }) => {
127
+ const computedStyle = React.useMemo((): React.CSSProperties => {
128
+ const style: React.CSSProperties = { ...modalContentStyle };
129
+
130
+ if (width !== undefined) {
131
+ style.width = typeof width === "number" ? `${width}px` : width;
132
+ }
133
+ if (height !== undefined) {
134
+ style.height = typeof height === "number" ? `${height}px` : height;
135
+ }
136
+ if (maxWidth !== undefined) {
137
+ style.maxWidth = typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth;
138
+ }
139
+ if (maxHeight !== undefined) {
140
+ style.maxHeight = typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight;
141
+ }
142
+
143
+ return style;
144
+ }, [width, height, maxWidth, maxHeight]);
145
+
146
+ const effectiveAriaLabel = ariaLabel ?? header?.title ?? "Modal";
147
+
148
+ return (
149
+ <DialogContainer
150
+ visible={visible}
151
+ onClose={onClose}
152
+ position="center"
153
+ dismissible={dismissible}
154
+ closeOnEscape={closeOnEscape}
155
+ preventBodyScroll={preventBodyScroll}
156
+ returnFocus={returnFocus}
157
+ ariaLabel={effectiveAriaLabel}
158
+ ariaLabelledBy={ariaLabelledBy}
159
+ ariaDescribedBy={ariaDescribedBy}
160
+ transitionMode={transitionMode}
161
+ transitionDuration={transitionDuration}
162
+ transitionEasing={transitionEasing}
163
+ swipeDismissible={swipeDismissible}
164
+ openDirection={openDirection}
165
+ useViewTransition={useViewTransition}
166
+ >
167
+ <div style={computedStyle}>
168
+ <ModalContentRenderer
169
+ chrome={chrome}
170
+ header={header}
171
+ dismissible={dismissible}
172
+ onClose={onClose}
173
+ contentStyle={propContentStyle}
174
+ >
175
+ {children}
176
+ </ModalContentRenderer>
177
+ </div>
178
+ </DialogContainer>
179
+ );
180
+ };
181
+
182
+ Modal.displayName = "Modal";
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @file Swipeable dialog container component.
3
+ *
4
+ * This component extends the base dialog functionality with swipe-to-dismiss
5
+ * and multi-phase animations. It uses native <dialog> element for top layer
6
+ * positioning and custom transform animations.
7
+ */
8
+ import * as React from "react";
9
+ import type { DialogContainerProps } from "./types.js";
10
+ import { useDialogContainer } from "./useDialogContainer.js";
11
+ import { useDialogSwipeInput } from "./useDialogSwipeInput.js";
12
+ import { useDialogTransform } from "./useDialogTransform.js";
13
+ import { COLOR_MODAL_BACKDROP } from "../../constants/styles.js";
14
+
15
+ const dialogBaseStyle: React.CSSProperties = {
16
+ border: "none",
17
+ padding: 0,
18
+ background: "transparent",
19
+ maxWidth: "none",
20
+ maxHeight: "none",
21
+ overflow: "visible",
22
+ };
23
+
24
+ const contentWrapperStyle: React.CSSProperties = {
25
+ display: "flex",
26
+ alignItems: "center",
27
+ justifyContent: "center",
28
+ position: "fixed",
29
+ inset: 0,
30
+ pointerEvents: "none",
31
+ };
32
+
33
+ const contentStyle: React.CSSProperties = {
34
+ pointerEvents: "auto",
35
+ willChange: "transform, opacity",
36
+ };
37
+
38
+ const backdropStyle: React.CSSProperties = {
39
+ position: "fixed",
40
+ inset: 0,
41
+ background: COLOR_MODAL_BACKDROP,
42
+ pointerEvents: "auto",
43
+ willChange: "opacity",
44
+ };
45
+
46
+ /**
47
+ * Swipeable dialog container with multi-phase animation.
48
+ *
49
+ * Uses native <dialog> for top layer positioning and custom JS animations
50
+ * for swipe-to-dismiss functionality.
51
+ */
52
+ export const SwipeDialogContainer: React.FC<DialogContainerProps> = ({
53
+ visible,
54
+ onClose,
55
+ children,
56
+ position = "center",
57
+ dismissible = true,
58
+ closeOnEscape = true,
59
+ returnFocus = true,
60
+ preventBodyScroll = true,
61
+ ariaLabel,
62
+ ariaLabelledBy,
63
+ ariaDescribedBy,
64
+ swipeDismissible = true,
65
+ openDirection = "center",
66
+ useViewTransition = false,
67
+ }) => {
68
+ const contentRef = React.useRef<HTMLDivElement>(null);
69
+ const backdropRef = React.useRef<HTMLDivElement>(null);
70
+
71
+ // Base dialog lifecycle management
72
+ const { dialogRef, dialogProps } = useDialogContainer({
73
+ visible,
74
+ onClose,
75
+ dismissible,
76
+ closeOnEscape,
77
+ returnFocus,
78
+ preventBodyScroll,
79
+ });
80
+
81
+ // Swipe input detection - free 2D movement
82
+ const swipeEnabled = swipeDismissible ? visible : false;
83
+ const {
84
+ state: swipeState,
85
+ containerProps: swipeContainerProps,
86
+ displacement,
87
+ displacement2D,
88
+ isOperating,
89
+ } = useDialogSwipeInput({
90
+ containerRef: contentRef,
91
+ openDirection,
92
+ enabled: swipeEnabled,
93
+ onSwipeDismiss: onClose,
94
+ });
95
+
96
+ // Transform and animation management
97
+ const { phase, isAnimating, triggerClose } = useDialogTransform({
98
+ elementRef: contentRef,
99
+ backdropRef,
100
+ visible,
101
+ openDirection,
102
+ swipeState,
103
+ displacement,
104
+ displacement2D,
105
+ useViewTransition,
106
+ onCloseComplete: onClose,
107
+ });
108
+
109
+ // Handle backdrop click
110
+ const handleBackdropClick = React.useCallback(() => {
111
+ if (!dismissible) {
112
+ return;
113
+ }
114
+ if (isOperating) {
115
+ return;
116
+ }
117
+ if (isAnimating) {
118
+ return;
119
+ }
120
+ triggerClose();
121
+ }, [dismissible, isOperating, isAnimating, triggerClose]);
122
+
123
+ // Compute content wrapper style based on position
124
+ const computedContentWrapperStyle = React.useMemo((): React.CSSProperties => {
125
+ if (position === "center") {
126
+ return contentWrapperStyle;
127
+ }
128
+ // Absolute position
129
+ const style: React.CSSProperties = {
130
+ ...contentWrapperStyle,
131
+ alignItems: "flex-start",
132
+ justifyContent: "flex-start",
133
+ };
134
+ if (position.x !== undefined) {
135
+ style.left = position.x;
136
+ }
137
+ if (position.y !== undefined) {
138
+ style.top = position.y;
139
+ }
140
+ return style;
141
+ }, [position]);
142
+
143
+ // Merge swipe container props with content style
144
+ // Note: We don't use React.Activity here because it uses display:none,
145
+ // which prevents dimension measurement needed for animations.
146
+ // Instead, we use visibility and opacity controlled by the animation.
147
+ const mergedContentStyle = React.useMemo((): React.CSSProperties => {
148
+ const style: React.CSSProperties = {
149
+ ...contentStyle,
150
+ ...swipeContainerProps.style,
151
+ };
152
+
153
+ // When closed, hide the content but keep it in layout for dimension measurement
154
+ if (phase === "closed") {
155
+ style.visibility = "hidden";
156
+ style.pointerEvents = "none";
157
+ }
158
+
159
+ return style;
160
+ }, [swipeContainerProps.style, phase]);
161
+
162
+ // Stop propagation on content pointer events
163
+ const handleContentPointerDown = React.useCallback((event: React.PointerEvent<HTMLDivElement>) => {
164
+ swipeContainerProps.onPointerDown?.(event);
165
+ // Don't stop propagation - let swipe input handle it
166
+ }, [swipeContainerProps]);
167
+
168
+ // Hide dialog styling (we use our own backdrop)
169
+ const hideNativeBackdrop = `
170
+ dialog::backdrop {
171
+ background: transparent;
172
+ }
173
+ `;
174
+
175
+ return (
176
+ <>
177
+ <style>{hideNativeBackdrop}</style>
178
+ <dialog
179
+ ref={dialogRef}
180
+ style={dialogBaseStyle}
181
+ aria-label={ariaLabel}
182
+ aria-labelledby={ariaLabelledBy}
183
+ aria-describedby={ariaDescribedBy}
184
+ {...dialogProps}
185
+ >
186
+ {/* Custom backdrop for animated opacity */}
187
+ <div
188
+ ref={backdropRef}
189
+ style={backdropStyle}
190
+ onClick={handleBackdropClick}
191
+ aria-hidden="true"
192
+ />
193
+
194
+ <div style={computedContentWrapperStyle}>
195
+ <div
196
+ ref={contentRef}
197
+ style={mergedContentStyle}
198
+ onPointerDown={handleContentPointerDown}
199
+ >
200
+ {children}
201
+ </div>
202
+ </div>
203
+ </dialog>
204
+ </>
205
+ );
206
+ };
207
+
208
+ SwipeDialogContainer.displayName = "SwipeDialogContainer";