@telegraph/modal 0.1.0 → 0.1.2
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.
- package/CHANGELOG.md +18 -0
- package/README.md +1044 -10
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.mjs +1688 -1676
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
# 🪟 Modal
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Accessible modal dialog component with stacking support, animations, and focus management built on Radix UI.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/@telegraph/modal)
|
|
8
|
+
[](https://bundlephobia.com/result?p=@telegraph/modal)
|
|
9
|
+
[](https://github.com/knocklabs/telegraph/blob/main/LICENSE)
|
|
8
10
|
|
|
9
|
-
## Installation
|
|
11
|
+
## Installation
|
|
10
12
|
|
|
11
|
-
```
|
|
13
|
+
```bash
|
|
12
14
|
npm install @telegraph/modal
|
|
13
15
|
```
|
|
14
16
|
|
|
@@ -18,15 +20,1047 @@ Pick one:
|
|
|
18
20
|
|
|
19
21
|
Via CSS (preferred):
|
|
20
22
|
|
|
21
|
-
```
|
|
22
|
-
@import "@telegraph/modal"
|
|
23
|
+
```css
|
|
24
|
+
@import "@telegraph/modal";
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
Via Javascript:
|
|
26
28
|
|
|
27
|
-
```
|
|
28
|
-
import "@telegraph/modal/default.css"
|
|
29
|
+
```tsx
|
|
30
|
+
import "@telegraph/modal/default.css";
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
> Then, include `className="tgph"` on the farthest parent element wrapping the telegraph components
|
|
32
34
|
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { Button } from "@telegraph/button";
|
|
39
|
+
import { Modal } from "@telegraph/modal";
|
|
40
|
+
import { useState } from "react";
|
|
41
|
+
|
|
42
|
+
export const BasicModal = () => {
|
|
43
|
+
const [open, setOpen] = useState(false);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<Button onClick={() => setOpen(true)}>Open Modal</Button>
|
|
48
|
+
|
|
49
|
+
<Modal.Root open={open} onOpenChange={setOpen} a11yTitle="Settings">
|
|
50
|
+
<Modal.Content>
|
|
51
|
+
<Modal.Header>
|
|
52
|
+
<h2>Settings</h2>
|
|
53
|
+
<Modal.Close />
|
|
54
|
+
</Modal.Header>
|
|
55
|
+
|
|
56
|
+
<Modal.Body>
|
|
57
|
+
<p>Configure your application settings here.</p>
|
|
58
|
+
</Modal.Body>
|
|
59
|
+
|
|
60
|
+
<Modal.Footer>
|
|
61
|
+
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
62
|
+
Cancel
|
|
63
|
+
</Button>
|
|
64
|
+
<Button onClick={() => setOpen(false)}>Save Changes</Button>
|
|
65
|
+
</Modal.Footer>
|
|
66
|
+
</Modal.Content>
|
|
67
|
+
</Modal.Root>
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### `<Modal.Root>`
|
|
76
|
+
|
|
77
|
+
The root modal container that manages state and provides context.
|
|
78
|
+
|
|
79
|
+
| Prop | Type | Default | Description |
|
|
80
|
+
| -------------------- | ------------------------- | ----------- | ------------------------------------------ |
|
|
81
|
+
| `open` | `boolean` | `undefined` | Controlled open state |
|
|
82
|
+
| `defaultOpen` | `boolean` | `false` | Default open state |
|
|
83
|
+
| `onOpenChange` | `(open: boolean) => void` | `undefined` | Callback when open state changes |
|
|
84
|
+
| `a11yTitle` | `string` | required | Accessible title for screen readers |
|
|
85
|
+
| `a11yDescription` | `string` | `undefined` | Accessible description for screen readers |
|
|
86
|
+
| `trapped` | `boolean` | `true` | Whether focus should be trapped |
|
|
87
|
+
| `onMountAutoFocus` | `(event: Event) => void` | `undefined` | Called when modal opens and focuses |
|
|
88
|
+
| `onUnmountAutoFocus` | `(event: Event) => void` | `undefined` | Called when modal closes and focus returns |
|
|
89
|
+
| `layer` | `number` | `undefined` | Layer index for stacked modals |
|
|
90
|
+
|
|
91
|
+
Inherits all Stack props for additional styling.
|
|
92
|
+
|
|
93
|
+
### `<Modal.Content>`
|
|
94
|
+
|
|
95
|
+
The content wrapper that handles the modal's main container.
|
|
96
|
+
|
|
97
|
+
| Prop | Type | Default | Description |
|
|
98
|
+
| ---------- | ----------------- | -------- | ------------- |
|
|
99
|
+
| `children` | `React.ReactNode` | required | Modal content |
|
|
100
|
+
|
|
101
|
+
Inherits all Stack props for layout and styling.
|
|
102
|
+
|
|
103
|
+
### `<Modal.Header>`
|
|
104
|
+
|
|
105
|
+
Header section typically containing title and close button.
|
|
106
|
+
|
|
107
|
+
| Prop | Type | Default | Description |
|
|
108
|
+
| ---------- | ----------------- | -------- | -------------- |
|
|
109
|
+
| `children` | `React.ReactNode` | required | Header content |
|
|
110
|
+
|
|
111
|
+
Inherits all Stack props for layout and styling.
|
|
112
|
+
|
|
113
|
+
### `<Modal.Body>`
|
|
114
|
+
|
|
115
|
+
Main content area with automatic scrolling when content overflows.
|
|
116
|
+
|
|
117
|
+
| Prop | Type | Default | Description |
|
|
118
|
+
| ---------- | ----------------- | -------- | ------------ |
|
|
119
|
+
| `children` | `React.ReactNode` | required | Body content |
|
|
120
|
+
|
|
121
|
+
Inherits all Stack props for layout and styling.
|
|
122
|
+
|
|
123
|
+
### `<Modal.Footer>`
|
|
124
|
+
|
|
125
|
+
Footer section typically containing action buttons.
|
|
126
|
+
|
|
127
|
+
| Prop | Type | Default | Description |
|
|
128
|
+
| ---------- | ----------------- | -------- | -------------- |
|
|
129
|
+
| `children` | `React.ReactNode` | required | Footer content |
|
|
130
|
+
|
|
131
|
+
Inherits all Stack props for layout and styling.
|
|
132
|
+
|
|
133
|
+
### `<Modal.Close>`
|
|
134
|
+
|
|
135
|
+
Close button that dismisses the modal.
|
|
136
|
+
|
|
137
|
+
| Prop | Type | Default | Description |
|
|
138
|
+
| --------- | --------------- | --------- | -------------- |
|
|
139
|
+
| `size` | `ButtonSize` | `"1"` | Button size |
|
|
140
|
+
| `variant` | `ButtonVariant` | `"ghost"` | Button variant |
|
|
141
|
+
|
|
142
|
+
Inherits all Button props for additional styling.
|
|
143
|
+
|
|
144
|
+
### `<ModalStackingProvider>`
|
|
145
|
+
|
|
146
|
+
Provider for managing multiple stacked modals.
|
|
147
|
+
|
|
148
|
+
| Prop | Type | Default | Description |
|
|
149
|
+
| ---------- | ----------------- | -------- | ----------- |
|
|
150
|
+
| `children` | `React.ReactNode` | required | App content |
|
|
151
|
+
|
|
152
|
+
## Usage Patterns
|
|
153
|
+
|
|
154
|
+
### Basic Modal
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { Button } from "@telegraph/button";
|
|
158
|
+
import { Modal } from "@telegraph/modal";
|
|
159
|
+
import { useState } from "react";
|
|
160
|
+
|
|
161
|
+
const BasicModal = () => {
|
|
162
|
+
const [open, setOpen] = useState(false);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<>
|
|
166
|
+
<Button onClick={() => setOpen(true)}>Open Modal</Button>
|
|
167
|
+
|
|
168
|
+
<Modal.Root open={open} onOpenChange={setOpen} a11yTitle="Basic Modal">
|
|
169
|
+
<Modal.Content>
|
|
170
|
+
<Modal.Header>
|
|
171
|
+
<h2>Modal Title</h2>
|
|
172
|
+
<Modal.Close />
|
|
173
|
+
</Modal.Header>
|
|
174
|
+
|
|
175
|
+
<Modal.Body>
|
|
176
|
+
<p>This is the modal content.</p>
|
|
177
|
+
</Modal.Body>
|
|
178
|
+
</Modal.Content>
|
|
179
|
+
</Modal.Root>
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Confirmation Modal
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { Button } from "@telegraph/button";
|
|
189
|
+
import { Modal } from "@telegraph/modal";
|
|
190
|
+
import { AlertTriangle } from "lucide-react";
|
|
191
|
+
|
|
192
|
+
const ConfirmationModal = ({ open, onClose, onConfirm, title, message }) => (
|
|
193
|
+
<Modal.Root open={open} onOpenChange={onClose} a11yTitle={title}>
|
|
194
|
+
<Modal.Content maxW="96">
|
|
195
|
+
<Modal.Header>
|
|
196
|
+
<Stack direction="row" align="center" gap="2">
|
|
197
|
+
<Icon icon={AlertTriangle} color="red" />
|
|
198
|
+
<h2>{title}</h2>
|
|
199
|
+
</Stack>
|
|
200
|
+
<Modal.Close />
|
|
201
|
+
</Modal.Header>
|
|
202
|
+
|
|
203
|
+
<Modal.Body>
|
|
204
|
+
<p>{message}</p>
|
|
205
|
+
</Modal.Body>
|
|
206
|
+
|
|
207
|
+
<Modal.Footer>
|
|
208
|
+
<Button variant="outline" onClick={onClose}>
|
|
209
|
+
Cancel
|
|
210
|
+
</Button>
|
|
211
|
+
<Button color="red" onClick={onConfirm}>
|
|
212
|
+
Confirm
|
|
213
|
+
</Button>
|
|
214
|
+
</Modal.Footer>
|
|
215
|
+
</Modal.Content>
|
|
216
|
+
</Modal.Root>
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Usage
|
|
220
|
+
<ConfirmationModal
|
|
221
|
+
open={deleteModalOpen}
|
|
222
|
+
onClose={() => setDeleteModalOpen(false)}
|
|
223
|
+
onConfirm={handleDelete}
|
|
224
|
+
title="Delete Item"
|
|
225
|
+
message="Are you sure you want to delete this item? This action cannot be undone."
|
|
226
|
+
/>;
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Form Modal
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
import { Button } from "@telegraph/button";
|
|
233
|
+
import { Input } from "@telegraph/input";
|
|
234
|
+
import { Stack } from "@telegraph/layout";
|
|
235
|
+
import { Modal } from "@telegraph/modal";
|
|
236
|
+
import { useState } from "react";
|
|
237
|
+
|
|
238
|
+
const FormModal = ({ open, onClose, onSubmit }) => {
|
|
239
|
+
const [formData, setFormData] = useState({ name: "", email: "" });
|
|
240
|
+
|
|
241
|
+
const handleSubmit = (e) => {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
onSubmit(formData);
|
|
244
|
+
onClose();
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<Modal.Root open={open} onOpenChange={onClose} a11yTitle="Add User">
|
|
249
|
+
<Modal.Content>
|
|
250
|
+
<Modal.Header>
|
|
251
|
+
<h2>Add New User</h2>
|
|
252
|
+
<Modal.Close />
|
|
253
|
+
</Modal.Header>
|
|
254
|
+
|
|
255
|
+
<Modal.Body>
|
|
256
|
+
<form onSubmit={handleSubmit}>
|
|
257
|
+
<Stack direction="column" gap="4">
|
|
258
|
+
<Stack direction="column" gap="1">
|
|
259
|
+
<label htmlFor="name">Name</label>
|
|
260
|
+
<Input
|
|
261
|
+
id="name"
|
|
262
|
+
value={formData.name}
|
|
263
|
+
onChange={(e) =>
|
|
264
|
+
setFormData((prev) => ({ ...prev, name: e.target.value }))
|
|
265
|
+
}
|
|
266
|
+
placeholder="Enter name"
|
|
267
|
+
/>
|
|
268
|
+
</Stack>
|
|
269
|
+
|
|
270
|
+
<Stack direction="column" gap="1">
|
|
271
|
+
<label htmlFor="email">Email</label>
|
|
272
|
+
<Input
|
|
273
|
+
id="email"
|
|
274
|
+
type="email"
|
|
275
|
+
value={formData.email}
|
|
276
|
+
onChange={(e) =>
|
|
277
|
+
setFormData((prev) => ({ ...prev, email: e.target.value }))
|
|
278
|
+
}
|
|
279
|
+
placeholder="Enter email"
|
|
280
|
+
/>
|
|
281
|
+
</Stack>
|
|
282
|
+
</Stack>
|
|
283
|
+
</form>
|
|
284
|
+
</Modal.Body>
|
|
285
|
+
|
|
286
|
+
<Modal.Footer>
|
|
287
|
+
<Button variant="outline" onClick={onClose}>
|
|
288
|
+
Cancel
|
|
289
|
+
</Button>
|
|
290
|
+
<Button onClick={handleSubmit}>Add User</Button>
|
|
291
|
+
</Modal.Footer>
|
|
292
|
+
</Modal.Content>
|
|
293
|
+
</Modal.Root>
|
|
294
|
+
);
|
|
295
|
+
};
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Full-Screen Modal
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import { Modal } from "@telegraph/modal";
|
|
302
|
+
|
|
303
|
+
const FullScreenModal = ({ open, onClose, children }) => (
|
|
304
|
+
<Modal.Root open={open} onOpenChange={onClose} a11yTitle="Full Screen View">
|
|
305
|
+
<Modal.Content w="screen" maxW="screen" h="screen" rounded="0">
|
|
306
|
+
<Modal.Header>
|
|
307
|
+
<h2>Full Screen Modal</h2>
|
|
308
|
+
<Modal.Close />
|
|
309
|
+
</Modal.Header>
|
|
310
|
+
|
|
311
|
+
<Modal.Body flex="1">{children}</Modal.Body>
|
|
312
|
+
</Modal.Content>
|
|
313
|
+
</Modal.Root>
|
|
314
|
+
);
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Advanced Usage
|
|
318
|
+
|
|
319
|
+
### Stacked Modals
|
|
320
|
+
|
|
321
|
+
For applications that need multiple modals open simultaneously:
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
import { Button } from "@telegraph/button";
|
|
325
|
+
import { Modal, ModalStackingProvider } from "@telegraph/modal";
|
|
326
|
+
import { useState } from "react";
|
|
327
|
+
|
|
328
|
+
const StackedModalsExample = () => {
|
|
329
|
+
const [modal1Open, setModal1Open] = useState(false);
|
|
330
|
+
const [modal2Open, setModal2Open] = useState(false);
|
|
331
|
+
const [modal3Open, setModal3Open] = useState(false);
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<ModalStackingProvider>
|
|
335
|
+
<Button onClick={() => setModal1Open(true)}>Open First Modal</Button>
|
|
336
|
+
|
|
337
|
+
{/* First Modal */}
|
|
338
|
+
<Modal.Root
|
|
339
|
+
open={modal1Open}
|
|
340
|
+
onOpenChange={setModal1Open}
|
|
341
|
+
a11yTitle="First Modal"
|
|
342
|
+
>
|
|
343
|
+
<Modal.Content>
|
|
344
|
+
<Modal.Header>
|
|
345
|
+
<h2>First Modal</h2>
|
|
346
|
+
<Modal.Close />
|
|
347
|
+
</Modal.Header>
|
|
348
|
+
|
|
349
|
+
<Modal.Body>
|
|
350
|
+
<p>This is the first modal.</p>
|
|
351
|
+
<Button onClick={() => setModal2Open(true)}>
|
|
352
|
+
Open Second Modal
|
|
353
|
+
</Button>
|
|
354
|
+
</Modal.Body>
|
|
355
|
+
</Modal.Content>
|
|
356
|
+
</Modal.Root>
|
|
357
|
+
|
|
358
|
+
{/* Second Modal */}
|
|
359
|
+
<Modal.Root
|
|
360
|
+
open={modal2Open}
|
|
361
|
+
onOpenChange={setModal2Open}
|
|
362
|
+
a11yTitle="Second Modal"
|
|
363
|
+
>
|
|
364
|
+
<Modal.Content>
|
|
365
|
+
<Modal.Header>
|
|
366
|
+
<h2>Second Modal</h2>
|
|
367
|
+
<Modal.Close />
|
|
368
|
+
</Modal.Header>
|
|
369
|
+
|
|
370
|
+
<Modal.Body>
|
|
371
|
+
<p>This modal is stacked on top.</p>
|
|
372
|
+
<Button onClick={() => setModal3Open(true)}>
|
|
373
|
+
Open Third Modal
|
|
374
|
+
</Button>
|
|
375
|
+
</Modal.Body>
|
|
376
|
+
</Modal.Content>
|
|
377
|
+
</Modal.Root>
|
|
378
|
+
|
|
379
|
+
{/* Third Modal */}
|
|
380
|
+
<Modal.Root
|
|
381
|
+
open={modal3Open}
|
|
382
|
+
onOpenChange={setModal3Open}
|
|
383
|
+
a11yTitle="Third Modal"
|
|
384
|
+
>
|
|
385
|
+
<Modal.Content>
|
|
386
|
+
<Modal.Header>
|
|
387
|
+
<h2>Third Modal</h2>
|
|
388
|
+
<Modal.Close />
|
|
389
|
+
</Modal.Header>
|
|
390
|
+
|
|
391
|
+
<Modal.Body>
|
|
392
|
+
<p>This is the top-most modal.</p>
|
|
393
|
+
</Modal.Body>
|
|
394
|
+
</Modal.Content>
|
|
395
|
+
</Modal.Root>
|
|
396
|
+
</ModalStackingProvider>
|
|
397
|
+
);
|
|
398
|
+
};
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Custom Focus Management
|
|
402
|
+
|
|
403
|
+
```tsx
|
|
404
|
+
import { Modal } from "@telegraph/modal";
|
|
405
|
+
import { useRef } from "react";
|
|
406
|
+
|
|
407
|
+
const CustomFocusModal = ({ open, onClose }) => {
|
|
408
|
+
const firstInputRef = useRef(null);
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<Modal.Root
|
|
412
|
+
open={open}
|
|
413
|
+
onOpenChange={onClose}
|
|
414
|
+
a11yTitle="Custom Focus"
|
|
415
|
+
onMountAutoFocus={(event) => {
|
|
416
|
+
event.preventDefault();
|
|
417
|
+
firstInputRef.current?.focus();
|
|
418
|
+
}}
|
|
419
|
+
onUnmountAutoFocus={(event) => {
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
// Custom focus return logic
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
424
|
+
<Modal.Content>
|
|
425
|
+
<Modal.Header>
|
|
426
|
+
<h2>Custom Focus Management</h2>
|
|
427
|
+
<Modal.Close />
|
|
428
|
+
</Modal.Header>
|
|
429
|
+
|
|
430
|
+
<Modal.Body>
|
|
431
|
+
<Input ref={firstInputRef} placeholder="This will be focused" />
|
|
432
|
+
<Input placeholder="Second input" />
|
|
433
|
+
</Modal.Body>
|
|
434
|
+
</Modal.Content>
|
|
435
|
+
</Modal.Root>
|
|
436
|
+
);
|
|
437
|
+
};
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Modal with Loading State
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
import { Button } from "@telegraph/button";
|
|
444
|
+
import { Modal } from "@telegraph/modal";
|
|
445
|
+
import { Spinner } from "@telegraph/spinner";
|
|
446
|
+
import { useState } from "react";
|
|
447
|
+
|
|
448
|
+
const LoadingModal = ({ open, onClose }) => {
|
|
449
|
+
const [loading, setLoading] = useState(false);
|
|
450
|
+
|
|
451
|
+
const handleSave = async () => {
|
|
452
|
+
setLoading(true);
|
|
453
|
+
try {
|
|
454
|
+
await saveData();
|
|
455
|
+
onClose();
|
|
456
|
+
} catch (error) {
|
|
457
|
+
console.error("Save failed:", error);
|
|
458
|
+
} finally {
|
|
459
|
+
setLoading(false);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
return (
|
|
464
|
+
<Modal.Root open={open} onOpenChange={onClose} a11yTitle="Save Changes">
|
|
465
|
+
<Modal.Content>
|
|
466
|
+
<Modal.Header>
|
|
467
|
+
<h2>Save Changes</h2>
|
|
468
|
+
<Modal.Close disabled={loading} />
|
|
469
|
+
</Modal.Header>
|
|
470
|
+
|
|
471
|
+
<Modal.Body>
|
|
472
|
+
{loading ? (
|
|
473
|
+
<Stack align="center" gap="2">
|
|
474
|
+
<Spinner />
|
|
475
|
+
<p>Saving changes...</p>
|
|
476
|
+
</Stack>
|
|
477
|
+
) : (
|
|
478
|
+
<p>Are you sure you want to save these changes?</p>
|
|
479
|
+
)}
|
|
480
|
+
</Modal.Body>
|
|
481
|
+
|
|
482
|
+
<Modal.Footer>
|
|
483
|
+
<Button variant="outline" onClick={onClose} disabled={loading}>
|
|
484
|
+
Cancel
|
|
485
|
+
</Button>
|
|
486
|
+
<Button onClick={handleSave} disabled={loading}>
|
|
487
|
+
{loading ? "Saving..." : "Save"}
|
|
488
|
+
</Button>
|
|
489
|
+
</Modal.Footer>
|
|
490
|
+
</Modal.Content>
|
|
491
|
+
</Modal.Root>
|
|
492
|
+
);
|
|
493
|
+
};
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Modal with Custom Animation
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
import { Modal } from "@telegraph/modal";
|
|
500
|
+
import { motion } from "motion/react";
|
|
501
|
+
|
|
502
|
+
const AnimatedModal = ({ open, onClose }) => (
|
|
503
|
+
<Modal.Root open={open} onOpenChange={onClose} a11yTitle="Animated Modal">
|
|
504
|
+
<Modal.Content
|
|
505
|
+
as={motion.div}
|
|
506
|
+
initial={{ scale: 0.8, opacity: 0 }}
|
|
507
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
508
|
+
exit={{ scale: 0.8, opacity: 0 }}
|
|
509
|
+
transition={{ duration: 0.2 }}
|
|
510
|
+
>
|
|
511
|
+
<Modal.Header>
|
|
512
|
+
<h2>Animated Modal</h2>
|
|
513
|
+
<Modal.Close />
|
|
514
|
+
</Modal.Header>
|
|
515
|
+
|
|
516
|
+
<Modal.Body>
|
|
517
|
+
<p>This modal has custom animations.</p>
|
|
518
|
+
</Modal.Body>
|
|
519
|
+
</Modal.Content>
|
|
520
|
+
</Modal.Root>
|
|
521
|
+
);
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Controlled Modal
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
import { Button } from "@telegraph/button";
|
|
528
|
+
import { Modal } from "@telegraph/modal";
|
|
529
|
+
|
|
530
|
+
// For when you need more control over modal behavior
|
|
531
|
+
const ControlledModal = () => {
|
|
532
|
+
const [open, setOpen] = useState(false);
|
|
533
|
+
const [canClose, setCanClose] = useState(true);
|
|
534
|
+
|
|
535
|
+
const handleOpenChange = (newOpen) => {
|
|
536
|
+
if (!newOpen && !canClose) {
|
|
537
|
+
// Prevent closing if conditions aren't met
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
setOpen(newOpen);
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
return (
|
|
544
|
+
<>
|
|
545
|
+
<Button onClick={() => setOpen(true)}>Open Modal</Button>
|
|
546
|
+
|
|
547
|
+
<Modal.Root
|
|
548
|
+
open={open}
|
|
549
|
+
onOpenChange={handleOpenChange}
|
|
550
|
+
a11yTitle="Controlled Modal"
|
|
551
|
+
>
|
|
552
|
+
<Modal.Content>
|
|
553
|
+
<Modal.Header>
|
|
554
|
+
<h2>Controlled Modal</h2>
|
|
555
|
+
{canClose && <Modal.Close />}
|
|
556
|
+
</Modal.Header>
|
|
557
|
+
|
|
558
|
+
<Modal.Body>
|
|
559
|
+
<p>This modal's closing behavior is controlled.</p>
|
|
560
|
+
<Button onClick={() => setCanClose(!canClose)}>
|
|
561
|
+
{canClose ? "Disable" : "Enable"} Closing
|
|
562
|
+
</Button>
|
|
563
|
+
</Modal.Body>
|
|
564
|
+
|
|
565
|
+
<Modal.Footer>
|
|
566
|
+
<Button onClick={() => setOpen(false)} disabled={!canClose}>
|
|
567
|
+
Done
|
|
568
|
+
</Button>
|
|
569
|
+
</Modal.Footer>
|
|
570
|
+
</Modal.Content>
|
|
571
|
+
</Modal.Root>
|
|
572
|
+
</>
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Modal with Steps/Wizard
|
|
578
|
+
|
|
579
|
+
```tsx
|
|
580
|
+
import { Button } from "@telegraph/button";
|
|
581
|
+
import { Stack } from "@telegraph/layout";
|
|
582
|
+
import { Modal } from "@telegraph/modal";
|
|
583
|
+
import { useState } from "react";
|
|
584
|
+
|
|
585
|
+
const WizardModal = ({ open, onClose }) => {
|
|
586
|
+
const [currentStep, setCurrentStep] = useState(0);
|
|
587
|
+
|
|
588
|
+
const steps = [
|
|
589
|
+
{ title: "Step 1", content: "First step content" },
|
|
590
|
+
{ title: "Step 2", content: "Second step content" },
|
|
591
|
+
{ title: "Step 3", content: "Final step content" },
|
|
592
|
+
];
|
|
593
|
+
|
|
594
|
+
const isFirstStep = currentStep === 0;
|
|
595
|
+
const isLastStep = currentStep === steps.length - 1;
|
|
596
|
+
|
|
597
|
+
return (
|
|
598
|
+
<Modal.Root open={open} onOpenChange={onClose} a11yTitle="Setup Wizard">
|
|
599
|
+
<Modal.Content>
|
|
600
|
+
<Modal.Header>
|
|
601
|
+
<h2>{steps[currentStep].title}</h2>
|
|
602
|
+
<Modal.Close />
|
|
603
|
+
</Modal.Header>
|
|
604
|
+
|
|
605
|
+
<Modal.Body>
|
|
606
|
+
<Stack direction="column" gap="4">
|
|
607
|
+
{/* Progress indicator */}
|
|
608
|
+
<Stack direction="row" gap="1">
|
|
609
|
+
{steps.map((_, index) => (
|
|
610
|
+
<Box
|
|
611
|
+
key={index}
|
|
612
|
+
w="8"
|
|
613
|
+
h="1"
|
|
614
|
+
bg={index <= currentStep ? "accent-9" : "gray-6"}
|
|
615
|
+
rounded="full"
|
|
616
|
+
/>
|
|
617
|
+
))}
|
|
618
|
+
</Stack>
|
|
619
|
+
|
|
620
|
+
<p>{steps[currentStep].content}</p>
|
|
621
|
+
</Stack>
|
|
622
|
+
</Modal.Body>
|
|
623
|
+
|
|
624
|
+
<Modal.Footer>
|
|
625
|
+
<Button
|
|
626
|
+
variant="outline"
|
|
627
|
+
onClick={() => setCurrentStep((prev) => prev - 1)}
|
|
628
|
+
disabled={isFirstStep}
|
|
629
|
+
>
|
|
630
|
+
Previous
|
|
631
|
+
</Button>
|
|
632
|
+
|
|
633
|
+
{isLastStep ? (
|
|
634
|
+
<Button onClick={onClose}>Finish</Button>
|
|
635
|
+
) : (
|
|
636
|
+
<Button onClick={() => setCurrentStep((prev) => prev + 1)}>
|
|
637
|
+
Next
|
|
638
|
+
</Button>
|
|
639
|
+
)}
|
|
640
|
+
</Modal.Footer>
|
|
641
|
+
</Modal.Content>
|
|
642
|
+
</Modal.Root>
|
|
643
|
+
);
|
|
644
|
+
};
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
## Design Tokens & Styling
|
|
648
|
+
|
|
649
|
+
The modal component uses Telegraph design tokens for consistent styling:
|
|
650
|
+
|
|
651
|
+
### Layout Tokens
|
|
652
|
+
|
|
653
|
+
- **Max width**: `var(--tgph-spacing-160)` (default)
|
|
654
|
+
- **Margin**: `var(--tgph-spacing-16)` from viewport edges
|
|
655
|
+
- **Border radius**: `var(--tgph-rounded-4)`
|
|
656
|
+
- **Shadow**: `var(--tgph-shadow-3)`
|
|
657
|
+
|
|
658
|
+
### Color Tokens
|
|
659
|
+
|
|
660
|
+
- **Background**: `var(--tgph-surface-1)`
|
|
661
|
+
- **Overlay**: `var(--tgph-alpha-black-6)`
|
|
662
|
+
- **Border**: `var(--tgph-gray-8)`
|
|
663
|
+
|
|
664
|
+
### Z-Index Tokens
|
|
665
|
+
|
|
666
|
+
- **Overlay**: `var(--tgph-zIndex-overlay)`
|
|
667
|
+
- **Modal**: `var(--tgph-zIndex-modal)`
|
|
668
|
+
- **Stacked modals**: `calc(var(--tgph-zIndex-modal) + layer)`
|
|
669
|
+
|
|
670
|
+
### Animation Tokens
|
|
671
|
+
|
|
672
|
+
- **Duration**: `0.3s` spring transition
|
|
673
|
+
- **Stacking offset**: `var(--tgph-spacing-4)` per layer
|
|
674
|
+
- **Scale reduction**: `0.02` per background layer
|
|
675
|
+
|
|
676
|
+
## Accessibility
|
|
677
|
+
|
|
678
|
+
- ✅ **Focus Management**: Automatic focus trapping and restoration
|
|
679
|
+
- ✅ **Keyboard Navigation**: Escape key closes modal
|
|
680
|
+
- ✅ **Screen Reader Support**: Proper ARIA roles and descriptions
|
|
681
|
+
- ✅ **Content Structure**: Semantic heading hierarchy
|
|
682
|
+
- ✅ **Backdrop Interaction**: Click outside to close
|
|
683
|
+
- ✅ **Focus Restoration**: Returns focus to trigger element
|
|
684
|
+
|
|
685
|
+
### Keyboard Shortcuts
|
|
686
|
+
|
|
687
|
+
| Key | Action |
|
|
688
|
+
| ----------------- | -------------------------------------- |
|
|
689
|
+
| `Escape` | Close modal |
|
|
690
|
+
| `Tab` | Navigate to next focusable element |
|
|
691
|
+
| `Shift + Tab` | Navigate to previous focusable element |
|
|
692
|
+
| `Enter` / `Space` | Activate focused button |
|
|
693
|
+
|
|
694
|
+
### ARIA Attributes
|
|
695
|
+
|
|
696
|
+
- `role="dialog"` on modal content
|
|
697
|
+
- `aria-labelledby` references the title
|
|
698
|
+
- `aria-describedby` references the description
|
|
699
|
+
- `aria-modal="true"` indicates modal state
|
|
700
|
+
- `aria-hidden` on background content when modal is open
|
|
701
|
+
|
|
702
|
+
### Accessibility Guidelines
|
|
703
|
+
|
|
704
|
+
1. **Provide Clear Titles**: Always include `a11yTitle` for screen readers
|
|
705
|
+
2. **Use Semantic Headings**: Structure content with proper heading hierarchy
|
|
706
|
+
3. **Descriptive Labels**: Add `a11yDescription` for complex modals
|
|
707
|
+
4. **Focus Management**: Let the component handle focus automatically
|
|
708
|
+
5. **Escape Routes**: Always provide clear ways to close the modal
|
|
709
|
+
|
|
710
|
+
```tsx
|
|
711
|
+
// ✅ Good accessibility practices
|
|
712
|
+
<Modal.Root
|
|
713
|
+
open={open}
|
|
714
|
+
onOpenChange={setOpen}
|
|
715
|
+
a11yTitle="Delete Confirmation"
|
|
716
|
+
a11yDescription="Confirm that you want to permanently delete this item"
|
|
717
|
+
>
|
|
718
|
+
<Modal.Content>
|
|
719
|
+
<Modal.Header>
|
|
720
|
+
<h2>Delete Item</h2>
|
|
721
|
+
<Modal.Close />
|
|
722
|
+
</Modal.Header>
|
|
723
|
+
|
|
724
|
+
<Modal.Body>
|
|
725
|
+
<p>Are you sure you want to delete "Document.pdf"? This action cannot be undone.</p>
|
|
726
|
+
</Modal.Body>
|
|
727
|
+
|
|
728
|
+
<Modal.Footer>
|
|
729
|
+
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
730
|
+
Cancel
|
|
731
|
+
</Button>
|
|
732
|
+
<Button color="red" onClick={handleDelete}>
|
|
733
|
+
Delete
|
|
734
|
+
</Button>
|
|
735
|
+
</Modal.Footer>
|
|
736
|
+
</Modal.Content>
|
|
737
|
+
</Modal.Root>
|
|
738
|
+
|
|
739
|
+
// ❌ Poor accessibility
|
|
740
|
+
<Modal.Root open={open} onOpenChange={setOpen}> {/* Missing a11yTitle */}
|
|
741
|
+
<Modal.Content>
|
|
742
|
+
<div>Delete Item</div> {/* Not a proper heading */}
|
|
743
|
+
<div>Are you sure?</div> {/* No clear context */}
|
|
744
|
+
<button onClick={handleDelete}>OK</button> {/* Unclear action */}
|
|
745
|
+
</Modal.Content>
|
|
746
|
+
</Modal.Root>
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Best Practices
|
|
750
|
+
|
|
751
|
+
1. **Meaningful Titles**: Use descriptive titles that explain the modal's purpose
|
|
752
|
+
2. **Logical Structure**: Use Header, Body, Footer to create clear content areas
|
|
753
|
+
3. **Action Clarity**: Make button labels clear about their actions
|
|
754
|
+
4. **Error Handling**: Provide clear feedback for errors
|
|
755
|
+
5. **Consistent Patterns**: Use consistent modal patterns across your application
|
|
756
|
+
|
|
757
|
+
## Examples
|
|
758
|
+
|
|
759
|
+
### Settings Modal
|
|
760
|
+
|
|
761
|
+
```tsx
|
|
762
|
+
import { Button } from "@telegraph/button";
|
|
763
|
+
import { Input } from "@telegraph/input";
|
|
764
|
+
import { Stack } from "@telegraph/layout";
|
|
765
|
+
import { Modal } from "@telegraph/modal";
|
|
766
|
+
import { Switch } from "@telegraph/switch";
|
|
767
|
+
|
|
768
|
+
export const SettingsModal = ({ open, onClose, settings, onSave }) => {
|
|
769
|
+
const [localSettings, setLocalSettings] = useState(settings);
|
|
770
|
+
|
|
771
|
+
const handleSave = () => {
|
|
772
|
+
onSave(localSettings);
|
|
773
|
+
onClose();
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
return (
|
|
777
|
+
<Modal.Root
|
|
778
|
+
open={open}
|
|
779
|
+
onOpenChange={onClose}
|
|
780
|
+
a11yTitle="Application Settings"
|
|
781
|
+
>
|
|
782
|
+
<Modal.Content maxW="120">
|
|
783
|
+
<Modal.Header>
|
|
784
|
+
<h2>Settings</h2>
|
|
785
|
+
<Modal.Close />
|
|
786
|
+
</Modal.Header>
|
|
787
|
+
|
|
788
|
+
<Modal.Body>
|
|
789
|
+
<Stack direction="column" gap="4">
|
|
790
|
+
<Stack direction="column" gap="2">
|
|
791
|
+
<h3>Profile</h3>
|
|
792
|
+
<Stack direction="column" gap="1">
|
|
793
|
+
<label htmlFor="display-name">Display Name</label>
|
|
794
|
+
<Input
|
|
795
|
+
id="display-name"
|
|
796
|
+
value={localSettings.displayName}
|
|
797
|
+
onChange={(e) =>
|
|
798
|
+
setLocalSettings((prev) => ({
|
|
799
|
+
...prev,
|
|
800
|
+
displayName: e.target.value,
|
|
801
|
+
}))
|
|
802
|
+
}
|
|
803
|
+
/>
|
|
804
|
+
</Stack>
|
|
805
|
+
</Stack>
|
|
806
|
+
|
|
807
|
+
<Stack direction="column" gap="2">
|
|
808
|
+
<h3>Preferences</h3>
|
|
809
|
+
<Stack direction="row" align="center" justify="between">
|
|
810
|
+
<label htmlFor="notifications">Email Notifications</label>
|
|
811
|
+
<Switch
|
|
812
|
+
id="notifications"
|
|
813
|
+
checked={localSettings.emailNotifications}
|
|
814
|
+
onCheckedChange={(checked) =>
|
|
815
|
+
setLocalSettings((prev) => ({
|
|
816
|
+
...prev,
|
|
817
|
+
emailNotifications: checked,
|
|
818
|
+
}))
|
|
819
|
+
}
|
|
820
|
+
/>
|
|
821
|
+
</Stack>
|
|
822
|
+
|
|
823
|
+
<Stack direction="row" align="center" justify="between">
|
|
824
|
+
<label htmlFor="dark-mode">Dark Mode</label>
|
|
825
|
+
<Switch
|
|
826
|
+
id="dark-mode"
|
|
827
|
+
checked={localSettings.darkMode}
|
|
828
|
+
onCheckedChange={(checked) =>
|
|
829
|
+
setLocalSettings((prev) => ({
|
|
830
|
+
...prev,
|
|
831
|
+
darkMode: checked,
|
|
832
|
+
}))
|
|
833
|
+
}
|
|
834
|
+
/>
|
|
835
|
+
</Stack>
|
|
836
|
+
</Stack>
|
|
837
|
+
</Stack>
|
|
838
|
+
</Modal.Body>
|
|
839
|
+
|
|
840
|
+
<Modal.Footer>
|
|
841
|
+
<Button variant="outline" onClick={onClose}>
|
|
842
|
+
Cancel
|
|
843
|
+
</Button>
|
|
844
|
+
<Button onClick={handleSave}>Save Settings</Button>
|
|
845
|
+
</Modal.Footer>
|
|
846
|
+
</Modal.Content>
|
|
847
|
+
</Modal.Root>
|
|
848
|
+
);
|
|
849
|
+
};
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### Image Gallery Modal
|
|
853
|
+
|
|
854
|
+
```tsx
|
|
855
|
+
import { Button } from "@telegraph/button";
|
|
856
|
+
import { Modal } from "@telegraph/modal";
|
|
857
|
+
import { ChevronLeft, ChevronRight, Download, Share } from "lucide-react";
|
|
858
|
+
import { useState } from "react";
|
|
859
|
+
|
|
860
|
+
export const ImageGalleryModal = ({
|
|
861
|
+
open,
|
|
862
|
+
onClose,
|
|
863
|
+
images,
|
|
864
|
+
initialIndex = 0,
|
|
865
|
+
}) => {
|
|
866
|
+
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
|
867
|
+
|
|
868
|
+
const currentImage = images[currentIndex];
|
|
869
|
+
const canGoPrev = currentIndex > 0;
|
|
870
|
+
const canGoNext = currentIndex < images.length - 1;
|
|
871
|
+
|
|
872
|
+
return (
|
|
873
|
+
<Modal.Root open={open} onOpenChange={onClose} a11yTitle="Image Gallery">
|
|
874
|
+
<Modal.Content w="screen" maxW="screen" h="screen" rounded="0" p="0">
|
|
875
|
+
<Modal.Header px="6" py="4">
|
|
876
|
+
<Stack direction="row" align="center" justify="between" w="full">
|
|
877
|
+
<h2>{currentImage?.title || `Image ${currentIndex + 1}`}</h2>
|
|
878
|
+
<Stack direction="row" gap="2">
|
|
879
|
+
<Button
|
|
880
|
+
variant="ghost"
|
|
881
|
+
icon={{ icon: Download, alt: "Download" }}
|
|
882
|
+
/>
|
|
883
|
+
<Button variant="ghost" icon={{ icon: Share, alt: "Share" }} />
|
|
884
|
+
<Modal.Close />
|
|
885
|
+
</Stack>
|
|
886
|
+
</Stack>
|
|
887
|
+
</Modal.Header>
|
|
888
|
+
|
|
889
|
+
<Modal.Body p="0" position="relative">
|
|
890
|
+
<img
|
|
891
|
+
src={currentImage?.src}
|
|
892
|
+
alt={currentImage?.alt}
|
|
893
|
+
style={{
|
|
894
|
+
width: "100%",
|
|
895
|
+
height: "100%",
|
|
896
|
+
objectFit: "contain",
|
|
897
|
+
backgroundColor: "var(--tgph-black)",
|
|
898
|
+
}}
|
|
899
|
+
/>
|
|
900
|
+
|
|
901
|
+
{/* Navigation buttons */}
|
|
902
|
+
{canGoPrev && (
|
|
903
|
+
<Button
|
|
904
|
+
variant="ghost"
|
|
905
|
+
icon={{ icon: ChevronLeft, alt: "Previous image" }}
|
|
906
|
+
onClick={() => setCurrentIndex((prev) => prev - 1)}
|
|
907
|
+
style={{
|
|
908
|
+
position: "absolute",
|
|
909
|
+
left: "var(--tgph-spacing-4)",
|
|
910
|
+
top: "50%",
|
|
911
|
+
transform: "translateY(-50%)",
|
|
912
|
+
}}
|
|
913
|
+
/>
|
|
914
|
+
)}
|
|
915
|
+
|
|
916
|
+
{canGoNext && (
|
|
917
|
+
<Button
|
|
918
|
+
variant="ghost"
|
|
919
|
+
icon={{ icon: ChevronRight, alt: "Next image" }}
|
|
920
|
+
onClick={() => setCurrentIndex((prev) => prev + 1)}
|
|
921
|
+
style={{
|
|
922
|
+
position: "absolute",
|
|
923
|
+
right: "var(--tgph-spacing-4)",
|
|
924
|
+
top: "50%",
|
|
925
|
+
transform: "translateY(-50%)",
|
|
926
|
+
}}
|
|
927
|
+
/>
|
|
928
|
+
)}
|
|
929
|
+
</Modal.Body>
|
|
930
|
+
|
|
931
|
+
<Modal.Footer px="6" py="4">
|
|
932
|
+
<Stack direction="row" align="center" justify="center" gap="2">
|
|
933
|
+
{images.map((_, index) => (
|
|
934
|
+
<Box
|
|
935
|
+
key={index}
|
|
936
|
+
w="2"
|
|
937
|
+
h="2"
|
|
938
|
+
bg={index === currentIndex ? "white" : "alpha-white-6"}
|
|
939
|
+
rounded="full"
|
|
940
|
+
onClick={() => setCurrentIndex(index)}
|
|
941
|
+
style={{ cursor: "pointer" }}
|
|
942
|
+
/>
|
|
943
|
+
))}
|
|
944
|
+
</Stack>
|
|
945
|
+
</Modal.Footer>
|
|
946
|
+
</Modal.Content>
|
|
947
|
+
</Modal.Root>
|
|
948
|
+
);
|
|
949
|
+
};
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
### Data Export Modal
|
|
953
|
+
|
|
954
|
+
```tsx
|
|
955
|
+
import { Button } from "@telegraph/button";
|
|
956
|
+
import { Checkbox } from "@telegraph/checkbox";
|
|
957
|
+
import { Stack } from "@telegraph/layout";
|
|
958
|
+
import { Modal } from "@telegraph/modal";
|
|
959
|
+
import { RadioCards } from "@telegraph/radio";
|
|
960
|
+
import { Select } from "@telegraph/select";
|
|
961
|
+
import { useState } from "react";
|
|
962
|
+
|
|
963
|
+
export const ExportModal = ({ open, onClose, onExport }) => {
|
|
964
|
+
const [format, setFormat] = useState("csv");
|
|
965
|
+
const [dateRange, setDateRange] = useState("last-30-days");
|
|
966
|
+
const [includeHeaders, setIncludeHeaders] = useState(true);
|
|
967
|
+
const [includeMetadata, setIncludeMetadata] = useState(false);
|
|
968
|
+
|
|
969
|
+
const handleExport = () => {
|
|
970
|
+
onExport({
|
|
971
|
+
format,
|
|
972
|
+
dateRange,
|
|
973
|
+
includeHeaders,
|
|
974
|
+
includeMetadata,
|
|
975
|
+
});
|
|
976
|
+
onClose();
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
return (
|
|
980
|
+
<Modal.Root open={open} onOpenChange={onClose} a11yTitle="Export Data">
|
|
981
|
+
<Modal.Content>
|
|
982
|
+
<Modal.Header>
|
|
983
|
+
<h2>Export Data</h2>
|
|
984
|
+
<Modal.Close />
|
|
985
|
+
</Modal.Header>
|
|
986
|
+
|
|
987
|
+
<Modal.Body>
|
|
988
|
+
<Stack direction="column" gap="4">
|
|
989
|
+
<Stack direction="column" gap="2">
|
|
990
|
+
<h3>Export Format</h3>
|
|
991
|
+
<RadioCards value={format} onValueChange={setFormat}>
|
|
992
|
+
<RadioCards.Item value="csv">
|
|
993
|
+
<Stack direction="column" gap="1">
|
|
994
|
+
<strong>CSV</strong>
|
|
995
|
+
<span>Comma-separated values</span>
|
|
996
|
+
</Stack>
|
|
997
|
+
</RadioCards.Item>
|
|
998
|
+
<RadioCards.Item value="json">
|
|
999
|
+
<Stack direction="column" gap="1">
|
|
1000
|
+
<strong>JSON</strong>
|
|
1001
|
+
<span>JavaScript Object Notation</span>
|
|
1002
|
+
</Stack>
|
|
1003
|
+
</RadioCards.Item>
|
|
1004
|
+
<RadioCards.Item value="xlsx">
|
|
1005
|
+
<Stack direction="column" gap="1">
|
|
1006
|
+
<strong>Excel</strong>
|
|
1007
|
+
<span>Microsoft Excel format</span>
|
|
1008
|
+
</Stack>
|
|
1009
|
+
</RadioCards.Item>
|
|
1010
|
+
</RadioCards>
|
|
1011
|
+
</Stack>
|
|
1012
|
+
|
|
1013
|
+
<Stack direction="column" gap="2">
|
|
1014
|
+
<label htmlFor="date-range">Date Range</label>
|
|
1015
|
+
<Select value={dateRange} onValueChange={setDateRange}>
|
|
1016
|
+
<Select.Option value="last-7-days">Last 7 days</Select.Option>
|
|
1017
|
+
<Select.Option value="last-30-days">Last 30 days</Select.Option>
|
|
1018
|
+
<Select.Option value="last-90-days">Last 90 days</Select.Option>
|
|
1019
|
+
<Select.Option value="all-time">All time</Select.Option>
|
|
1020
|
+
</Select>
|
|
1021
|
+
</Stack>
|
|
1022
|
+
|
|
1023
|
+
<Stack direction="column" gap="2">
|
|
1024
|
+
<h3>Options</h3>
|
|
1025
|
+
<Stack direction="column" gap="2">
|
|
1026
|
+
<Checkbox
|
|
1027
|
+
checked={includeHeaders}
|
|
1028
|
+
onCheckedChange={setIncludeHeaders}
|
|
1029
|
+
>
|
|
1030
|
+
Include column headers
|
|
1031
|
+
</Checkbox>
|
|
1032
|
+
<Checkbox
|
|
1033
|
+
checked={includeMetadata}
|
|
1034
|
+
onCheckedChange={setIncludeMetadata}
|
|
1035
|
+
>
|
|
1036
|
+
Include metadata
|
|
1037
|
+
</Checkbox>
|
|
1038
|
+
</Stack>
|
|
1039
|
+
</Stack>
|
|
1040
|
+
</Stack>
|
|
1041
|
+
</Modal.Body>
|
|
1042
|
+
|
|
1043
|
+
<Modal.Footer>
|
|
1044
|
+
<Button variant="outline" onClick={onClose}>
|
|
1045
|
+
Cancel
|
|
1046
|
+
</Button>
|
|
1047
|
+
<Button onClick={handleExport}>Export Data</Button>
|
|
1048
|
+
</Modal.Footer>
|
|
1049
|
+
</Modal.Content>
|
|
1050
|
+
</Modal.Root>
|
|
1051
|
+
);
|
|
1052
|
+
};
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
## References
|
|
1056
|
+
|
|
1057
|
+
- [Radix UI Dialog](https://www.radix-ui.com/docs/primitives/components/dialog)
|
|
1058
|
+
- [Storybook Demo](https://storybook.telegraph.dev/?path=/docs/modal)
|
|
1059
|
+
|
|
1060
|
+
## Contributing
|
|
1061
|
+
|
|
1062
|
+
See our [Contributing Guide](../../CONTRIBUTING.md) for more details.
|
|
1063
|
+
|
|
1064
|
+
## License
|
|
1065
|
+
|
|
1066
|
+
MIT License - see [LICENSE](../../LICENSE) for details.
|