@sproutsocial/seeds-react-modal 1.0.2 → 1.0.4

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 (54) hide show
  1. package/.turbo/turbo-build.log +39 -19
  2. package/CHANGELOG.md +19 -0
  3. package/dist/Modal-ki8oiGbC.d.mts +69 -0
  4. package/dist/Modal-ki8oiGbC.d.ts +69 -0
  5. package/dist/ModalRail-OQ8DZ1vH.d.mts +178 -0
  6. package/dist/ModalRail-OQ8DZ1vH.d.ts +178 -0
  7. package/dist/esm/chunk-GKQRFPCX.js +642 -0
  8. package/dist/esm/chunk-GKQRFPCX.js.map +1 -0
  9. package/dist/esm/chunk-IYDY4OPB.js +237 -0
  10. package/dist/esm/chunk-IYDY4OPB.js.map +1 -0
  11. package/dist/esm/index.js +28 -235
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/v1/index.js +9 -0
  14. package/dist/esm/v1/index.js.map +1 -0
  15. package/dist/esm/v2/index.js +39 -0
  16. package/dist/esm/v2/index.js.map +1 -0
  17. package/dist/index.d.mts +11 -66
  18. package/dist/index.d.ts +11 -66
  19. package/dist/index.js +658 -17
  20. package/dist/index.js.map +1 -1
  21. package/dist/v1/index.d.mts +11 -0
  22. package/dist/v1/index.d.ts +11 -0
  23. package/dist/v1/index.js +273 -0
  24. package/dist/v1/index.js.map +1 -0
  25. package/dist/v2/index.d.mts +26 -0
  26. package/dist/v2/index.d.ts +26 -0
  27. package/dist/v2/index.js +694 -0
  28. package/dist/v2/index.js.map +1 -0
  29. package/package.json +36 -16
  30. package/src/Modal.stories.tsx +1 -1
  31. package/src/__tests__/v1/Modal.test.tsx +134 -0
  32. package/src/__tests__/v1/Modal.typetest.tsx +209 -0
  33. package/src/index.ts +36 -3
  34. package/src/shared/constants.ts +28 -0
  35. package/src/v1/Modal.tsx +159 -0
  36. package/src/v1/ModalTypes.ts +67 -0
  37. package/src/v1/index.ts +14 -0
  38. package/src/v1/styles.tsx +141 -0
  39. package/src/v2/ModalV2.stories.tsx +282 -0
  40. package/src/v2/ModalV2.tsx +306 -0
  41. package/src/v2/ModalV2Styles.tsx +150 -0
  42. package/src/v2/ModalV2Types.ts +158 -0
  43. package/src/v2/components/ModalClose.tsx +29 -0
  44. package/src/v2/components/ModalCloseButton.tsx +100 -0
  45. package/src/v2/components/ModalContent.tsx +16 -0
  46. package/src/v2/components/ModalDescription.tsx +19 -0
  47. package/src/v2/components/ModalFooter.tsx +20 -0
  48. package/src/v2/components/ModalHeader.tsx +52 -0
  49. package/src/v2/components/ModalRail.tsx +121 -0
  50. package/src/v2/components/ModalTrigger.tsx +39 -0
  51. package/src/v2/components/index.ts +8 -0
  52. package/src/v2/index.ts +37 -0
  53. package/tsconfig.json +7 -1
  54. package/tsup.config.ts +5 -1
