@sproutsocial/seeds-react-modal 2.1.2 → 2.2.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.
@@ -101,6 +101,7 @@ export const WithActions: Story = {
101
101
  return (
102
102
  <Modal
103
103
  aria-label="Modal With Actions"
104
+ closeButtonAriaLabel="Close Modal"
104
105
  modalTrigger={
105
106
  <Button appearance="primary">Open Modal With Actions</Button>
106
107
  }
@@ -137,11 +138,56 @@ export const WithActions: Story = {
137
138
  },
138
139
  };
139
140
 
141
+ export const CustomCloseButton: Story = {
142
+ render: () => {
143
+ const [clickCount, setClickCount] = useState(0);
144
+
145
+ return (
146
+ <Box>
147
+ <Text mb={400}>Close button clicked {clickCount} times</Text>
148
+ <Modal
149
+ aria-label="Modal With Custom Close Button"
150
+ modalTrigger={
151
+ <Button appearance="primary">Open Modal With Custom Close</Button>
152
+ }
153
+ closeButtonProps={{
154
+ "aria-label": "Close this dialog",
155
+ onClick: () => {
156
+ console.log("Close button clicked!");
157
+ setClickCount((prev) => prev + 1);
158
+ },
159
+ id: "custom-close-btn",
160
+ iconName: "x-outline",
161
+ }}
162
+ >
163
+ <ModalHeader
164
+ title="Custom Close Button"
165
+ subtitle="The close button has custom props"
166
+ />
167
+ <ModalBody>
168
+ <Text>
169
+ This modal demonstrates the closeButtonProps API. The close button
170
+ has a custom onClick handler that tracks clicks, a custom id
171
+ attribute, and a custom aria-label. You can pass any button
172
+ attributes like disabled, className, or data attributes.
173
+ </Text>
174
+ </ModalBody>
175
+ <ModalFooter
176
+ cancelButton={<Button>Cancel</Button>}
177
+ primaryButton={<Button appearance="primary">Confirm</Button>}
178
+ />
179
+ </Modal>
180
+ </Box>
181
+ );
182
+ },
183
+ };
184
+
140
185
  export const DraggableModal: Story = {
141
186
  render: () => {
142
187
  return (
143
188
  <Modal
144
189
  aria-label="Draggable Modal"
190
+ closeButtonAriaLabel="Close Modal"
145
191
  draggable={true}
146
192
  showOverlay={false}
147
193
  modalTrigger={
@@ -184,12 +230,10 @@ export const ControlledState: Story = {
184
230
 
185
231
  return (
186
232
  <div>
187
- <Box mb={400}>
188
- <Text>
189
- Use controlled state when you need to manage modal state externally.
190
- Modal opened count: {count}
191
- </Text>
192
- </Box>
233
+ <Text mb={400}>
234
+ Use controlled state when you need to manage modal state externally.
235
+ Modal opened count: {count}
236
+ </Text>
193
237
 
194
238
  <Modal
195
239
  open={isOpen}
@@ -198,6 +242,7 @@ export const ControlledState: Story = {
198
242
  if (open) setCount((c) => c + 1);
199
243
  }}
200
244
  aria-label="Controlled Modal"
245
+ closeButtonAriaLabel="Close Modal"
201
246
  modalTrigger={
202
247
  <Button appearance="primary">Open Controlled Modal</Button>
203
248
  }
@@ -274,16 +319,14 @@ export const DraggableWithFormInteraction: Story = {
274
319
 
275
320
  return (
276
321
  <Box p={500}>
277
- <Box mb={400}>
278
- <Text fontSize={500} fontWeight="bold">
279
- Test Draggable Modal with Form Interaction
280
- </Text>
281
- <Text fontSize={200} color="text.subtle" mt={200}>
282
- Open the draggable modal, move it to the side, then try interacting
283
- with the form below. You should be able to fill out the form while
284
- the modal is open and moved aside.
285
- </Text>
286
- </Box>
322
+ <Text fontSize={500} fontWeight="bold" mb={400}>
323
+ Test Draggable Modal with Form Interaction
324
+ </Text>
325
+ <Text fontSize={200} color="text.subtle" mb={400}>
326
+ Open the draggable modal, move it to the side, then try interacting
327
+ with the form below. You should be able to fill out the form while the
328
+ modal is open and moved aside.
329
+ </Text>
287
330
 
288
331
  <Box
289
332
  p={500}
@@ -410,6 +453,7 @@ export const DraggableWithFormInteraction: Story = {
410
453
 
411
454
  <Modal
412
455
  aria-label="Reference Modal"
456
+ closeButtonAriaLabel="Close Reference Modal"
413
457
  draggable={true}
414
458
  showOverlay={false}
415
459
  modalTrigger={
@@ -420,57 +464,49 @@ export const DraggableWithFormInteraction: Story = {
420
464
  title="Reference Information"
421
465
  subtitle="Drag this modal by the header to move it around"
422
466
  />
423
- <ModalBody>
424
- <Box p={300}>
425
- <Text fontSize={400} fontWeight="bold" mb={200}>
426
- Instructions for filling out the form:
427
- </Text>
428
- <Box as="ul" pl={400}>
429
- <li>
430
- <Text mb={100}>
431
- First Name: Enter your given name
432
- </Text>
433
- </li>
434
- <li>
435
- <Text mb={100}>
436
- Last Name: Enter your family name
437
- </Text>
438
- </li>
439
- <li>
440
- <Text mb={100}>
441
- Email: Use your work email address
442
- </Text>
443
- </li>
444
- <li>
445
- <Text mb={100}>
446
- Company: Enter your organization name
447
- </Text>
448
- </li>
449
- </Box>
450
-
451
- <Box
452
- mt={400}
453
- p={300}
454
- bg="container.background.info"
455
- borderRadius="6px"
456
- border="1px solid"
457
- borderColor="container.border.info"
458
- >
459
- <Text fontWeight="bold" mb={100}>
460
- Try this:
467
+ <ModalBody p={300}>
468
+ <Text fontSize={400} fontWeight="bold" mb={200}>
469
+ Instructions for filling out the form:
470
+ </Text>
471
+ <Box as="ul" pl={400}>
472
+ <li>
473
+ <Text mb={100}>First Name: Enter your given name</Text>
474
+ </li>
475
+ <li>
476
+ <Text mb={100}>Last Name: Enter your family name</Text>
477
+ </li>
478
+ <li>
479
+ <Text mb={100}>Email: Use your work email address</Text>
480
+ </li>
481
+ <li>
482
+ <Text mb={100}>
483
+ Company: Enter your organization name
461
484
  </Text>
462
- <Text fontSize={100}>
463
- 1. Grab this modal by the header (you'll see a grab
464
- cursor)
465
- <br />
466
- 2. Drag it to the side of the screen
467
- <br />
468
- 3. Click on the form inputs in the background
469
- <br />
470
- 4. Notice you can interact with the form while the
471
- modal is open!
472
- </Text>
473
- </Box>
485
+ </li>
486
+ </Box>
487
+
488
+ <Box
489
+ mt={400}
490
+ p={300}
491
+ bg="container.background.info"
492
+ borderRadius="6px"
493
+ border="1px solid"
494
+ borderColor="container.border.info"
495
+ >
496
+ <Text fontWeight="bold" mb={100}>
497
+ Try this:
498
+ </Text>
499
+ <Text fontSize={100}>
500
+ 1. Grab this modal by the header (you'll see a grab
501
+ cursor)
502
+ <br />
503
+ 2. Drag it to the side of the screen
504
+ <br />
505
+ 3. Click on the form inputs in the background
506
+ <br />
507
+ 4. Notice you can interact with the form while the modal
508
+ is open!
509
+ </Text>
474
510
  </Box>
475
511
  </ModalBody>
476
512
  <ModalFooter
@@ -523,6 +559,7 @@ export const TallContentMobile: Story = {
523
559
  return (
524
560
  <Modal
525
561
  aria-label="Tall Content Modal"
562
+ closeButtonAriaLabel="Close Modal"
526
563
  modalTrigger={
527
564
  <Button appearance="primary">Open Tall Modal (Test Mobile)</Button>
528
565
  }
@@ -697,6 +734,7 @@ export const SimplifiedFooterAPI: Story = {
697
734
  return (
698
735
  <Modal
699
736
  aria-label="Simplified Footer API Example"
737
+ closeButtonAriaLabel="Close Modal"
700
738
  modalTrigger={
701
739
  <Button appearance="primary">Open Modal with Simple Footer</Button>
702
740
  }
@@ -745,6 +783,7 @@ export const FooterWithLeftAction: Story = {
745
783
  open={isOpen}
746
784
  onOpenChange={setIsOpen}
747
785
  aria-label="Footer with Left Action"
786
+ closeButtonAriaLabel="Close Modal"
748
787
  modalTrigger={
749
788
  <Button appearance="primary">Open Modal with Delete Action</Button>
750
789
  }
@@ -802,6 +841,7 @@ export const CustomFooterOverride: Story = {
802
841
  return (
803
842
  <Modal
804
843
  aria-label="Custom Footer Override"
844
+ closeButtonAriaLabel="Close Modal"
805
845
  modalTrigger={<Button appearance="primary">Open Custom Footer</Button>}
806
846
  >
807
847
  <ModalHeader
@@ -30,6 +30,7 @@ import { RAIL_BUTTON_SIZE } from "../../shared/constants";
30
30
  * onClick={() => console.log('expand')}
31
31
  * />
32
32
  */
33
+
33
34
  const RailButton = styled.button`
34
35
  width: ${RAIL_BUTTON_SIZE}px;
35
36
  height: ${(props) => props.theme.space[500]};
@@ -74,6 +75,9 @@ export const ModalAction: React.FC<TypeModalActionProps> = ({
74
75
  }) => {
75
76
  const button = (
76
77
  <RailButton
78
+ data-slot="modal-action"
79
+ data-qa-modal-action
80
+ data-qa-modal-action-type={actionType || "button"}
77
81
  aria-label={ariaLabel}
78
82
  title={ariaLabel}
79
83
  disabled={disabled}
@@ -44,7 +44,12 @@ StyledModalBody.displayName = "ModalBody";
44
44
  export const ModalBody = React.forwardRef<HTMLDivElement, TypeModalBodyProps>(
45
45
  ({ children, ...rest }, ref) => {
46
46
  return (
47
- <StyledModalBody data-qa-modal-body ref={ref} {...rest}>
47
+ <StyledModalBody
48
+ data-slot="modal-body"
49
+ data-qa-modal-body
50
+ ref={ref}
51
+ {...rest}
52
+ >
48
53
  {children}
49
54
  </StyledModalBody>
50
55
  );
@@ -27,7 +27,13 @@ export const ModalCloseWrapper = (props: ModalCloseWrapperProps) => {
27
27
 
28
28
  return (
29
29
  <Dialog.Close asChild={asChild} onClick={handleClick} {...rest}>
30
- {children}
30
+ {React.isValidElement(children)
31
+ ? React.cloneElement(children as React.ReactElement<any>, {
32
+ "data-slot": "modal-close-wrapper",
33
+ "data-qa-modal-close-wrapper": "",
34
+ ...((children as React.ReactElement<any>).props || {}),
35
+ })
36
+ : children}
31
37
  </Dialog.Close>
32
38
  );
33
39
  };
@@ -32,11 +32,15 @@ interface StyledContentProps
32
32
  }
33
33
 
34
34
  // Styled motion.div wrapper that handles positioning
35
- const StyledMotionWrapper = styled(motion.div)`
35
+ const StyledMotionWrapper = styled(motion.div)<{
36
+ $isMobile: boolean;
37
+ $zIndex?: number;
38
+ }>`
36
39
  position: fixed;
37
- top: ${(props: { $isMobile: boolean }) => (props.$isMobile ? "auto" : "50%")};
40
+ top: ${(props) => (props.$isMobile ? "auto" : "50%")};
38
41
  left: 50%;
39
- bottom: ${(props: { $isMobile: boolean }) => (props.$isMobile ? 0 : "auto")};
42
+ bottom: ${(props) => (props.$isMobile ? 0 : "auto")};
43
+ z-index: ${(props) => (props.$zIndex ? props.$zIndex + 1 : 7)};
40
44
  `;
41
45
 
42
46
  export const StyledContent = styled.div.withConfig({
@@ -113,6 +117,7 @@ interface ModalContentProps {
113
117
  label?: string;
114
118
  dataAttributes: Record<string, string>;
115
119
  draggable?: boolean;
120
+ zIndex?: number;
116
121
  rest: any;
117
122
  }
118
123
 
@@ -124,6 +129,7 @@ export const StaticModalContent: React.FC<ModalContentProps> = ({
124
129
  children,
125
130
  label,
126
131
  dataAttributes,
132
+ zIndex,
127
133
  rest,
128
134
  }) => {
129
135
  const isMobile = useIsMobile();
@@ -134,12 +140,18 @@ export const StaticModalContent: React.FC<ModalContentProps> = ({
134
140
  <Dialog.Content asChild aria-label={label}>
135
141
  <StyledMotionWrapper
136
142
  $isMobile={isMobile}
143
+ $zIndex={zIndex}
137
144
  variants={contentVariants}
138
145
  initial="initial"
139
146
  animate="animate"
140
147
  exit="exit"
141
148
  >
142
- <StyledContent draggable={false} {...dataAttributes} {...rest}>
149
+ <StyledContent
150
+ data-slot="modal-content"
151
+ draggable={false}
152
+ {...dataAttributes}
153
+ {...rest}
154
+ >
143
155
  {children}
144
156
  </StyledContent>
145
157
  </StyledMotionWrapper>
@@ -156,6 +168,7 @@ export const DraggableModalContent: React.FC<ModalContentProps> = ({
156
168
  children,
157
169
  label,
158
170
  dataAttributes,
171
+ zIndex,
159
172
  rest,
160
173
  }) => {
161
174
  const [position, setPosition] = React.useState({ x: 0, y: 0 });
@@ -265,6 +278,7 @@ export const DraggableModalContent: React.FC<ModalContentProps> = ({
265
278
  >
266
279
  <StyledMotionWrapper
267
280
  $isMobile={isMobile}
281
+ $zIndex={zIndex}
268
282
  variants={contentVariants}
269
283
  initial="initial"
270
284
  animate="animate"
@@ -278,6 +292,7 @@ export const DraggableModalContent: React.FC<ModalContentProps> = ({
278
292
  }}
279
293
  >
280
294
  <StyledContent
295
+ data-slot="modal-content"
281
296
  ref={contentRef}
282
297
  draggable={true}
283
298
  isDragging={isDragging}
@@ -21,7 +21,12 @@ export const ModalDescription = React.forwardRef<
21
21
  >(({ children, descriptionProps = {}, ...rest }, ref) => {
22
22
  return (
23
23
  <Dialog.Description asChild {...descriptionProps}>
24
- <Box ref={ref} {...rest}>
24
+ <Box
25
+ data-slot="modal-description"
26
+ data-qa-modal-description
27
+ ref={ref}
28
+ {...rest}
29
+ >
25
30
  {children}
26
31
  </Box>
27
32
  </Dialog.Description>
@@ -83,7 +83,7 @@ export const ModalFooter = (props: TypeModalFooterProps) => {
83
83
 
84
84
  // Build simplified API layout
85
85
  return (
86
- <ModalCustomFooter {...rest}>
86
+ <ModalCustomFooter data-slot="modal-footer" data-qa-modal-footer {...rest}>
87
87
  {/* Left action (e.g., Delete button) */}
88
88
  {leftAction ? leftAction : null}
89
89
 
@@ -77,6 +77,8 @@ export const ModalHeader = (props: TypeModalHeaderProps) => {
77
77
 
78
78
  return (
79
79
  <ModalCustomHeader
80
+ data-slot="modal-header"
81
+ data-qa-modal-header
80
82
  {...rest}
81
83
  onMouseDown={isDraggable ? dragContext?.onHeaderMouseDown : undefined}
82
84
  draggable={isDraggable}
@@ -21,12 +21,13 @@ interface StyledOverlayProps
21
21
  }
22
22
 
23
23
  // Styled motion.div for the overlay wrapper
24
- export const StyledMotionOverlay = styled(motion.div)`
24
+ export const StyledMotionOverlay = styled(motion.div)<{ $zIndex?: number }>`
25
25
  position: fixed;
26
26
  top: 0px;
27
27
  left: 0px;
28
28
  right: 0px;
29
29
  bottom: 0px;
30
+ z-index: ${(props) => props.$zIndex ?? 6};
30
31
  `;
31
32
 
32
33
  export const StyledOverlay = styled.div.withConfig({