@sproutsocial/seeds-react-toast 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.
- package/.eslintignore +6 -0
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +12 -0
- package/dist/esm/index.js +247 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +288 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +9 -0
- package/package.json +45 -0
- package/src/Toast.stories.tsx +300 -0
- package/src/Toast.tsx +173 -0
- package/src/ToastTypes.ts +38 -0
- package/src/index.ts +3 -0
- package/src/styled.d.ts +7 -0
- package/src/styles.ts +115 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +12 -0
package/src/Toast.tsx
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { PropsWithChildren, ReactNode, ComponentProps } from "react";
|
|
2
|
+
import {
|
|
3
|
+
toast as toastifyToast,
|
|
4
|
+
cssTransition,
|
|
5
|
+
type ToastContent,
|
|
6
|
+
} from "react-toastify";
|
|
7
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
8
|
+
import Icon, { type TypeIconName } from "@sproutsocial/seeds-react-icon";
|
|
9
|
+
import Text from "@sproutsocial/seeds-react-text";
|
|
10
|
+
import { Container, ToastRoot } from "./styles";
|
|
11
|
+
import type { TypeToastOptions, TypeToastTheme } from "./ToastTypes";
|
|
12
|
+
import styled from "styled-components";
|
|
13
|
+
|
|
14
|
+
export const toastDismiss: typeof toastifyToast.dismiss = (...input) =>
|
|
15
|
+
// @ts-ignore Not sure what this type is supposed to be
|
|
16
|
+
toastifyToast.dismiss(...input);
|
|
17
|
+
export const toastIsActive: typeof toastifyToast.isActive = (...input) =>
|
|
18
|
+
toastifyToast.isActive(...input);
|
|
19
|
+
export const toastUpdate: typeof toastifyToast.update = (...input) =>
|
|
20
|
+
toastifyToast.update(...input);
|
|
21
|
+
|
|
22
|
+
const NoTransition = cssTransition({
|
|
23
|
+
enter: "SproutToast__none-in",
|
|
24
|
+
exit: "SproutToast__none-out",
|
|
25
|
+
});
|
|
26
|
+
const SproutZoomTransition = cssTransition({
|
|
27
|
+
enter: "SproutToast__zoom-in",
|
|
28
|
+
exit: "SproutToast__zoom-out",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const ToastContainer = (
|
|
32
|
+
props: Partial<ComponentProps<typeof ToastRoot>>
|
|
33
|
+
) => <ToastRoot {...props} />;
|
|
34
|
+
|
|
35
|
+
const themeIcon: Record<TypeToastTheme, TypeIconName> = {
|
|
36
|
+
success: "circle-check-outline",
|
|
37
|
+
info: "circle-i-outline",
|
|
38
|
+
warning: "triangle-exclamation-outline",
|
|
39
|
+
error: "triangle-exclamation-outline",
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
export function toast<TData = unknown>(
|
|
43
|
+
options: TypeToastOptions<TData>
|
|
44
|
+
): ReturnType<typeof toastifyToast> {
|
|
45
|
+
const {
|
|
46
|
+
closeOnClick = true,
|
|
47
|
+
content,
|
|
48
|
+
onClose,
|
|
49
|
+
persist,
|
|
50
|
+
toastId: inputToastId,
|
|
51
|
+
useTransition = true,
|
|
52
|
+
position = "bottom-right",
|
|
53
|
+
autoClose = persist ? false : 6000,
|
|
54
|
+
transition = useTransition ? SproutZoomTransition : NoTransition,
|
|
55
|
+
...rest
|
|
56
|
+
} = options;
|
|
57
|
+
|
|
58
|
+
let toastId = inputToastId;
|
|
59
|
+
if (!toastId && typeof content === "string") {
|
|
60
|
+
toastId = content;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const renderToast: ToastContent<TData> = (toastInput) => {
|
|
64
|
+
const renderedContent =
|
|
65
|
+
typeof content === "function" ? content(toastInput) : content;
|
|
66
|
+
|
|
67
|
+
if (options.theme === "custom") {
|
|
68
|
+
return renderedContent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const theme = options.theme || "info";
|
|
72
|
+
const iconName = options.icon || themeIcon[theme];
|
|
73
|
+
const containerColor =
|
|
74
|
+
options.color ||
|
|
75
|
+
(
|
|
76
|
+
{
|
|
77
|
+
success: "container.border.success",
|
|
78
|
+
error: "container.border.error",
|
|
79
|
+
info: "container.border.info",
|
|
80
|
+
warning: "container.border.warning",
|
|
81
|
+
} as const
|
|
82
|
+
)[theme];
|
|
83
|
+
const iconColor =
|
|
84
|
+
options.color ||
|
|
85
|
+
(
|
|
86
|
+
{
|
|
87
|
+
success: "icon.success",
|
|
88
|
+
error: "icon.error",
|
|
89
|
+
info: "icon.info",
|
|
90
|
+
warning: "icon.warning",
|
|
91
|
+
} as const
|
|
92
|
+
)[theme];
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
// TODO: if this closes when clicked, there should be a label saying "Click to close" that can be overridden
|
|
96
|
+
<ToastContentContainer
|
|
97
|
+
icon={<Icon name={iconName} color={iconColor} />}
|
|
98
|
+
close={<Icon name="x-outline" color="icon.base" aria-hidden />}
|
|
99
|
+
highlightColor={containerColor}
|
|
100
|
+
>
|
|
101
|
+
{renderedContent}
|
|
102
|
+
</ToastContentContainer>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const toastOptions = {
|
|
107
|
+
autoClose,
|
|
108
|
+
closeOnClick,
|
|
109
|
+
onClose,
|
|
110
|
+
toastId: toastId || undefined,
|
|
111
|
+
transition,
|
|
112
|
+
position,
|
|
113
|
+
...rest,
|
|
114
|
+
icon: undefined,
|
|
115
|
+
color: undefined,
|
|
116
|
+
} as const;
|
|
117
|
+
|
|
118
|
+
if (toastId && toastIsActive(toastId)) {
|
|
119
|
+
toastifyToast.update<TData>(toastId, {
|
|
120
|
+
...toastOptions,
|
|
121
|
+
render: renderToast,
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
toastId = toastifyToast<TData>(renderToast, toastOptions);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return toastId;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default ToastContainer;
|
|
131
|
+
|
|
132
|
+
export const ToastContentContainer = ({
|
|
133
|
+
children,
|
|
134
|
+
icon,
|
|
135
|
+
close,
|
|
136
|
+
highlightColor,
|
|
137
|
+
}: PropsWithChildren<{
|
|
138
|
+
/**
|
|
139
|
+
* A ReactNode in the icon slot
|
|
140
|
+
*/
|
|
141
|
+
icon: ReactNode;
|
|
142
|
+
/**
|
|
143
|
+
* A ReactNode in the close button slot
|
|
144
|
+
*/
|
|
145
|
+
close: ReactNode;
|
|
146
|
+
highlightColor?: ComponentProps<typeof Box>["bg"];
|
|
147
|
+
}>) => {
|
|
148
|
+
return (
|
|
149
|
+
<Container data-qa-toast="">
|
|
150
|
+
{highlightColor ? (
|
|
151
|
+
<ToastHighlight bg={highlightColor} aria-hidden />
|
|
152
|
+
) : null}
|
|
153
|
+
|
|
154
|
+
<Box css="line-height: 1;">{icon}</Box>
|
|
155
|
+
|
|
156
|
+
<Box flex={1}>
|
|
157
|
+
<Text as="div" color="text.body" data-qa-toast-content="">
|
|
158
|
+
{children}
|
|
159
|
+
</Text>
|
|
160
|
+
</Box>
|
|
161
|
+
|
|
162
|
+
<Box css="line-height: 1;">{close}</Box>
|
|
163
|
+
</Container>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const ToastHighlight = styled(Box)<ComponentProps<typeof Box>>`
|
|
168
|
+
position: absolute;
|
|
169
|
+
top: 0;
|
|
170
|
+
bottom: 0;
|
|
171
|
+
left: 0;
|
|
172
|
+
width: 2px;
|
|
173
|
+
`;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { type Icon, type TypeIconName } from "@sproutsocial/seeds-react-icon";
|
|
3
|
+
import type { ToastContent, ToastOptions } from "react-toastify";
|
|
4
|
+
|
|
5
|
+
export type TypeToastTheme = "info" | "success" | "warning" | "error";
|
|
6
|
+
|
|
7
|
+
interface BaseToastOptions<TData>
|
|
8
|
+
extends Omit<ToastOptions<TData>, "closeButton" | "icon" | "type"> {
|
|
9
|
+
theme?: TypeToastTheme | "custom";
|
|
10
|
+
content: ToastContent<TData>;
|
|
11
|
+
persist?: boolean;
|
|
12
|
+
useTransition?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ThemedToastOptions<TData> extends BaseToastOptions<TData> {
|
|
16
|
+
/**
|
|
17
|
+
* One of `info`, `success`, `warning`, or `error`.
|
|
18
|
+
*/
|
|
19
|
+
theme?: TypeToastTheme;
|
|
20
|
+
/**
|
|
21
|
+
* @deprecated Use `custom` theme instead.
|
|
22
|
+
*/
|
|
23
|
+
color?: ComponentProps<typeof Icon>["color"];
|
|
24
|
+
/**
|
|
25
|
+
* @deprecated Use `custom` theme instead.
|
|
26
|
+
*/
|
|
27
|
+
icon?: TypeIconName;
|
|
28
|
+
}
|
|
29
|
+
interface CustomToastOptions<TData> extends BaseToastOptions<TData> {
|
|
30
|
+
/**
|
|
31
|
+
* If you need to break out of the supported styles you can use the `custom` theme. You can use `ToastContentContainer` with `Icon` and `ToastHighlight` to build your custom toast.
|
|
32
|
+
*/
|
|
33
|
+
theme: "custom";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type TypeToastOptions<TData> =
|
|
37
|
+
| ThemedToastOptions<TData>
|
|
38
|
+
| CustomToastOptions<TData>;
|
package/src/index.ts
ADDED
package/src/styled.d.ts
ADDED
package/src/styles.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
import "react-toastify/dist/ReactToastify.css";
|
|
6
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
7
|
+
import { ToastContainer } from "react-toastify";
|
|
8
|
+
|
|
9
|
+
export const TOAST_Z_INDEX = 9999;
|
|
10
|
+
|
|
11
|
+
export const Container = styled(Box)<ComponentProps<typeof Box>>`
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: ${({ theme }) => theme.space[350]};
|
|
15
|
+
font-family: ${({ theme }) => theme.fontFamily};
|
|
16
|
+
${({ theme }) => theme.typography[200]}
|
|
17
|
+
position: relative;
|
|
18
|
+
padding: ${({ theme }) => theme.space[350]};
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
export const ToastRoot = styled(ToastContainer).attrs({
|
|
22
|
+
toastClassName: "Toastify-toast-overrides",
|
|
23
|
+
hideProgressBar: true,
|
|
24
|
+
closeButton: false,
|
|
25
|
+
icon: false,
|
|
26
|
+
position: "bottom-right",
|
|
27
|
+
})`
|
|
28
|
+
--toastify-z-index: ${TOAST_Z_INDEX};
|
|
29
|
+
--toastify-toast-offset: ${({ theme }) => theme.space[400]};
|
|
30
|
+
--toastify-toast-width: 360px;
|
|
31
|
+
--toastify-toast-min-height: 48px;
|
|
32
|
+
--toastify-toast-max-height: 70vh;
|
|
33
|
+
--toastify-toast-bd-radius: ${({ theme }) => theme.radii[400]};
|
|
34
|
+
--toastify-toast-background: ${({ theme }) =>
|
|
35
|
+
theme.colors.container.background.base};
|
|
36
|
+
/* there's margin-bottom on the last toast so we can remove this */
|
|
37
|
+
--toastify-toast-bottom: 0;
|
|
38
|
+
|
|
39
|
+
padding: 0;
|
|
40
|
+
|
|
41
|
+
.Toastify-toast-overrides {
|
|
42
|
+
background: ${({ theme }) => theme.colors.container.background.base};
|
|
43
|
+
color: ${({ theme }) => theme.colors.text.body};
|
|
44
|
+
box-shadow: ${({ theme }) => theme.shadows.low};
|
|
45
|
+
padding: 0;
|
|
46
|
+
margin-bottom: var(--toastify-toast-offset);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.Toastify__toast-body {
|
|
50
|
+
padding: 0;
|
|
51
|
+
margin: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Override React Toastify's mobile width styles */
|
|
55
|
+
@media only screen and (max-width: 480px) {
|
|
56
|
+
.Toastify-container-overrides {
|
|
57
|
+
min-width: initial !important;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Zoom animation */
|
|
62
|
+
@keyframes SproutToast__zoom-in {
|
|
63
|
+
from {
|
|
64
|
+
opacity: 0;
|
|
65
|
+
transform: scale3d(0.3, 0.3, 0.3);
|
|
66
|
+
}
|
|
67
|
+
50% {
|
|
68
|
+
opacity: 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
@keyframes SproutToast__zoom-out {
|
|
72
|
+
from {
|
|
73
|
+
opacity: 1;
|
|
74
|
+
}
|
|
75
|
+
50% {
|
|
76
|
+
opacity: 0;
|
|
77
|
+
transform: scale3d(0.3, 0.3, 0.3) translate3d(0, var(--y), 0);
|
|
78
|
+
}
|
|
79
|
+
to {
|
|
80
|
+
opacity: 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
.SproutToast__zoom-in {
|
|
84
|
+
animation: SproutToast__zoom-in ${({ theme }) => theme.duration.medium}
|
|
85
|
+
${({ theme }) => theme.easing.ease_out} both;
|
|
86
|
+
}
|
|
87
|
+
.SproutToast__zoom-out {
|
|
88
|
+
animation: SproutToast__zoom-out ${({ theme }) => theme.duration.slow}
|
|
89
|
+
${({ theme }) => theme.easing.ease_in} both;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* No animation (it's still necessary to define classes for no animation to work properly) */
|
|
93
|
+
@keyframes SproutToast__none-in {
|
|
94
|
+
from {
|
|
95
|
+
opacity: 0;
|
|
96
|
+
}
|
|
97
|
+
to {
|
|
98
|
+
opacity: 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
@keyframes SproutToast__none-out {
|
|
102
|
+
from {
|
|
103
|
+
opacity: 1;
|
|
104
|
+
}
|
|
105
|
+
to {
|
|
106
|
+
opacity: 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
.SproutToast__none-in {
|
|
110
|
+
animation: SproutToast__none-in 0s both;
|
|
111
|
+
}
|
|
112
|
+
.SproutToast__none-out {
|
|
113
|
+
animation: SproutToast__none-out 0s both;
|
|
114
|
+
}
|
|
115
|
+
`;
|
package/tsconfig.json
ADDED
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
|
+
}));
|