@@ -0,0 +1,159 @@
1
+ import * as React from "react";
2
+ import { useContext } from "react";
3
+ import Box from "@sproutsocial/seeds-react-box";
4
+ import Button from "@sproutsocial/seeds-react-button";
5
+ import Icon from "@sproutsocial/seeds-react-icon";
6
+ import Text from "@sproutsocial/seeds-react-text";
7
+ import { Container, Content, Header, Footer, Body } from "./styles";
8
+ import type {
9
+ TypeModalProps,
10
+ TypeModalCloseButtonProps,
11
+ TypeModalContentProps,
12
+ TypeModalFooterProps,
13
+ TypeModalHeaderProps,
14
+ } from "./ModalTypes";
15
+
16
+ type TypeModalContext = Partial<{
17
+ onClose: () => void;
18
+ closeButtonLabel: string;
19
+ label: string;
20
+ }>;
21
+
22
+ const ModalContext = React.createContext<TypeModalContext>({});
23
+
24
+ const ModalHeader = (props: TypeModalHeaderProps) => {
25
+ const { title, subtitle, children, bordered, ...rest } = props;
26
+ return (
27
+ <Header bordered={title || subtitle || bordered} {...rest}>
28
+ {children ? (
29
+ children
30
+ ) : (
31
+ <React.Fragment>
32
+ <Box>
33
+ {title && (
34
+ <Text as="h1" fontSize={400} fontWeight="semibold">
35
+ {title}
36
+ </Text>
37
+ )}
38
+ {subtitle && (
39
+ <Text as="div" fontSize={200}>
40
+ {subtitle}
41
+ </Text>
42
+ )}
43
+ </Box>
44
+ <Box display="flex" alignItems="center" justify-content="flex-end">
45
+ <ModalCloseButton ml={400} />
46
+ </Box>
47
+ </React.Fragment>
48
+ )}
49
+ </Header>
50
+ );
51
+ };
52
+
53
+ const ModalCloseButton = (props: TypeModalCloseButtonProps) => {
54
+ const { onClose, closeButtonLabel } = useContext(ModalContext);
55
+ if (!onClose) return null;
56
+ return (
57
+ <Button onClick={onClose} {...props}>
58
+ <Icon name="x-outline" ariaLabel={closeButtonLabel} />
59
+ </Button>
60
+ );
61
+ };
62
+
63
+ const ModalFooter = ({
64
+ bg = "container.background.base",
65
+ ...rest
66
+ }: TypeModalFooterProps) => (
67
+ <Footer
68
+ bg={bg}
69
+ borderTop={500}
70
+ borderColor="container.border.base"
71
+ {...rest}
72
+ />
73
+ );
74
+
75
+ const ModalContent = React.forwardRef(
76
+ ({ children, ...rest }: TypeModalContentProps, ref) => {
77
+ const { label } = useContext(ModalContext);
78
+ return (
79
+ <Content data-qa-modal data-qa-label={label} ref={ref} {...rest}>
80
+ {children}
81
+ </Content>
82
+ );
83
+ }
84
+ );
85
+
86
+ /**
87
+ * The modal you want
88
+ */
89
+ const Modal = (props: TypeModalProps) => {
90
+ const {
91
+ appElementSelector,
92
+ children,
93
+ isOpen,
94
+ label,
95
+ onClose,
96
+ closeButtonLabel,
97
+ width = "800px",
98
+ zIndex = 6,
99
+ data = {},
100
+ ...rest
101
+ } = props;
102
+
103
+ const isCloseable = Boolean(onClose);
104
+ const appElement =
105
+ appElementSelector && document
106
+ ? (document.querySelector(appElementSelector) as HTMLElement)
107
+ : undefined;
108
+
109
+ return (
110
+ <Container
111
+ appElement={appElement}
112
+ ariaHideApp={!!appElement}
113
+ isOpen={isOpen}
114
+ contentLabel={label}
115
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
116
+ onRequestClose={onClose || (() => {})}
117
+ shouldFocusAfterRender={true}
118
+ shouldCloseOnOverlayClick={isCloseable}
119
+ shouldCloseOnEsc={isCloseable}
120
+ shouldReturnFocusAfterClose={true}
121
+ closeTimeoutMS={200}
122
+ role="dialog"
123
+ width={width}
124
+ zIndex={zIndex}
125
+ data={{
126
+ "qa-modal": "",
127
+ "qa-modal-isopen": isOpen,
128
+ ...data,
129
+ }}
130
+ {...rest}
131
+ >
132
+ <React.Fragment>
133
+ <Body />
134
+
135
+ <ModalContext.Provider
136
+ value={{
137
+ onClose,
138
+ closeButtonLabel,
139
+ label,
140
+ }}
141
+ >
142
+ {children}
143
+ </ModalContext.Provider>
144
+ </React.Fragment>
145
+ </Container>
146
+ );
147
+ };
148
+
149
+ ModalHeader.displayName = "Modal.Header";
150
+ ModalFooter.displayName = "Modal.Footer";
151
+ ModalContent.displayName = "Modal.Content";
152
+ ModalCloseButton.displayName = "Modal.CloseButton";
153
+
154
+ Modal.Header = ModalHeader;
155
+ Modal.Footer = ModalFooter;
156
+ Modal.Content = ModalContent;
157
+ Modal.CloseButton = ModalCloseButton;
158
+
159
+ export default Modal;
@@ -0,0 +1,67 @@
1
+ import * as React from "react";
2
+ import ReactModal from "react-modal";
3
+ import type {
4
+ TypeBoxProps,
5
+ TypeContainerProps,
6
+ } from "@sproutsocial/seeds-react-box";
7
+ import type { TypeButtonProps } from "@sproutsocial/seeds-react-button";
8
+
9
+ export interface TypeModalCloseButtonProps
10
+ extends Omit<TypeButtonProps, "children"> {
11
+ children?: void | null;
12
+ }
13
+
14
+ export interface TypeModalHeaderProps extends TypeBoxProps {
15
+ title?: string;
16
+ subtitle?: string;
17
+
18
+ /** Passing children will override the default modal header */
19
+ children?: React.ReactNode;
20
+
21
+ /** If you're rendering a custom header, you can use this prop to add a bottom border */
22
+ bordered?: boolean;
23
+ }
24
+
25
+ export interface TypeModalFooterProps extends TypeBoxProps {
26
+ bg?: string;
27
+ children: React.ReactNode;
28
+ }
29
+
30
+ export interface TypeModalContentProps extends TypeBoxProps {
31
+ children?: React.ReactNode;
32
+ }
33
+
34
+ export interface TypeModalProps
35
+ extends TypeContainerProps,
36
+ // @ts-notes - onClose is an alias for onRequestClose so we don't need to include it here
37
+ Omit<ReactModal.Props, keyof TypeContainerProps | "onRequestClose"> {
38
+ /** section of app to aria hide for the modal */
39
+ appElementSelector?: string;
40
+
41
+ /** trigger to open or close the modal */
42
+ isOpen: boolean;
43
+
44
+ /** label for screen readers to announce the modal */
45
+ label: string;
46
+
47
+ /** body content of the modal */
48
+ children: React.ReactNode;
49
+
50
+ /** callback for close */
51
+ onClose?: () => void;
52
+
53
+ /** aria-label for modal X */
54
+ closeButtonLabel: string;
55
+
56
+ /** Controls the z-index CSS property */
57
+ zIndex?: number;
58
+
59
+ /** The max width of the modal container */
60
+ width?: string | number;
61
+
62
+ /**
63
+ * Custom attributes to be added to the modals container
64
+ * Each key will be prepended with "data-" when rendered in the DOM
65
+ */
66
+ data?: Record<string, string | boolean | number>;
67
+ }
@@ -0,0 +1,14 @@
1
+ // V1 Modal - Explicit exports for optimal tree shaking
2
+ import Modal from "./Modal";
3
+
4
+ export default Modal;
5
+ export { Modal };
6
+
7
+ // Explicit type exports
8
+ export type {
9
+ TypeModalProps,
10
+ TypeModalHeaderProps,
11
+ TypeModalFooterProps,
12
+ TypeModalContentProps,
13
+ TypeModalCloseButtonProps,
14
+ } from "./ModalTypes";
@@ -0,0 +1,141 @@
1
+ import React from "react";
2
+ import styled, { createGlobalStyle } from "styled-components";
3
+ import { width, zIndex } from "styled-system";
4
+ import ReactModal from "react-modal";
5
+ import { COMMON } from "@sproutsocial/seeds-react-system-props";
6
+ import Box, { type TypeContainerProps } from "@sproutsocial/seeds-react-box";
7
+
8
+ // This is the max space allowed between the modal and the edge of the browser
9
+ const BODY_PADDING = "64px";
10
+
11
+ const ReactModalAdapter = ({
12
+ className = "",
13
+ ...props
14
+ }: { className?: string } & Omit<
15
+ ReactModal.Props,
16
+ "portalClassName" | "className" | "overlayClassName"
17
+ >) => {
18
+ // We want to create *__Content and *__Overlay class names on the subcomponents.
19
+ // Because `className` could be a space-separated list of class names, we make
20
+ // sure that we append `__Content` and `__Overlay` to every class name.
21
+ const contentClassName = className
22
+ .split(" ")
23
+ .map((className) => `${className} ${className}__Content`)
24
+ .join(" ");
25
+
26
+ const overlayClassName = className
27
+ .split(" ")
28
+ .map((className) => `${className} ${className}__Overlay`)
29
+ .join(" ");
30
+
31
+ return (
32
+ <ReactModal
33
+ portalClassName={className}
34
+ className={contentClassName}
35
+ overlayClassName={overlayClassName}
36
+ {...props}
37
+ />
38
+ );
39
+ };
40
+
41
+ export const Body = createGlobalStyle`
42
+ .ReactModal__Body--open {
43
+ overflow: hidden;
44
+ }
45
+ `;
46
+
47
+ export const Container = styled(ReactModalAdapter)<TypeContainerProps>`
48
+ &__Overlay {
49
+ position: fixed;
50
+ top: 0px;
51
+ left: 0px;
52
+ right: 0px;
53
+ bottom: 0px;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ background-color: ${(props) => props.theme.colors.overlay.background.base};
58
+ opacity: 0;
59
+ will-change: opacity;
60
+ transition: opacity ${(props) => props.theme.duration.medium}
61
+ ${(props) => props.theme.easing.ease_inout};
62
+
63
+ ${zIndex}
64
+
65
+ &.ReactModal__Overlay--after-open {
66
+ opacity: 1;
67
+ }
68
+ &.ReactModal__Overlay--before-close {
69
+ opacity: 0;
70
+ }
71
+ }
72
+
73
+ &__Content {
74
+ display: flex;
75
+ flex-direction: column;
76
+ background: ${(props) => props.theme.colors.container.background.base};
77
+ border-radius: ${(props) => props.theme.radii[600]};
78
+ box-shadow: ${(props) => props.theme.shadows.medium};
79
+ filter: blur(0);
80
+ color: ${(props) => props.theme.colors.text.body};
81
+
82
+ outline: none;
83
+ max-width: calc(100vw - ${BODY_PADDING});
84
+ max-height: calc(100vh - ${BODY_PADDING});
85
+ @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
86
+ /**
87
+ * This prevents the modal from being very short in IE11. Better too big
88
+ * than too small.
89
+ */
90
+ height: calc(100vh - ${BODY_PADDING});
91
+ }
92
+
93
+ ${width}
94
+
95
+ ${COMMON}
96
+ }
97
+ `;
98
+
99
+ export const Content = styled(Box)`
100
+ font-family: ${(props) => props.theme.fontFamily};
101
+ min-height: 80px;
102
+ overflow-y: auto;
103
+ flex: 1 1 auto;
104
+ padding: ${(props) => props.theme.space[400]}
105
+ ${(props) => props.theme.space[450]};
106
+ @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
107
+ /* 'flex-basis: auto' breaks overflow in IE11 */
108
+ flex-basis: 100%;
109
+ }
110
+ `;
111
+
112
+ export const HeaderContainer = styled(Box)`
113
+ font-family: ${(props) => props.theme.fontFamily};
114
+ padding: ${(props) => props.theme.space[400]}
115
+ ${(props) => props.theme.space[450]};
116
+ `;
117
+
118
+ export const Header = styled(HeaderContainer)<{ bordered?: boolean }>`
119
+ display: flex;
120
+ align-items: center;
121
+ justify-content: space-between;
122
+ flex: 0 0 auto;
123
+ border-bottom-width: ${(props) => props.theme.borderWidths[500]};
124
+ border-bottom-color: ${(props) =>
125
+ props.bordered ? props.theme.colors.container.border.base : "transparent"};
126
+ border-bottom-style: solid;
127
+ `;
128
+
129
+ export const Footer = styled(Box)`
130
+ flex: 0 0 auto;
131
+ font-family: ${(props) => props.theme.fontFamily};
132
+ padding: ${(props) => props.theme.space[400]}
133
+ ${(props) => props.theme.space[450]};
134
+ border-bottom-right-radius: ${(props) => props.theme.radii[500]};
135
+ border-bottom-left-radius: ${(props) => props.theme.radii[500]};
136
+ `;
137
+
138
+ Container.displayName = "ModalContainer";
139
+ Content.displayName = "Content";
140
+ Header.displayName = "Modal.Header";
141
+ Footer.displayName = "Modal.Footer";
@@ -0,0 +1,282 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Box } from "@sproutsocial/seeds-react-box";
4
+ import { Button } from "@sproutsocial/seeds-react-button";
5
+ import { Text } from "@sproutsocial/seeds-react-text";
6
+ import { Modal, ModalHeader, ModalFooter, ModalContent, ModalClose } from "./";
7
+
8
+ const meta: Meta<typeof Modal> = {
9
+ title: "Components/Modal (Radix UI)",
10
+ component: Modal,
11
+ parameters: {
12
+ docs: {
13
+ description: {
14
+ component:
15
+ "Modal is the new modal component built with Radix UI Dialog. It provides the same API as the original Modal but with improved accessibility and modern React patterns.",
16
+ },
17
+ },
18
+ },
19
+ argTypes: {
20
+ open: {
21
+ control: { type: "boolean" },
22
+ description:
23
+ "Controls whether the modal is open or closed (controlled mode)",
24
+ },
25
+ defaultOpen: {
26
+ control: { type: "boolean" },
27
+ description: "Default open state for uncontrolled mode",
28
+ },
29
+ onOpenChange: {
30
+ action: "openChanged",
31
+ description: "Callback when open state changes",
32
+ },
33
+ "aria-label": {
34
+ control: { type: "text" },
35
+ description: "Accessibility label for screen readers",
36
+ },
37
+ showOverlay: {
38
+ control: { type: "boolean" },
39
+ description: "Whether to show the background overlay",
40
+ },
41
+ title: {
42
+ control: { type: "text" },
43
+ description: "Modal title (auto-creates header)",
44
+ },
45
+ subtitle: {
46
+ control: { type: "text" },
47
+ description: "Modal subtitle (auto-creates header)",
48
+ },
49
+ description: {
50
+ control: { type: "text" },
51
+ description: "Modal description (auto-wrapped in Dialog.Description)",
52
+ },
53
+ size: {
54
+ control: { type: "select" },
55
+ options: ["small", "medium", "large", "full", "400px", "800px"],
56
+ description: "Modal size preset or custom width",
57
+ },
58
+ priority: {
59
+ control: { type: "select" },
60
+ options: ["low", "medium", "high"],
61
+ description: "Priority level for z-index",
62
+ },
63
+ draggable: {
64
+ control: { type: "boolean" },
65
+ description: "Enable draggable functionality",
66
+ },
67
+ },
68
+ args: {
69
+ "aria-label": "Example Modal",
70
+ showOverlay: true,
71
+ size: "medium",
72
+ draggable: false,
73
+ },
74
+ };
75
+ export default meta;
76
+
77
+ type Story = StoryObj<typeof Modal>;
78
+
79
+ export const Default: Story = {
80
+ render: (args) => {
81
+ return (
82
+ <Modal
83
+ {...args}
84
+ modalTrigger={<Button appearance="primary">Open Modal</Button>}
85
+ >
86
+ <ModalContent>
87
+ <ModalHeader title="Modal Title" subtitle="This is a subtitle" />
88
+ <Box
89
+ border="1px dashed"
90
+ borderColor="purple.500"
91
+ p={500}
92
+ bg="purple.200"
93
+ borderRadius="6px"
94
+ >
95
+ <Text>
96
+ This modal uses uncontrolled state - no need to manage open/close
97
+ state! The modalTrigger prop and floating close button handle
98
+ everything.
99
+ </Text>
100
+ </Box>
101
+ <ModalFooter>
102
+ <Box display="flex" justifyContent="flex-end" gap={300}>
103
+ <ModalClose asChild>
104
+ <Button>Cancel</Button>
105
+ </ModalClose>
106
+ <ModalClose asChild>
107
+ <Button appearance="primary">Confirm</Button>
108
+ </ModalClose>
109
+ </Box>
110
+ </ModalFooter>
111
+ </ModalContent>
112
+ </Modal>
113
+ );
114
+ },
115
+ };
116
+
117
+ export const WithActions: Story = {
118
+ render: () => {
119
+ return (
120
+ <Modal
121
+ size="medium"
122
+ modalTrigger={
123
+ <Button appearance="primary">Open Modal With Actions</Button>
124
+ }
125
+ actions={[
126
+ {
127
+ "aria-label": "Expand",
128
+ iconName: "arrows-pointing-out",
129
+ onClick: () => console.log("expand"),
130
+ },
131
+ {
132
+ "aria-label": "React",
133
+ iconName: "sprout-leaf-outline",
134
+ onClick: () => console.log("reacted"),
135
+ },
136
+ ]}
137
+ >
138
+ <ModalContent>
139
+ <ModalHeader
140
+ title="Modal With Actions"
141
+ subtitle="This modal has extra floating actions"
142
+ />
143
+ <Box
144
+ border="1px dashed"
145
+ borderColor="purple.500"
146
+ p={500}
147
+ bg="purple.200"
148
+ borderRadius="6px"
149
+ >
150
+ <Text>
151
+ This modal shows the floating close by default and includes extra
152
+ actions on the rail via the actions prop. The floating close
153
+ button is always present, and additional actions appear below it.
154
+ </Text>
155
+ </Box>
156
+ <ModalFooter>
157
+ <Box display="flex" justifyContent="flex-end" gap={300}>
158
+ <ModalClose asChild>
159
+ <Button>Cancel</Button>
160
+ </ModalClose>
161
+ <ModalClose asChild>
162
+ <Button appearance="primary">Confirm</Button>
163
+ </ModalClose>
164
+ </Box>
165
+ </ModalFooter>
166
+ </ModalContent>
167
+ </Modal>
168
+ );
169
+ },
170
+ };
171
+
172
+ export const DraggableModal: Story = {
173
+ render: () => {
174
+ return (
175
+ <Modal
176
+ aria-label="Draggable Modal"
177
+ size="medium"
178
+ draggable={true}
179
+ showOverlay={false}
180
+ modalTrigger={
181
+ <Button appearance="primary">Open Draggable Modal</Button>
182
+ }
183
+ >
184
+ <ModalContent>
185
+ <ModalHeader
186
+ title="Draggable Modal"
187
+ subtitle="Click and drag anywhere on this modal to move it around"
188
+ />
189
+ <Box
190
+ border="1px dashed"
191
+ borderColor="blue.500"
192
+ p={500}
193
+ bg="blue.200"
194
+ borderRadius="6px"
195
+ >
196
+ <Text>
197
+ This modal is draggable! You can click and drag anywhere on the
198
+ modal to move it around the screen. The overlay has been disabled
199
+ so you can see the modal moving freely. Try dragging it to
200
+ different corners of the screen.
201
+ </Text>
202
+ </Box>
203
+ <ModalFooter>
204
+ <Box display="flex" justifyContent="flex-end" gap={300}>
205
+ <ModalClose asChild>
206
+ <Button appearance="primary">Got it!</Button>
207
+ </ModalClose>
208
+ </Box>
209
+ </ModalFooter>
210
+ </ModalContent>
211
+ </Modal>
212
+ );
213
+ },
214
+ };
215
+
216
+ export const ControlledState: Story = {
217
+ render: () => {
218
+ const [isOpen, setIsOpen] = useState(false);
219
+ const [count, setCount] = useState(0);
220
+
221
+ return (
222
+ <div>
223
+ <Box mb={400}>
224
+ <Text>
225
+ Use controlled state when you need to manage modal state externally.
226
+ Modal opened count: {count}
227
+ </Text>
228
+ </Box>
229
+
230
+ <Modal
231
+ open={isOpen}
232
+ onOpenChange={(open) => {
233
+ setIsOpen(open);
234
+ if (open) setCount((c) => c + 1);
235
+ }}
236
+ aria-label="Controlled Modal"
237
+ size="medium"
238
+ modalTrigger={
239
+ <Button appearance="primary">Open Controlled Modal</Button>
240
+ }
241
+ >
242
+ <ModalContent>
243
+ <ModalHeader
244
+ title="Controlled Modal"
245
+ subtitle="This modal's state is managed externally"
246
+ />
247
+ <Box
248
+ border="1px dashed"
249
+ borderColor="purple.500"
250
+ p={500}
251
+ bg="purple.200"
252
+ borderRadius="6px"
253
+ >
254
+ <Text>
255
+ This modal's state is controlled by the parent component. You
256
+ can track when it opens/closes and perform side effects. The
257
+ floating close button still works, but the state is managed
258
+ externally.
259
+ </Text>
260
+ </Box>
261
+ <ModalFooter>
262
+ <Box display="flex" justifyContent="flex-end" gap={300}>
263
+ <ModalClose asChild>
264
+ <Button>Cancel</Button>
265
+ </ModalClose>
266
+ <ModalClose asChild>
267
+ <Button appearance="primary">Confirm</Button>
268
+ </ModalClose>
269
+ </Box>
270
+ </ModalFooter>
271
+ </ModalContent>
272
+ </Modal>
273
+
274
+ <Box mt={300}>
275
+ <Button appearance="secondary" onClick={() => setIsOpen(!isOpen)}>
276
+ {isOpen ? "Close" : "Open"} Modal Programmatically
277
+ </Button>
278
+ </Box>
279
+ </div>
280
+ );
281
+ },
282
+ };