@sproutsocial/seeds-react-modal 1.0.2 → 1.0.3
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 +39 -19
- package/CHANGELOG.md +8 -0
- package/dist/Modal-ki8oiGbC.d.mts +69 -0
- package/dist/Modal-ki8oiGbC.d.ts +69 -0
- package/dist/ModalRail-OQ8DZ1vH.d.mts +178 -0
- package/dist/ModalRail-OQ8DZ1vH.d.ts +178 -0
- package/dist/esm/chunk-GKQRFPCX.js +642 -0
- package/dist/esm/chunk-GKQRFPCX.js.map +1 -0
- package/dist/esm/chunk-IYDY4OPB.js +237 -0
- package/dist/esm/chunk-IYDY4OPB.js.map +1 -0
- package/dist/esm/index.js +28 -235
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/v1/index.js +9 -0
- package/dist/esm/v1/index.js.map +1 -0
- package/dist/esm/v2/index.js +39 -0
- package/dist/esm/v2/index.js.map +1 -0
- package/dist/index.d.mts +11 -66
- package/dist/index.d.ts +11 -66
- package/dist/index.js +658 -17
- package/dist/index.js.map +1 -1
- package/dist/v1/index.d.mts +11 -0
- package/dist/v1/index.d.ts +11 -0
- package/dist/v1/index.js +273 -0
- package/dist/v1/index.js.map +1 -0
- package/dist/v2/index.d.mts +26 -0
- package/dist/v2/index.d.ts +26 -0
- package/dist/v2/index.js +694 -0
- package/dist/v2/index.js.map +1 -0
- package/package.json +33 -13
- package/src/Modal.stories.tsx +1 -1
- package/src/__tests__/v1/Modal.test.tsx +134 -0
- package/src/__tests__/v1/Modal.typetest.tsx +209 -0
- package/src/index.ts +36 -3
- package/src/shared/constants.ts +28 -0
- package/src/v1/Modal.tsx +159 -0
- package/src/v1/ModalTypes.ts +67 -0
- package/src/v1/index.ts +14 -0
- package/src/v1/styles.tsx +141 -0
- package/src/v2/ModalV2.stories.tsx +282 -0
- package/src/v2/ModalV2.tsx +306 -0
- package/src/v2/ModalV2Styles.tsx +150 -0
- package/src/v2/ModalV2Types.ts +158 -0
- package/src/v2/components/ModalClose.tsx +29 -0
- package/src/v2/components/ModalCloseButton.tsx +100 -0
- package/src/v2/components/ModalContent.tsx +16 -0
- package/src/v2/components/ModalDescription.tsx +19 -0
- package/src/v2/components/ModalFooter.tsx +20 -0
- package/src/v2/components/ModalHeader.tsx +52 -0
- package/src/v2/components/ModalRail.tsx +121 -0
- package/src/v2/components/ModalTrigger.tsx +39 -0
- package/src/v2/components/index.ts +8 -0
- package/src/v2/index.ts +37 -0
- package/tsconfig.json +7 -1
- package/tsup.config.ts +5 -1
package/src/v1/Modal.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/v1/index.ts
ADDED
|
@@ -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
|
+
};
|