@sproutsocial/seeds-react-modal 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +19 -19
- package/CHANGELOG.md +23 -0
- package/dist/{ModalRail-OQ8DZ1vH.d.mts → ModalRail-5PeilhW7.d.mts} +12 -4
- package/dist/{ModalRail-OQ8DZ1vH.d.ts → ModalRail-5PeilhW7.d.ts} +12 -4
- package/dist/esm/{chunk-GKQRFPCX.js → chunk-4ITF4DBY.js} +107 -32
- package/dist/esm/chunk-4ITF4DBY.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/v2/index.js +5 -3
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +105 -31
- package/dist/index.js.map +1 -1
- package/dist/v2/index.d.mts +1 -1
- package/dist/v2/index.d.ts +1 -1
- package/dist/v2/index.js +109 -33
- package/dist/v2/index.js.map +1 -1
- package/package.json +7 -7
- package/src/v2/ModalV2.stories.tsx +273 -2
- package/src/v2/ModalV2.tsx +113 -31
- package/src/v2/ModalV2Styles.tsx +39 -9
- package/src/v2/ModalV2Types.ts +0 -4
- package/src/v2/components/ModalHeader.tsx +10 -2
- package/src/v2/components/ModalRail.tsx +11 -9
- package/src/v2/index.ts +1 -1
- package/dist/esm/chunk-GKQRFPCX.js.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import React, { useState } from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
4
4
|
import { Button } from "@sproutsocial/seeds-react-button";
|
|
5
|
-
import
|
|
5
|
+
import Text from "@sproutsocial/seeds-react-text";
|
|
6
|
+
import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
6
7
|
import { Modal, ModalHeader, ModalFooter, ModalContent, ModalClose } from "./";
|
|
7
8
|
|
|
8
9
|
const meta: Meta<typeof Modal> = {
|
|
@@ -280,3 +281,273 @@ export const ControlledState: Story = {
|
|
|
280
281
|
);
|
|
281
282
|
},
|
|
282
283
|
};
|
|
284
|
+
|
|
285
|
+
export const DraggableWithFormInteraction: Story = {
|
|
286
|
+
render: () => {
|
|
287
|
+
const [formData, setFormData] = useState({
|
|
288
|
+
firstName: "",
|
|
289
|
+
lastName: "",
|
|
290
|
+
email: "",
|
|
291
|
+
company: "",
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const [submittedData, setSubmittedData] = useState<typeof formData | null>(
|
|
295
|
+
null
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
setSubmittedData(formData);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const handleInputChange = (field: keyof typeof formData, value: string) => {
|
|
304
|
+
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<Box p={500}>
|
|
309
|
+
<Box mb={400}>
|
|
310
|
+
<Text fontSize={500} fontWeight="bold">
|
|
311
|
+
Test Draggable Modal with Form Interaction
|
|
312
|
+
</Text>
|
|
313
|
+
<Text fontSize={200} color="text.subtle" mt={200}>
|
|
314
|
+
Open the draggable modal, move it to the side, then try interacting
|
|
315
|
+
with the form below. You should be able to fill out the form while
|
|
316
|
+
the modal is open and moved aside.
|
|
317
|
+
</Text>
|
|
318
|
+
</Box>
|
|
319
|
+
|
|
320
|
+
<Box
|
|
321
|
+
p={500}
|
|
322
|
+
bg="neutral.100"
|
|
323
|
+
borderRadius="8px"
|
|
324
|
+
border="2px solid"
|
|
325
|
+
borderColor="neutral.400"
|
|
326
|
+
>
|
|
327
|
+
<Text fontSize={400} fontWeight="bold" mb={300}>
|
|
328
|
+
Contact Form
|
|
329
|
+
</Text>
|
|
330
|
+
<form onSubmit={handleSubmit}>
|
|
331
|
+
<Box display="flex" flexDirection="column" gap={300}>
|
|
332
|
+
<FormField
|
|
333
|
+
label="First Name"
|
|
334
|
+
error={null}
|
|
335
|
+
helperText={null}
|
|
336
|
+
id=""
|
|
337
|
+
qa={{}}
|
|
338
|
+
required={false}
|
|
339
|
+
>
|
|
340
|
+
{(props) => (
|
|
341
|
+
<input
|
|
342
|
+
{...props}
|
|
343
|
+
type="text"
|
|
344
|
+
value={formData.firstName}
|
|
345
|
+
onChange={(e) =>
|
|
346
|
+
handleInputChange("firstName", e.target.value)
|
|
347
|
+
}
|
|
348
|
+
style={{
|
|
349
|
+
padding: "8px 12px",
|
|
350
|
+
borderRadius: "4px",
|
|
351
|
+
border: "1px solid #ccc",
|
|
352
|
+
width: "100%",
|
|
353
|
+
fontSize: "14px",
|
|
354
|
+
}}
|
|
355
|
+
/>
|
|
356
|
+
)}
|
|
357
|
+
</FormField>
|
|
358
|
+
|
|
359
|
+
<FormField
|
|
360
|
+
label="Last Name"
|
|
361
|
+
error={null}
|
|
362
|
+
helperText={null}
|
|
363
|
+
id=""
|
|
364
|
+
qa={{}}
|
|
365
|
+
required={false}
|
|
366
|
+
>
|
|
367
|
+
{(props) => (
|
|
368
|
+
<input
|
|
369
|
+
{...props}
|
|
370
|
+
type="text"
|
|
371
|
+
value={formData.lastName}
|
|
372
|
+
onChange={(e) =>
|
|
373
|
+
handleInputChange("lastName", e.target.value)
|
|
374
|
+
}
|
|
375
|
+
style={{
|
|
376
|
+
padding: "8px 12px",
|
|
377
|
+
borderRadius: "4px",
|
|
378
|
+
border: "1px solid #ccc",
|
|
379
|
+
width: "100%",
|
|
380
|
+
fontSize: "14px",
|
|
381
|
+
}}
|
|
382
|
+
/>
|
|
383
|
+
)}
|
|
384
|
+
</FormField>
|
|
385
|
+
|
|
386
|
+
<FormField
|
|
387
|
+
label="Email"
|
|
388
|
+
error={null}
|
|
389
|
+
helperText={null}
|
|
390
|
+
id=""
|
|
391
|
+
qa={{}}
|
|
392
|
+
required={false}
|
|
393
|
+
>
|
|
394
|
+
{(props) => (
|
|
395
|
+
<input
|
|
396
|
+
{...props}
|
|
397
|
+
type="email"
|
|
398
|
+
value={formData.email}
|
|
399
|
+
onChange={(e) => handleInputChange("email", e.target.value)}
|
|
400
|
+
style={{
|
|
401
|
+
padding: "8px 12px",
|
|
402
|
+
borderRadius: "4px",
|
|
403
|
+
border: "1px solid #ccc",
|
|
404
|
+
width: "100%",
|
|
405
|
+
fontSize: "14px",
|
|
406
|
+
}}
|
|
407
|
+
/>
|
|
408
|
+
)}
|
|
409
|
+
</FormField>
|
|
410
|
+
|
|
411
|
+
<FormField
|
|
412
|
+
label="Company"
|
|
413
|
+
error={null}
|
|
414
|
+
helperText={null}
|
|
415
|
+
id=""
|
|
416
|
+
qa={{}}
|
|
417
|
+
required={false}
|
|
418
|
+
>
|
|
419
|
+
{(props) => (
|
|
420
|
+
<input
|
|
421
|
+
{...props}
|
|
422
|
+
type="text"
|
|
423
|
+
value={formData.company}
|
|
424
|
+
onChange={(e) =>
|
|
425
|
+
handleInputChange("company", e.target.value)
|
|
426
|
+
}
|
|
427
|
+
style={{
|
|
428
|
+
padding: "8px 12px",
|
|
429
|
+
borderRadius: "4px",
|
|
430
|
+
border: "1px solid #ccc",
|
|
431
|
+
width: "100%",
|
|
432
|
+
fontSize: "14px",
|
|
433
|
+
}}
|
|
434
|
+
/>
|
|
435
|
+
)}
|
|
436
|
+
</FormField>
|
|
437
|
+
|
|
438
|
+
<Box display="flex" gap={300} mt={200}>
|
|
439
|
+
<Button type="submit" appearance="primary">
|
|
440
|
+
Submit Form
|
|
441
|
+
</Button>
|
|
442
|
+
|
|
443
|
+
<Modal
|
|
444
|
+
aria-label="Reference Modal"
|
|
445
|
+
size="medium"
|
|
446
|
+
draggable={true}
|
|
447
|
+
showOverlay={true}
|
|
448
|
+
modalTrigger={
|
|
449
|
+
<Button appearance="secondary">Open Reference Modal</Button>
|
|
450
|
+
}
|
|
451
|
+
>
|
|
452
|
+
<ModalContent>
|
|
453
|
+
<ModalHeader
|
|
454
|
+
title="Reference Information"
|
|
455
|
+
subtitle="Drag this modal by the header to move it around"
|
|
456
|
+
/>
|
|
457
|
+
<Box p={300}>
|
|
458
|
+
<Text fontSize={400} fontWeight="bold" mb={200}>
|
|
459
|
+
Instructions for filling out the form:
|
|
460
|
+
</Text>
|
|
461
|
+
<Box as="ul" pl={400}>
|
|
462
|
+
<li>
|
|
463
|
+
<Text mb={100}>
|
|
464
|
+
First Name: Enter your given name
|
|
465
|
+
</Text>
|
|
466
|
+
</li>
|
|
467
|
+
<li>
|
|
468
|
+
<Text mb={100}>
|
|
469
|
+
Last Name: Enter your family name
|
|
470
|
+
</Text>
|
|
471
|
+
</li>
|
|
472
|
+
<li>
|
|
473
|
+
<Text mb={100}>
|
|
474
|
+
Email: Use your work email address
|
|
475
|
+
</Text>
|
|
476
|
+
</li>
|
|
477
|
+
<li>
|
|
478
|
+
<Text mb={100}>
|
|
479
|
+
Company: Enter your organization name
|
|
480
|
+
</Text>
|
|
481
|
+
</li>
|
|
482
|
+
</Box>
|
|
483
|
+
|
|
484
|
+
<Box
|
|
485
|
+
mt={400}
|
|
486
|
+
p={300}
|
|
487
|
+
bg="blue.200"
|
|
488
|
+
borderRadius="6px"
|
|
489
|
+
border="1px solid"
|
|
490
|
+
borderColor="blue.400"
|
|
491
|
+
>
|
|
492
|
+
<Text fontWeight="bold" mb={100}>
|
|
493
|
+
Try this:
|
|
494
|
+
</Text>
|
|
495
|
+
<Text fontSize={100}>
|
|
496
|
+
1. Grab this modal by the header (you'll see a grab
|
|
497
|
+
cursor)
|
|
498
|
+
<br />
|
|
499
|
+
2. Drag it to the side of the screen
|
|
500
|
+
<br />
|
|
501
|
+
3. Click on the form inputs in the background
|
|
502
|
+
<br />
|
|
503
|
+
4. Notice you can interact with the form while the
|
|
504
|
+
modal is open!
|
|
505
|
+
</Text>
|
|
506
|
+
</Box>
|
|
507
|
+
</Box>
|
|
508
|
+
<ModalFooter>
|
|
509
|
+
<Box display="flex" justifyContent="flex-end">
|
|
510
|
+
<ModalClose asChild>
|
|
511
|
+
<Button appearance="primary">Got it!</Button>
|
|
512
|
+
</ModalClose>
|
|
513
|
+
</Box>
|
|
514
|
+
</ModalFooter>
|
|
515
|
+
</ModalContent>
|
|
516
|
+
</Modal>
|
|
517
|
+
</Box>
|
|
518
|
+
</Box>
|
|
519
|
+
</form>
|
|
520
|
+
|
|
521
|
+
{submittedData && (
|
|
522
|
+
<Box
|
|
523
|
+
mt={400}
|
|
524
|
+
p={400}
|
|
525
|
+
bg="green.200"
|
|
526
|
+
borderRadius="6px"
|
|
527
|
+
border="2px solid"
|
|
528
|
+
borderColor="green.500"
|
|
529
|
+
>
|
|
530
|
+
<Text fontSize={400} fontWeight="bold" mb={200}>
|
|
531
|
+
Form Submitted!
|
|
532
|
+
</Text>
|
|
533
|
+
<Box as="ul" pl={400}>
|
|
534
|
+
<li>
|
|
535
|
+
<Text>First Name: {submittedData.firstName}</Text>
|
|
536
|
+
</li>
|
|
537
|
+
<li>
|
|
538
|
+
<Text>Last Name: {submittedData.lastName}</Text>
|
|
539
|
+
</li>
|
|
540
|
+
<li>
|
|
541
|
+
<Text>Email: {submittedData.email}</Text>
|
|
542
|
+
</li>
|
|
543
|
+
<li>
|
|
544
|
+
<Text>Company: {submittedData.company}</Text>
|
|
545
|
+
</li>
|
|
546
|
+
</Box>
|
|
547
|
+
</Box>
|
|
548
|
+
)}
|
|
549
|
+
</Box>
|
|
550
|
+
</Box>
|
|
551
|
+
);
|
|
552
|
+
},
|
|
553
|
+
};
|
package/src/v2/ModalV2.tsx
CHANGED
|
@@ -26,8 +26,25 @@ interface DraggableModalContentProps {
|
|
|
26
26
|
dataAttributes: Record<string, string>;
|
|
27
27
|
draggable?: boolean;
|
|
28
28
|
rest: any;
|
|
29
|
+
railProps?: Partial<TypeModalV2Props["railProps"]>;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// Context to share drag state between modal content and header
|
|
33
|
+
interface DragContextValue {
|
|
34
|
+
position: { x: number; y: number };
|
|
35
|
+
isDragging: boolean;
|
|
36
|
+
onHeaderMouseDown: (e: React.MouseEvent) => void;
|
|
37
|
+
contentRef: React.RefObject<HTMLDivElement>;
|
|
38
|
+
draggable: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DragContext = React.createContext<DragContextValue | null>(null);
|
|
42
|
+
|
|
43
|
+
export const useDragContext = () => {
|
|
44
|
+
const context = React.useContext(DragContext);
|
|
45
|
+
return context;
|
|
46
|
+
};
|
|
47
|
+
|
|
31
48
|
const DraggableModalContent: React.FC<DraggableModalContentProps> = ({
|
|
32
49
|
children,
|
|
33
50
|
computedWidth,
|
|
@@ -37,16 +54,26 @@ const DraggableModalContent: React.FC<DraggableModalContentProps> = ({
|
|
|
37
54
|
dataAttributes,
|
|
38
55
|
draggable,
|
|
39
56
|
rest,
|
|
57
|
+
railProps,
|
|
40
58
|
}) => {
|
|
41
59
|
const [position, setPosition] = React.useState({ x: 0, y: 0 });
|
|
42
60
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
43
61
|
const contentRef = React.useRef<HTMLDivElement>(null);
|
|
44
62
|
|
|
45
|
-
|
|
63
|
+
// Calculate rail dimensions for boundary constraints
|
|
64
|
+
const railSize = railProps?.size ?? 44;
|
|
65
|
+
const railOffset = railProps?.offset ?? 12;
|
|
66
|
+
const railSide = railProps?.side ?? "right";
|
|
67
|
+
|
|
68
|
+
// Total extra space needed on the side with the rail
|
|
69
|
+
// (only when viewport is wider than 400px breakpoint)
|
|
70
|
+
const railExtraSpace = railSize + railOffset;
|
|
71
|
+
|
|
72
|
+
const handleHeaderMouseDown = React.useCallback(
|
|
46
73
|
(e: React.MouseEvent) => {
|
|
47
74
|
if (!draggable) return;
|
|
48
75
|
|
|
49
|
-
// Only allow dragging from
|
|
76
|
+
// Only allow dragging from header (not interactive elements)
|
|
50
77
|
const target = e.target as HTMLElement;
|
|
51
78
|
if (
|
|
52
79
|
target.tagName === "BUTTON" ||
|
|
@@ -73,13 +100,33 @@ const DraggableModalContent: React.FC<DraggableModalContentProps> = ({
|
|
|
73
100
|
const newX = e.clientX - offsetX;
|
|
74
101
|
const newY = e.clientY - offsetY;
|
|
75
102
|
|
|
76
|
-
//
|
|
103
|
+
// Determine if rail is on the side (viewport > 400px) or above (viewport <= 400px)
|
|
104
|
+
const isRailOnSide = window.innerWidth > 400;
|
|
105
|
+
|
|
106
|
+
// Constrain to viewport bounds (keeping modal AND rail fully visible)
|
|
77
107
|
const modalWidth = rect.width;
|
|
78
108
|
const modalHeight = rect.height;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
109
|
+
|
|
110
|
+
// Adjust boundaries to account for rail position
|
|
111
|
+
let maxX = window.innerWidth - modalWidth;
|
|
112
|
+
let minX = 0;
|
|
113
|
+
let maxY = window.innerHeight - modalHeight;
|
|
114
|
+
let minY = 0;
|
|
115
|
+
|
|
116
|
+
if (isRailOnSide) {
|
|
117
|
+
// Rail is positioned on the side of the modal
|
|
118
|
+
if (railSide === "right") {
|
|
119
|
+
// Rail is on the right, so reduce maxX to prevent rail from going off-screen
|
|
120
|
+
maxX = window.innerWidth - modalWidth - railExtraSpace;
|
|
121
|
+
} else {
|
|
122
|
+
// Rail is on the left, so increase minX to prevent rail from going off-screen
|
|
123
|
+
minX = railExtraSpace;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// Rail is positioned above the modal (viewport <= 400px)
|
|
127
|
+
// Account for rail height + offset at the top
|
|
128
|
+
minY = railSize + railOffset;
|
|
129
|
+
}
|
|
83
130
|
|
|
84
131
|
const constrainedX = Math.max(minX, Math.min(maxX, newX));
|
|
85
132
|
const constrainedY = Math.max(minY, Math.min(maxY, newY));
|
|
@@ -103,32 +150,58 @@ const DraggableModalContent: React.FC<DraggableModalContentProps> = ({
|
|
|
103
150
|
document.addEventListener("mousemove", handleMouseMove);
|
|
104
151
|
document.addEventListener("mouseup", handleMouseUp);
|
|
105
152
|
},
|
|
153
|
+
[draggable, railSide, railExtraSpace]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const dragContextValue = React.useMemo<DragContextValue>(
|
|
157
|
+
() => ({
|
|
158
|
+
position,
|
|
159
|
+
isDragging,
|
|
160
|
+
onHeaderMouseDown: handleHeaderMouseDown,
|
|
161
|
+
contentRef,
|
|
162
|
+
draggable: draggable ?? false,
|
|
163
|
+
}),
|
|
164
|
+
[position, isDragging, handleHeaderMouseDown, draggable]
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Prevent modal from closing on outside interaction when draggable
|
|
168
|
+
const handleInteractOutside = React.useCallback(
|
|
169
|
+
(e: Event) => {
|
|
170
|
+
if (draggable) {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
}
|
|
173
|
+
},
|
|
106
174
|
[draggable]
|
|
107
175
|
);
|
|
108
176
|
|
|
109
177
|
return (
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
178
|
+
<DragContext.Provider value={draggable ? dragContextValue : null}>
|
|
179
|
+
<StyledContent
|
|
180
|
+
ref={contentRef}
|
|
181
|
+
width={computedWidth}
|
|
182
|
+
bg={bg}
|
|
183
|
+
zIndex={computedZIndex}
|
|
184
|
+
aria-label={label}
|
|
185
|
+
draggable={draggable}
|
|
186
|
+
isDragging={isDragging}
|
|
187
|
+
railSize={railSize}
|
|
188
|
+
railOffset={railOffset}
|
|
189
|
+
railSide={railSide}
|
|
190
|
+
onInteractOutside={handleInteractOutside}
|
|
191
|
+
style={
|
|
192
|
+
draggable
|
|
193
|
+
? {
|
|
194
|
+
transform: `translate(calc(-50% + ${position.x}px), calc(-50% + ${position.y}px))`,
|
|
195
|
+
transition: isDragging ? "none" : undefined,
|
|
196
|
+
}
|
|
197
|
+
: undefined
|
|
198
|
+
}
|
|
199
|
+
{...dataAttributes}
|
|
200
|
+
{...rest}
|
|
201
|
+
>
|
|
202
|
+
{children}
|
|
203
|
+
</StyledContent>
|
|
204
|
+
</DragContext.Provider>
|
|
132
205
|
);
|
|
133
206
|
};
|
|
134
207
|
|
|
@@ -233,8 +306,14 @@ const Modal = (props: TypeModalV2Props) => {
|
|
|
233
306
|
props.onOpenChange = handleOpenChange;
|
|
234
307
|
}
|
|
235
308
|
|
|
309
|
+
// When draggable, prevent modal from closing on outside interaction
|
|
310
|
+
// This allows users to interact with background content
|
|
311
|
+
if (draggable) {
|
|
312
|
+
props.modal = false;
|
|
313
|
+
}
|
|
314
|
+
|
|
236
315
|
return props;
|
|
237
|
-
}, [open, defaultOpen, handleOpenChange, onOpenChange]);
|
|
316
|
+
}, [open, defaultOpen, handleOpenChange, onOpenChange, draggable]);
|
|
238
317
|
|
|
239
318
|
// Handle trigger - use modalTrigger prop if provided, otherwise look for ModalTrigger children (backward compatibility)
|
|
240
319
|
const triggers: React.ReactNode[] = [];
|
|
@@ -266,7 +345,9 @@ const Modal = (props: TypeModalV2Props) => {
|
|
|
266
345
|
{triggers}
|
|
267
346
|
|
|
268
347
|
<Dialog.Portal>
|
|
269
|
-
{showOverlay &&
|
|
348
|
+
{showOverlay && (
|
|
349
|
+
<StyledOverlay zIndex={computedZIndex} allowInteraction={draggable} />
|
|
350
|
+
)}
|
|
270
351
|
<DraggableModalContent
|
|
271
352
|
computedWidth={computedWidth}
|
|
272
353
|
bg={bg}
|
|
@@ -275,6 +356,7 @@ const Modal = (props: TypeModalV2Props) => {
|
|
|
275
356
|
dataAttributes={dataAttributes}
|
|
276
357
|
draggable={draggable}
|
|
277
358
|
rest={rest}
|
|
359
|
+
railProps={railProps}
|
|
278
360
|
>
|
|
279
361
|
{/* Floating actions rail - always show a close by default */}
|
|
280
362
|
<ModalRail {...railProps}>
|
package/src/v2/ModalV2Styles.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
interface StyledOverlayProps extends TypeContainerProps {
|
|
13
13
|
zIndex?: number;
|
|
14
|
+
allowInteraction?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export const StyledOverlay = styled(Dialog.Overlay)<StyledOverlayProps>`
|
|
@@ -27,6 +28,13 @@ export const StyledOverlay = styled(Dialog.Overlay)<StyledOverlayProps>`
|
|
|
27
28
|
z-index: ${(props) =>
|
|
28
29
|
props.zIndex ? props.zIndex + DEFAULT_OVERLAY_Z_INDEX_OFFSET : 999};
|
|
29
30
|
|
|
31
|
+
/* Allow clicking through overlay when modal is draggable */
|
|
32
|
+
${(props) =>
|
|
33
|
+
props.allowInteraction &&
|
|
34
|
+
`
|
|
35
|
+
pointer-events: none;
|
|
36
|
+
`}
|
|
37
|
+
|
|
30
38
|
${zIndex}
|
|
31
39
|
|
|
32
40
|
&[data-state="open"] {
|
|
@@ -41,6 +49,9 @@ interface StyledContentProps extends TypeContainerProps {
|
|
|
41
49
|
zIndex?: number;
|
|
42
50
|
isDragging?: boolean;
|
|
43
51
|
draggable?: boolean;
|
|
52
|
+
railSize?: number;
|
|
53
|
+
railOffset?: number;
|
|
54
|
+
railSide?: "right" | "left";
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
export const StyledContent = styled(Dialog.Content)<StyledContentProps>`
|
|
@@ -64,17 +75,23 @@ export const StyledContent = styled(Dialog.Content)<StyledContentProps>`
|
|
|
64
75
|
filter: blur(0);
|
|
65
76
|
color: ${(props) => props.theme.colors.text.body};
|
|
66
77
|
outline: none;
|
|
67
|
-
max-width:
|
|
78
|
+
max-width: ${(props) => {
|
|
79
|
+
// Calculate extra space needed for the rail when it's on the side (viewport > 400px)
|
|
80
|
+
const railSize = props.railSize ?? 44;
|
|
81
|
+
const railOffset = props.railOffset ?? 12;
|
|
82
|
+
const railExtraSpace = railSize + railOffset;
|
|
83
|
+
|
|
84
|
+
// Account for rail space when positioned on the side
|
|
85
|
+
// At viewport <= 400px, rail is above modal, so no horizontal space needed
|
|
86
|
+
return `calc(100vw - ${BODY_PADDING} - ${railExtraSpace}px)`;
|
|
87
|
+
}};
|
|
68
88
|
max-height: calc(100vh - ${BODY_PADDING});
|
|
69
89
|
z-index: ${(props) => props.zIndex || 1000};
|
|
70
90
|
|
|
71
|
-
/*
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
cursor: ${props.isDragging ? "grabbing" : "grab"};
|
|
76
|
-
user-select: none;
|
|
77
|
-
`}
|
|
91
|
+
/* When viewport is <= 400px, rail is above modal, so restore full width */
|
|
92
|
+
@media (max-width: 400px) {
|
|
93
|
+
max-width: calc(100vw - ${BODY_PADDING});
|
|
94
|
+
}
|
|
78
95
|
|
|
79
96
|
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
|
|
80
97
|
height: calc(100vh - ${BODY_PADDING});
|
|
@@ -120,7 +137,12 @@ export const Content = styled(Box)`
|
|
|
120
137
|
}
|
|
121
138
|
`;
|
|
122
139
|
|
|
123
|
-
|
|
140
|
+
interface HeaderProps {
|
|
141
|
+
draggable?: boolean;
|
|
142
|
+
isDragging?: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const Header = styled(Box)<HeaderProps>`
|
|
124
146
|
font-family: ${(props) => props.theme.fontFamily};
|
|
125
147
|
padding: ${(props) => props.theme.space[400]}
|
|
126
148
|
${(props) => props.theme.space[300]};
|
|
@@ -128,6 +150,14 @@ export const Header = styled(Box)`
|
|
|
128
150
|
align-items: center;
|
|
129
151
|
justify-content: space-between;
|
|
130
152
|
flex: 0 0 auto;
|
|
153
|
+
|
|
154
|
+
/* Draggable cursor styling */
|
|
155
|
+
${(props) =>
|
|
156
|
+
props.draggable &&
|
|
157
|
+
`
|
|
158
|
+
cursor: ${props.isDragging ? "grabbing" : "grab"};
|
|
159
|
+
user-select: none;
|
|
160
|
+
`}
|
|
131
161
|
`;
|
|
132
162
|
|
|
133
163
|
export const Footer = styled(Box)`
|
package/src/v2/ModalV2Types.ts
CHANGED
|
@@ -42,9 +42,6 @@ export interface TypeModalV2HeaderProps extends TypeBoxProps {
|
|
|
42
42
|
/** Passing children will override the default modal header */
|
|
43
43
|
children?: React.ReactNode;
|
|
44
44
|
|
|
45
|
-
/** If you're rendering a custom header, you can use this prop to add a bottom border */
|
|
46
|
-
bordered?: boolean;
|
|
47
|
-
|
|
48
45
|
/** Additional props for the Dialog.Title when title is provided */
|
|
49
46
|
titleProps?: Omit<
|
|
50
47
|
React.ComponentPropsWithoutRef<typeof Dialog.Title>,
|
|
@@ -137,7 +134,6 @@ export type TypeModalRailProps = {
|
|
|
137
134
|
offset?: number; // px from card edge; default: 12
|
|
138
135
|
gap?: number; // space between buttons; default: 12
|
|
139
136
|
size?: number; // button square size; default: 44
|
|
140
|
-
collapseAt?: number; // px viewport width to pull rail inside; default: 640
|
|
141
137
|
children?: React.ReactNode;
|
|
142
138
|
};
|
|
143
139
|
|
|
@@ -5,21 +5,29 @@ import Text from "@sproutsocial/seeds-react-text";
|
|
|
5
5
|
import { Header } from "../ModalV2Styles";
|
|
6
6
|
import { ModalCloseButton } from "./ModalCloseButton";
|
|
7
7
|
import type { TypeModalV2HeaderProps } from "../ModalV2Types";
|
|
8
|
+
import { useDragContext } from "../ModalV2";
|
|
8
9
|
|
|
9
10
|
export const ModalHeader = (props: TypeModalV2HeaderProps) => {
|
|
10
11
|
const {
|
|
11
12
|
title,
|
|
12
13
|
subtitle,
|
|
13
14
|
children,
|
|
14
|
-
bordered,
|
|
15
15
|
titleProps = {},
|
|
16
16
|
subtitleProps = {},
|
|
17
17
|
showInlineClose,
|
|
18
18
|
...rest
|
|
19
19
|
} = props;
|
|
20
20
|
|
|
21
|
+
const dragContext = useDragContext();
|
|
22
|
+
const isDraggable = dragContext?.draggable ?? false;
|
|
23
|
+
|
|
21
24
|
return (
|
|
22
|
-
<Header
|
|
25
|
+
<Header
|
|
26
|
+
{...rest}
|
|
27
|
+
onMouseDown={isDraggable ? dragContext?.onHeaderMouseDown : undefined}
|
|
28
|
+
draggable={isDraggable}
|
|
29
|
+
isDragging={dragContext?.isDragging}
|
|
30
|
+
>
|
|
23
31
|
{children ? (
|
|
24
32
|
children
|
|
25
33
|
) : (
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import * as Dialog from "@radix-ui/react-dialog";
|
|
4
4
|
import styled from "styled-components";
|
|
5
|
-
import Icon from "@sproutsocial/seeds-react-icon";
|
|
5
|
+
import Icon from "@sproutsocial/seeds-react-icon";
|
|
6
6
|
import type { TypeModalRailProps, TypeModalActionProps } from "../ModalV2Types";
|
|
7
7
|
|
|
8
8
|
// --- styled wrappers ---
|
|
@@ -11,7 +11,6 @@ const Rail = styled.div<{
|
|
|
11
11
|
offset: number;
|
|
12
12
|
gap: number;
|
|
13
13
|
size: number;
|
|
14
|
-
collapseAt: number;
|
|
15
14
|
}>`
|
|
16
15
|
position: absolute;
|
|
17
16
|
top: ${(p) => p.offset}px;
|
|
@@ -19,14 +18,19 @@ const Rail = styled.div<{
|
|
|
19
18
|
p.side === "right"
|
|
20
19
|
? `right: calc(-1 * (${p.size}px + ${p.offset}px));`
|
|
21
20
|
: `left: calc(-1 * (${p.size}px + ${p.offset}px));`}
|
|
22
|
-
display:
|
|
23
|
-
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
24
23
|
gap: ${(p) => p.gap}px;
|
|
25
24
|
z-index: 1;
|
|
26
25
|
|
|
27
|
-
@media (max-width:
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
@media (max-width: 400px) {
|
|
27
|
+
/* Move rail above the modal on the right side */
|
|
28
|
+
top: auto;
|
|
29
|
+
bottom: calc(100% + ${(p) => p.offset}px);
|
|
30
|
+
right: ${(p) => p.offset}px;
|
|
31
|
+
left: auto;
|
|
32
|
+
/* Change to horizontal layout with reversed order */
|
|
33
|
+
flex-direction: row-reverse;
|
|
30
34
|
}
|
|
31
35
|
`;
|
|
32
36
|
|
|
@@ -68,7 +72,6 @@ export const ModalRail: React.FC<TypeModalRailProps> = ({
|
|
|
68
72
|
offset = 12,
|
|
69
73
|
gap = 12,
|
|
70
74
|
size = 44,
|
|
71
|
-
collapseAt = 640,
|
|
72
75
|
children,
|
|
73
76
|
}) => {
|
|
74
77
|
return (
|
|
@@ -78,7 +81,6 @@ export const ModalRail: React.FC<TypeModalRailProps> = ({
|
|
|
78
81
|
offset={offset}
|
|
79
82
|
gap={gap}
|
|
80
83
|
size={size}
|
|
81
|
-
collapseAt={collapseAt}
|
|
82
84
|
aria-label="Modal quick actions"
|
|
83
85
|
>
|
|
84
86
|
{React.Children.map(children, (child) =>
|