@sproutsocial/seeds-react-modal 1.0.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.
@@ -0,0 +1,134 @@
1
+ /* eslint-disable */
2
+ import React from "react";
3
+ import { COLOR_PURPLE_300 } from "@sproutsocial/seeds-color";
4
+ import {
5
+ render,
6
+ fireEvent,
7
+ cleanup,
8
+ screen,
9
+ } from "@sproutsocial/seeds-react-testing-library";
10
+ import Modal from "../Modal";
11
+
12
+ afterEach(() => cleanup());
13
+
14
+ describe("Modal", () => {
15
+ it("renders a custom background color", () => {
16
+ // Use baseElement since it renders in a Portal
17
+ render(
18
+ <Modal
19
+ isOpen={true}
20
+ label="Label"
21
+ bg={COLOR_PURPLE_300}
22
+ onClose={() => {}}
23
+ closeButtonLabel="Close this dialog"
24
+ >
25
+ ajdsfljasdlfjlasdjf
26
+ </Modal>
27
+ );
28
+
29
+ expect(screen.getByRole("dialog")).toHaveStyleRule(
30
+ "background-color",
31
+ COLOR_PURPLE_300
32
+ );
33
+ });
34
+
35
+ it("should close on overlay click and esc", () => {
36
+ const onClose = jest.fn();
37
+ render(
38
+ <Modal
39
+ isOpen={true}
40
+ label="Label"
41
+ bg={COLOR_PURPLE_300}
42
+ onClose={onClose}
43
+ closeButtonLabel="Close this dialog"
44
+ >
45
+ <Modal.Header>
46
+ <Modal.CloseButton />
47
+ </Modal.Header>
48
+ ajdsfljasdlfjlasdjf
49
+ </Modal>
50
+ );
51
+
52
+ fireEvent.click(screen.getByRole("dialog").parentElement as HTMLElement);
53
+ expect(onClose).toHaveBeenCalled();
54
+
55
+ onClose.mockClear();
56
+ fireEvent.keyDown(screen.getByText("ajdsfljasdlfjlasdjf"), {
57
+ key: "Escape",
58
+ keyCode: 27,
59
+ });
60
+ expect(onClose).toHaveBeenCalled();
61
+
62
+ onClose.mockClear();
63
+ fireEvent.click(screen.getByLabelText("Close this dialog"));
64
+ expect(onClose).toHaveBeenCalled();
65
+ });
66
+
67
+ it("should NOT close on overlay click and esc if onClick is not provided", () => {
68
+ const onClose = jest.fn();
69
+ const { baseElement, getByText, queryByLabelText } = render(
70
+ <Modal
71
+ isOpen={true}
72
+ label="Label"
73
+ bg={COLOR_PURPLE_300}
74
+ closeButtonLabel="Close this dialog"
75
+ >
76
+ ajdsfljasdlfjlasdjf
77
+ </Modal>
78
+ );
79
+
80
+ expect(queryByLabelText("Close this dialog")).not.toBeInTheDocument();
81
+
82
+ fireEvent.click(screen.getByRole("dialog").parentElement as HTMLElement);
83
+ expect(onClose).not.toHaveBeenCalled();
84
+
85
+ onClose.mockClear();
86
+ fireEvent.keyDown(screen.getByText("ajdsfljasdlfjlasdjf"), {
87
+ key: "Escape",
88
+ keyCode: 27,
89
+ });
90
+ expect(onClose).not.toHaveBeenCalled();
91
+ });
92
+
93
+ describe("Modal.Header", () => {
94
+ it("should have an aria-label on the close button", () => {
95
+ render(
96
+ <Modal
97
+ isOpen={true}
98
+ label="Label"
99
+ bg={COLOR_PURPLE_300}
100
+ onClose={() => {}}
101
+ closeButtonLabel="Close this dialog"
102
+ >
103
+ <Modal.Header>
104
+ <Modal.CloseButton />
105
+ </Modal.Header>
106
+ ajdsfljasdlfjlasdjf
107
+ </Modal>
108
+ );
109
+
110
+ expect(screen.getByLabelText("Close this dialog")).toBeInTheDocument();
111
+ });
112
+
113
+ it("should accept the onClose handler from ModalContext", () => {
114
+ const onClose = jest.fn();
115
+ render(
116
+ <Modal
117
+ isOpen={true}
118
+ label="Label"
119
+ bg={COLOR_PURPLE_300}
120
+ onClose={onClose}
121
+ closeButtonLabel="Close this dialog"
122
+ >
123
+ <Modal.Header>
124
+ <Modal.CloseButton />
125
+ </Modal.Header>
126
+ ajdsfljasdlfjlasdjf
127
+ </Modal>
128
+ );
129
+
130
+ fireEvent.click(screen.getByLabelText("Close this dialog"));
131
+ expect(onClose).toHaveBeenCalled();
132
+ });
133
+ });
134
+ });
@@ -0,0 +1,209 @@
1
+ import * as React from "react";
2
+ import { Box } from "@sproutsocial/seeds-react-box";
3
+ import { Button } from "@sproutsocial/seeds-react-button";
4
+ import { Input } from "@sproutsocial/seeds-react-input";
5
+ import { Text } from "@sproutsocial/seeds-react-text";
6
+ import Modal from "../Modal";
7
+ import FormField from "@sproutsocial/seeds-react-form-field";
8
+
9
+ const longContent = `
10
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
11
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem
12
+ ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
13
+ tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum
14
+ dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
15
+ incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit
16
+ amet, consectetur adipiscing elit, sed do eiusmod tempor
17
+ incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit
18
+ amet, consectetur adipiscing elit, sed do eiusmod tempor
19
+ incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit
20
+ amet, consectetur adipiscing elit, sed do eiusmod tempor
21
+ incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit
22
+ amet, consectetur adipiscing elit, sed do eiusmod tempor
23
+ incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit
24
+ amet, consectetur adipiscing elit, sed do eiusmod tempor
25
+ incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit
26
+ amet, consectetur adipiscing elit, sed do eiusmod tempor
27
+ incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit
28
+ amet, consectetur adipiscing elit, sed do eiusmod tempor
29
+ incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit
30
+ amet, consectetur adipiscing elit, sed do eiusmod tempor
31
+ incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit
32
+ amet, consectetur adipiscing elit, sed do eiusmod tempor
33
+ incididunt ut labore et dolore magna aliqua.`;
34
+
35
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
36
+ function ModalTypeTest() {
37
+ return (
38
+ <>
39
+ <Modal
40
+ appElementSelector="#root"
41
+ isOpen={true}
42
+ onClose={jest.fn()}
43
+ closeButtonLabel="Close this dialog"
44
+ label="Example Modal"
45
+ >
46
+ <React.Fragment>
47
+ <Modal.Header
48
+ title="Assign Chatbot"
49
+ subtitle="The chatbot will respond to customers from this profile."
50
+ />
51
+ <Modal.Content>{longContent}</Modal.Content>
52
+ <Modal.Footer>
53
+ <Button appearance="primary" width={1}>
54
+ Full-Width Button
55
+ </Button>
56
+ </Modal.Footer>
57
+ </React.Fragment>
58
+ </Modal>
59
+ <Modal
60
+ appElementSelector="#root"
61
+ isOpen={false}
62
+ closeButtonLabel="n/a"
63
+ label="Example Modal"
64
+ >
65
+ <React.Fragment>
66
+ <Modal.Content>{longContent}</Modal.Content>
67
+ <Modal.Footer>
68
+ <Button appearance="primary" width={1} onClick={jest.fn()}>
69
+ Must click to close
70
+ </Button>
71
+ </Modal.Footer>
72
+ </React.Fragment>
73
+ </Modal>
74
+ <Modal
75
+ appElementSelector="#root"
76
+ isOpen={true}
77
+ onClose={jest.fn()}
78
+ closeButtonLabel="Close this dialog"
79
+ label="Example Modal"
80
+ >
81
+ <React.Fragment>
82
+ <Modal.Header title="" subtitle="" bordered>
83
+ <Box width="100%" bg="purple.400">
84
+ Custom header
85
+ </Box>
86
+ </Modal.Header>
87
+ <Modal.Content>{longContent}</Modal.Content>
88
+ <Modal.Footer>
89
+ <Button appearance="primary" width={1}>
90
+ Full-Width Button
91
+ </Button>
92
+ </Modal.Footer>
93
+ </React.Fragment>
94
+ </Modal>
95
+ <Modal
96
+ appElementSelector="#root"
97
+ isOpen={false}
98
+ onClose={jest.fn()}
99
+ closeButtonLabel="Close this dialog"
100
+ label="Example Modal"
101
+ >
102
+ <React.Fragment>
103
+ <Modal.Header bordered>
104
+ <Box>
105
+ <Text as="h1" fontSize={400} fontWeight="semibold">
106
+ Assign Chatbot
107
+ </Text>
108
+ <Text as="div" fontSize={200}>
109
+ The chatbot will respond to customers from this profile.
110
+ </Text>
111
+ </Box>
112
+ <Box>
113
+ <button>dummy button 1</button>
114
+ <button>dummy button 2</button>
115
+ <Modal.CloseButton />
116
+ </Box>
117
+ </Modal.Header>
118
+ <Modal.Content>{longContent}</Modal.Content>
119
+ </React.Fragment>
120
+ </Modal>
121
+ <Modal
122
+ appElementSelector="#root"
123
+ isOpen={true}
124
+ onClose={jest.fn()}
125
+ closeButtonLabel="Close this dialog"
126
+ label="Example Modal"
127
+ >
128
+ <React.Fragment>
129
+ <Modal.Header />
130
+ <Modal.Content>{longContent}</Modal.Content>
131
+ <Modal.Footer>
132
+ <Button appearance="primary" width={1}>
133
+ Full-Width Button
134
+ </Button>
135
+ </Modal.Footer>
136
+ </React.Fragment>
137
+ </Modal>
138
+ <Modal
139
+ width="500px"
140
+ appElementSelector="#root"
141
+ isOpen={true}
142
+ onClose={jest.fn()}
143
+ closeButtonLabel="Close this dialog"
144
+ label="Example Modal"
145
+ >
146
+ <React.Fragment>
147
+ <Modal.Header
148
+ title="Create Share Link"
149
+ subtitle="Anyone with this link will be able to view its contents."
150
+ />
151
+ <Modal.Content>
152
+ <FormField
153
+ label="Label"
154
+ helperText="This is some helpful helper text"
155
+ >
156
+ {
157
+ // is there a reason that the props are not passed to the input?
158
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
159
+ (props) => (
160
+ <Input
161
+ placeholder="Type the things..."
162
+ name="title"
163
+ id="title"
164
+ />
165
+ )
166
+ }
167
+ </FormField>
168
+ </Modal.Content>
169
+ <Modal.Footer>
170
+ <Box display="flex" justifyContent="flex-end">
171
+ <Button appearance="primary">Create Link</Button>
172
+ </Box>
173
+ </Modal.Footer>
174
+ </React.Fragment>
175
+ </Modal>
176
+ <Modal
177
+ bg="container.background.decorative.purple"
178
+ width="500px"
179
+ appElementSelector="#root"
180
+ isOpen={true}
181
+ onClose={jest.fn()}
182
+ closeButtonLabel="Close this dialog"
183
+ label="Example Modal"
184
+ >
185
+ <React.Fragment>
186
+ <Modal.Header
187
+ title="Create Share Link"
188
+ subtitle="Anyone with this link will be able to view its contents."
189
+ />
190
+ <Modal.Content>
191
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
192
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem
193
+ ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
194
+ tempor incididunt ut labore et dolore magna aliqua.
195
+ </Modal.Content>
196
+ <Modal.Footer>
197
+ <Box display="flex" justifyContent="flex-end">
198
+ <Button appearance="primary">Create Link</Button>
199
+ </Box>
200
+ </Modal.Footer>
201
+ </React.Fragment>
202
+ </Modal>
203
+ {/* @ts-expect-error - test that missing required props is rejected */}
204
+ <Modal />
205
+ </>
206
+ );
207
+ }
208
+
209
+ export default ModalTypeTest;
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import Modal from "./Modal";
2
+
3
+ export default Modal;
4
+ export { Modal };
5
+ export * from "./ModalTypes";
@@ -0,0 +1,7 @@
1
+ import "styled-components";
2
+ import { TypeTheme } from "@sproutsocial/seeds-react-theme";
3
+
4
+ declare module "styled-components" {
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
6
+ export interface DefaultTheme extends TypeTheme {}
7
+ }
package/src/styles.tsx ADDED
@@ -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";
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@sproutsocial/seeds-tsconfig/bundler/dom/library-monorepo",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "module": "esnext"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist", "coverage"]
9
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig((options) => ({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ clean: true,
7
+ legacyOutput: true,
8
+ dts: options.dts,
9
+ external: ["react"],
10
+ sourcemap: true,
11
+ metafile: options.metafile,
12
+ }));