@simplybusiness/mobius 7.0.0 → 7.1.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/CHANGELOG.md +10 -0
- package/dist/cjs/index.js +178 -35
- package/dist/cjs/index.js.map +4 -4
- package/dist/cjs/meta.json +141 -6
- package/dist/esm/index.js +176 -27
- package/dist/esm/index.js.map +4 -4
- package/dist/esm/meta.json +142 -5
- package/dist/esm/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/components/Toast/Toast.d.ts +13 -0
- package/dist/types/src/components/Toast/ToastOptionsDoc.d.ts +9 -0
- package/dist/types/src/components/Toast/Toaster.d.ts +19 -0
- package/dist/types/src/components/Toast/index.d.ts +4 -0
- package/dist/types/src/components/Toast/state.d.ts +3 -0
- package/dist/types/src/components/Toast/types.d.ts +27 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/package.json +3 -2
- package/src/components/Combobox/Combobox.tsx +1 -0
- package/src/components/Toast/Toast.css +233 -0
- package/src/components/Toast/Toast.mdx +131 -0
- package/src/components/Toast/Toast.stories.tsx +285 -0
- package/src/components/Toast/Toast.test.tsx +188 -0
- package/src/components/Toast/Toast.tsx +154 -0
- package/src/components/Toast/ToastOptionsDoc.tsx +8 -0
- package/src/components/Toast/Toaster.tsx +46 -0
- package/src/components/Toast/index.tsx +4 -0
- package/src/components/Toast/state.ts +6 -0
- package/src/components/Toast/types.ts +36 -0
- package/src/components/index.tsx +1 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-webpack5";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { Button } from "../Button";
|
|
4
|
+
import { Flex } from "../Flex";
|
|
5
|
+
import { Stack } from "../Stack";
|
|
6
|
+
import { Text } from "../Text";
|
|
7
|
+
import { toast } from "./Toast";
|
|
8
|
+
import { Toaster } from "./Toaster";
|
|
9
|
+
import type { ToasterProps } from "./Toaster";
|
|
10
|
+
import { excludeControls } from "../../utils";
|
|
11
|
+
|
|
12
|
+
type StoryType = StoryObj<typeof Toaster>;
|
|
13
|
+
|
|
14
|
+
const meta: Meta<typeof Toaster> = {
|
|
15
|
+
title: "Components/Toast",
|
|
16
|
+
component: Toaster,
|
|
17
|
+
argTypes: {
|
|
18
|
+
position: {
|
|
19
|
+
control: { type: "select" },
|
|
20
|
+
options: [
|
|
21
|
+
"top-left",
|
|
22
|
+
"top-center",
|
|
23
|
+
"top-right",
|
|
24
|
+
"bottom-left",
|
|
25
|
+
"bottom-center",
|
|
26
|
+
"bottom-right",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
...excludeControls("toastOptions", "theme", "icons", "dir", "hotkey"),
|
|
30
|
+
},
|
|
31
|
+
parameters: {
|
|
32
|
+
docs: {
|
|
33
|
+
description: {
|
|
34
|
+
component:
|
|
35
|
+
"Toast notifications for displaying brief, non-blocking messages. Built on Sonner.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default meta;
|
|
42
|
+
|
|
43
|
+
export const Default: StoryType = {
|
|
44
|
+
render: (args: ToasterProps) => (
|
|
45
|
+
<>
|
|
46
|
+
<Toaster {...args} />
|
|
47
|
+
<Button onClick={() => toast.info("This is an info toast")}>
|
|
48
|
+
Show Toast
|
|
49
|
+
</Button>
|
|
50
|
+
</>
|
|
51
|
+
),
|
|
52
|
+
args: {
|
|
53
|
+
position: "top-right",
|
|
54
|
+
closeButton: true,
|
|
55
|
+
expand: false,
|
|
56
|
+
duration: 4000,
|
|
57
|
+
visibleToasts: 3,
|
|
58
|
+
},
|
|
59
|
+
play: () => {
|
|
60
|
+
toast.info("This is an info toast");
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Info: StoryType = {
|
|
65
|
+
render: (args: ToasterProps) => (
|
|
66
|
+
<>
|
|
67
|
+
<Toaster {...args} />
|
|
68
|
+
<Button
|
|
69
|
+
onClick={() => toast.info("Your session will expire in 5 minutes")}
|
|
70
|
+
>
|
|
71
|
+
Show Info Toast
|
|
72
|
+
</Button>
|
|
73
|
+
</>
|
|
74
|
+
),
|
|
75
|
+
args: {
|
|
76
|
+
position: "top-right",
|
|
77
|
+
},
|
|
78
|
+
play: () => {
|
|
79
|
+
toast.info("Your session will expire in 5 minutes");
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const Success: StoryType = {
|
|
84
|
+
render: (args: ToasterProps) => (
|
|
85
|
+
<>
|
|
86
|
+
<Toaster {...args} />
|
|
87
|
+
<Button onClick={() => toast.success("Your changes have been saved")}>
|
|
88
|
+
Show Success Toast
|
|
89
|
+
</Button>
|
|
90
|
+
</>
|
|
91
|
+
),
|
|
92
|
+
args: {
|
|
93
|
+
position: "top-right",
|
|
94
|
+
},
|
|
95
|
+
play: () => {
|
|
96
|
+
toast.success("Your changes have been saved");
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const Warning: StoryType = {
|
|
101
|
+
render: (args: ToasterProps) => (
|
|
102
|
+
<>
|
|
103
|
+
<Toaster {...args} />
|
|
104
|
+
<Button
|
|
105
|
+
onClick={() =>
|
|
106
|
+
toast.warning("Please review your input before continuing")
|
|
107
|
+
}
|
|
108
|
+
>
|
|
109
|
+
Show Warning Toast
|
|
110
|
+
</Button>
|
|
111
|
+
</>
|
|
112
|
+
),
|
|
113
|
+
args: {
|
|
114
|
+
position: "top-right",
|
|
115
|
+
},
|
|
116
|
+
play: () => {
|
|
117
|
+
toast.warning("Please review your input before continuing");
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const ErrorVariant: StoryType = {
|
|
122
|
+
name: "Error",
|
|
123
|
+
render: (args: ToasterProps) => (
|
|
124
|
+
<>
|
|
125
|
+
<Toaster {...args} />
|
|
126
|
+
<Button
|
|
127
|
+
onClick={() => toast.error("Something went wrong. Please try again.")}
|
|
128
|
+
>
|
|
129
|
+
Show Error Toast
|
|
130
|
+
</Button>
|
|
131
|
+
</>
|
|
132
|
+
),
|
|
133
|
+
args: {
|
|
134
|
+
position: "top-right",
|
|
135
|
+
},
|
|
136
|
+
play: () => {
|
|
137
|
+
toast.error("Something went wrong. Please try again.");
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const WithTitle: StoryType = {
|
|
142
|
+
render: (args: ToasterProps) => (
|
|
143
|
+
<>
|
|
144
|
+
<Toaster {...args} />
|
|
145
|
+
<Button
|
|
146
|
+
onClick={() =>
|
|
147
|
+
toast.success("Your quote has been saved and sent to your email.", {
|
|
148
|
+
title: "Quote saved",
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
>
|
|
152
|
+
Show Toast with Title
|
|
153
|
+
</Button>
|
|
154
|
+
</>
|
|
155
|
+
),
|
|
156
|
+
args: {
|
|
157
|
+
position: "top-right",
|
|
158
|
+
},
|
|
159
|
+
play: () => {
|
|
160
|
+
toast.success("Your quote has been saved and sent to your email.", {
|
|
161
|
+
title: "Quote saved",
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const WithActions: StoryType = {
|
|
167
|
+
render: (args: ToasterProps) => (
|
|
168
|
+
<>
|
|
169
|
+
<Toaster {...args} />
|
|
170
|
+
<Button
|
|
171
|
+
onClick={() =>
|
|
172
|
+
toast.info("Would you like to save your progress?", {
|
|
173
|
+
title: "Save progress",
|
|
174
|
+
action: {
|
|
175
|
+
label: "Save",
|
|
176
|
+
onClick: () => console.log("Saved!"),
|
|
177
|
+
},
|
|
178
|
+
cancel: {
|
|
179
|
+
label: "Discard",
|
|
180
|
+
onClick: () => console.log("Discarded!"),
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
>
|
|
185
|
+
Show Toast with Actions
|
|
186
|
+
</Button>
|
|
187
|
+
</>
|
|
188
|
+
),
|
|
189
|
+
args: {
|
|
190
|
+
position: "top-right",
|
|
191
|
+
},
|
|
192
|
+
play: () => {
|
|
193
|
+
toast.info("Would you like to save your progress?", {
|
|
194
|
+
title: "Save progress",
|
|
195
|
+
action: {
|
|
196
|
+
label: "Save",
|
|
197
|
+
onClick: () => console.log("Saved!"),
|
|
198
|
+
},
|
|
199
|
+
cancel: {
|
|
200
|
+
label: "Discard",
|
|
201
|
+
onClick: () => console.log("Discarded!"),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const AllVariants: StoryType = {
|
|
208
|
+
render: (args: ToasterProps) => (
|
|
209
|
+
<>
|
|
210
|
+
<Toaster {...args} />
|
|
211
|
+
<Stack gap="sm">
|
|
212
|
+
<Button onClick={() => toast.info("This is an info message")}>
|
|
213
|
+
Info
|
|
214
|
+
</Button>
|
|
215
|
+
<Button onClick={() => toast.success("This is a success message")}>
|
|
216
|
+
Success
|
|
217
|
+
</Button>
|
|
218
|
+
<Button onClick={() => toast.warning("This is a warning message")}>
|
|
219
|
+
Warning
|
|
220
|
+
</Button>
|
|
221
|
+
<Button onClick={() => toast.error("This is an error message")}>
|
|
222
|
+
Error
|
|
223
|
+
</Button>
|
|
224
|
+
</Stack>
|
|
225
|
+
</>
|
|
226
|
+
),
|
|
227
|
+
args: {
|
|
228
|
+
position: "top-right",
|
|
229
|
+
},
|
|
230
|
+
play: () => {
|
|
231
|
+
toast.info("This is an info message");
|
|
232
|
+
toast.success("This is a success message");
|
|
233
|
+
toast.warning("This is a warning message");
|
|
234
|
+
toast.error("This is an error message");
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const InteractiveDemoComponent = () => {
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
const handler = (e: KeyboardEvent) => {
|
|
241
|
+
if (e.key === "r" || e.key === "R") {
|
|
242
|
+
const random = Math.floor(Math.random() * 4);
|
|
243
|
+
switch (random) {
|
|
244
|
+
case 0:
|
|
245
|
+
toast.info("This is an info toast");
|
|
246
|
+
break;
|
|
247
|
+
case 1:
|
|
248
|
+
toast.success("Operation completed successfully");
|
|
249
|
+
break;
|
|
250
|
+
case 2:
|
|
251
|
+
toast.warning("Please check your input");
|
|
252
|
+
break;
|
|
253
|
+
default:
|
|
254
|
+
toast.error("Something went wrong");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
window.addEventListener("keydown", handler);
|
|
259
|
+
return () => window.removeEventListener("keydown", handler);
|
|
260
|
+
}, []);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<Stack gap="md">
|
|
264
|
+
<Text>Press 'R' to show a random toast</Text>
|
|
265
|
+
<Flex gap="sm">
|
|
266
|
+
<Button onClick={() => toast.info("Info toast")}>Info</Button>
|
|
267
|
+
<Button onClick={() => toast.success("Success toast")}>Success</Button>
|
|
268
|
+
<Button onClick={() => toast.warning("Warning toast")}>Warning</Button>
|
|
269
|
+
<Button onClick={() => toast.error("Error toast")}>Error</Button>
|
|
270
|
+
</Flex>
|
|
271
|
+
</Stack>
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export const InteractiveDemo: StoryType = {
|
|
276
|
+
render: (args: ToasterProps) => (
|
|
277
|
+
<>
|
|
278
|
+
<Toaster {...args} />
|
|
279
|
+
<InteractiveDemoComponent />
|
|
280
|
+
</>
|
|
281
|
+
),
|
|
282
|
+
args: {
|
|
283
|
+
position: "top-right",
|
|
284
|
+
},
|
|
285
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { Toaster, toast } from ".";
|
|
4
|
+
|
|
5
|
+
// Mock setPointerCapture which JSDOM doesn't support (used by Sonner)
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
Element.prototype.setPointerCapture = jest.fn();
|
|
8
|
+
Element.prototype.releasePointerCapture = jest.fn();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("Toast", () => {
|
|
12
|
+
describe("Toaster", () => {
|
|
13
|
+
it("renders without errors", () => {
|
|
14
|
+
render(<Toaster />);
|
|
15
|
+
// Sonner renders a section element with aria-label
|
|
16
|
+
expect(screen.getByRole("region")).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("toast functions", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
toast.dismiss();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("shows info toast with correct content", async () => {
|
|
26
|
+
render(<Toaster />);
|
|
27
|
+
|
|
28
|
+
toast.info("Info message");
|
|
29
|
+
|
|
30
|
+
await waitFor(() => {
|
|
31
|
+
expect(screen.getByText("Info message")).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("shows success toast with correct content", async () => {
|
|
36
|
+
render(<Toaster />);
|
|
37
|
+
|
|
38
|
+
toast.success("Success message");
|
|
39
|
+
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(screen.getByText("Success message")).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("shows warning toast with correct content", async () => {
|
|
46
|
+
render(<Toaster />);
|
|
47
|
+
|
|
48
|
+
toast.warning("Warning message");
|
|
49
|
+
|
|
50
|
+
await waitFor(() => {
|
|
51
|
+
expect(screen.getByText("Warning message")).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("shows error toast with correct content", async () => {
|
|
56
|
+
render(<Toaster />);
|
|
57
|
+
|
|
58
|
+
toast.error("Error message");
|
|
59
|
+
|
|
60
|
+
await waitFor(() => {
|
|
61
|
+
expect(screen.getByText("Error message")).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("shows toast with title", async () => {
|
|
66
|
+
render(<Toaster />);
|
|
67
|
+
|
|
68
|
+
toast.info("Description text", { title: "Title text" });
|
|
69
|
+
|
|
70
|
+
await waitFor(() => {
|
|
71
|
+
expect(screen.getByText("Title text")).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("shows toast with description when title is provided", async () => {
|
|
76
|
+
render(<Toaster />);
|
|
77
|
+
|
|
78
|
+
toast.info("Description text", { title: "Title text" });
|
|
79
|
+
|
|
80
|
+
await waitFor(() => {
|
|
81
|
+
expect(screen.getByText("Description text")).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("renders toast with action button", async () => {
|
|
86
|
+
render(<Toaster />);
|
|
87
|
+
|
|
88
|
+
toast.info("Action toast", {
|
|
89
|
+
action: { label: "Undo", onClick: jest.fn() },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
expect(screen.getByText("Undo")).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("shows toast with cancel button", async () => {
|
|
98
|
+
render(<Toaster />);
|
|
99
|
+
|
|
100
|
+
toast.info("Cancel toast", {
|
|
101
|
+
cancel: { label: "Cancel" },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await waitFor(() => {
|
|
105
|
+
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("calls action onClick and dismisses toast when action button is clicked", async () => {
|
|
110
|
+
const user = userEvent.setup();
|
|
111
|
+
const onClickMock = jest.fn();
|
|
112
|
+
render(<Toaster />);
|
|
113
|
+
|
|
114
|
+
toast.info("Action toast", {
|
|
115
|
+
action: { label: "Confirm", onClick: onClickMock },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(screen.getByText("Confirm")).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await user.click(screen.getByText("Confirm"));
|
|
123
|
+
|
|
124
|
+
expect(onClickMock).toHaveBeenCalledTimes(1);
|
|
125
|
+
|
|
126
|
+
await waitFor(() => {
|
|
127
|
+
expect(screen.queryByText("Action toast")).not.toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("calls cancel onClick and dismisses toast when cancel button is clicked", async () => {
|
|
132
|
+
const user = userEvent.setup();
|
|
133
|
+
const onClickMock = jest.fn();
|
|
134
|
+
render(<Toaster />);
|
|
135
|
+
|
|
136
|
+
toast.info("Cancel toast", {
|
|
137
|
+
cancel: { label: "Dismiss", onClick: onClickMock },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(screen.getByText("Dismiss")).toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await user.click(screen.getByText("Dismiss"));
|
|
145
|
+
|
|
146
|
+
expect(onClickMock).toHaveBeenCalledTimes(1);
|
|
147
|
+
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(screen.queryByText("Cancel toast")).not.toBeInTheDocument();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("dismisses toast when dismiss is called", async () => {
|
|
154
|
+
render(<Toaster />);
|
|
155
|
+
|
|
156
|
+
toast.info("Dismissable toast");
|
|
157
|
+
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(screen.getByText("Dismissable toast")).toBeInTheDocument();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
toast.dismiss();
|
|
163
|
+
|
|
164
|
+
await waitFor(() => {
|
|
165
|
+
expect(screen.queryByText("Dismissable toast")).not.toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("calls onDismiss callback when toast is dismissed via close button", async () => {
|
|
170
|
+
const user = userEvent.setup();
|
|
171
|
+
const onDismissMock = jest.fn();
|
|
172
|
+
render(<Toaster closeButton />);
|
|
173
|
+
|
|
174
|
+
toast.info("Dismissable toast", { onDismiss: onDismissMock });
|
|
175
|
+
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(screen.getByText("Dismissable toast")).toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const closeButton = screen.getByRole("button", { name: "Close" });
|
|
181
|
+
await user.click(closeButton);
|
|
182
|
+
|
|
183
|
+
await waitFor(() => {
|
|
184
|
+
expect(onDismissMock).toHaveBeenCalledTimes(1);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
circleCheck,
|
|
3
|
+
circleExclamation,
|
|
4
|
+
circleInfo,
|
|
5
|
+
cross,
|
|
6
|
+
triangleExclamation,
|
|
7
|
+
} from "@simplybusiness/icons";
|
|
8
|
+
import classNames from "classnames/dedupe";
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
|
+
import { toast as sonnerToast } from "sonner";
|
|
11
|
+
import { Icon } from "../Icon";
|
|
12
|
+
import { toastState } from "./state";
|
|
13
|
+
import type { ToastOptions, ToastVariant } from "./types";
|
|
14
|
+
|
|
15
|
+
const variantIcons: Record<ToastVariant, typeof circleInfo> = {
|
|
16
|
+
info: circleInfo,
|
|
17
|
+
success: circleCheck,
|
|
18
|
+
warning: triangleExclamation,
|
|
19
|
+
error: circleExclamation,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const variantColors: Record<ToastVariant, string> = {
|
|
23
|
+
info: "var(--color-info)",
|
|
24
|
+
success: "var(--color-valid)",
|
|
25
|
+
warning: "var(--color-warning)",
|
|
26
|
+
error: "var(--color-error)",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ToastIcon = ({ variant }: { variant: ToastVariant }) => (
|
|
30
|
+
<span className="mobius-toast__icon">
|
|
31
|
+
<Icon icon={variantIcons[variant]} color={variantColors[variant]} />
|
|
32
|
+
</span>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const CloseIcon = () => (
|
|
36
|
+
<span className="mobius-toast__close-icon">
|
|
37
|
+
<Icon icon={cross} />
|
|
38
|
+
</span>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
type ToastContentProps = {
|
|
42
|
+
toastId: string | number;
|
|
43
|
+
variant: ToastVariant;
|
|
44
|
+
title?: string;
|
|
45
|
+
description?: ReactNode;
|
|
46
|
+
action?: ToastOptions["action"];
|
|
47
|
+
cancel?: ToastOptions["cancel"];
|
|
48
|
+
showCloseButton?: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const ToastContent = ({
|
|
52
|
+
toastId,
|
|
53
|
+
variant,
|
|
54
|
+
title,
|
|
55
|
+
description,
|
|
56
|
+
action,
|
|
57
|
+
cancel,
|
|
58
|
+
showCloseButton = toastState.showCloseButton,
|
|
59
|
+
}: ToastContentProps) => (
|
|
60
|
+
<div className={classNames("mobius", "mobius-toast", `--${variant}`)}>
|
|
61
|
+
<ToastIcon variant={variant} />
|
|
62
|
+
<div className="mobius-toast__body">
|
|
63
|
+
<div className="mobius-toast__content">
|
|
64
|
+
{title && <div className="mobius-toast__title">{title}</div>}
|
|
65
|
+
{description && (
|
|
66
|
+
<div className="mobius-toast__description">{description}</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
{(action || cancel) && (
|
|
70
|
+
<div className="mobius-toast__actions">
|
|
71
|
+
{cancel && (
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
className="mobius-toast__cancel"
|
|
75
|
+
onClick={() => {
|
|
76
|
+
cancel.onClick?.();
|
|
77
|
+
sonnerToast.dismiss(toastId);
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{cancel.label}
|
|
81
|
+
</button>
|
|
82
|
+
)}
|
|
83
|
+
{action && (
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
className="mobius-toast__action"
|
|
87
|
+
onClick={() => {
|
|
88
|
+
action.onClick();
|
|
89
|
+
sonnerToast.dismiss(toastId);
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{action.label}
|
|
93
|
+
</button>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
{showCloseButton && (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
className="mobius-toast__close"
|
|
102
|
+
onClick={() => sonnerToast.dismiss(toastId)}
|
|
103
|
+
aria-label="Close"
|
|
104
|
+
>
|
|
105
|
+
<CloseIcon />
|
|
106
|
+
</button>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const createCustomToast = (
|
|
112
|
+
message: string,
|
|
113
|
+
variant: ToastVariant,
|
|
114
|
+
options?: ToastOptions,
|
|
115
|
+
) =>
|
|
116
|
+
sonnerToast.custom(
|
|
117
|
+
id => (
|
|
118
|
+
<ToastContent
|
|
119
|
+
toastId={id}
|
|
120
|
+
variant={variant}
|
|
121
|
+
title={options?.title}
|
|
122
|
+
description={options?.description ?? message}
|
|
123
|
+
action={options?.action}
|
|
124
|
+
cancel={options?.cancel}
|
|
125
|
+
showCloseButton={options?.showCloseButton}
|
|
126
|
+
/>
|
|
127
|
+
),
|
|
128
|
+
{
|
|
129
|
+
duration: options?.duration,
|
|
130
|
+
onDismiss: options?.onDismiss,
|
|
131
|
+
onAutoClose: options?.onAutoClose,
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
export const toast = {
|
|
136
|
+
/** Show an info toast */
|
|
137
|
+
info: (message: string, options?: ToastOptions) =>
|
|
138
|
+
createCustomToast(message, "info", options),
|
|
139
|
+
|
|
140
|
+
/** Show a success toast */
|
|
141
|
+
success: (message: string, options?: ToastOptions) =>
|
|
142
|
+
createCustomToast(message, "success", options),
|
|
143
|
+
|
|
144
|
+
/** Show a warning toast */
|
|
145
|
+
warning: (message: string, options?: ToastOptions) =>
|
|
146
|
+
createCustomToast(message, "warning", options),
|
|
147
|
+
|
|
148
|
+
/** Show an error toast */
|
|
149
|
+
error: (message: string, options?: ToastOptions) =>
|
|
150
|
+
createCustomToast(message, "error", options),
|
|
151
|
+
|
|
152
|
+
/** Dismiss a specific toast by ID or all toasts */
|
|
153
|
+
dismiss: (toastId?: string | number) => sonnerToast.dismiss(toastId),
|
|
154
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToastOptions } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Documentation-only component for ToastOptions.
|
|
5
|
+
* Exists solely to provide ArgTypes for the toast() function options.
|
|
6
|
+
*/
|
|
7
|
+
export const ToastOptionsDoc = (_props: ToastOptions) => null;
|
|
8
|
+
ToastOptionsDoc.displayName = "ToastOptionsDoc";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { Toaster as SonnerToaster } from "sonner";
|
|
3
|
+
import { toastState } from "./state";
|
|
4
|
+
import type { ToastPosition } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface ToasterProps {
|
|
7
|
+
/** Position of the toast container */
|
|
8
|
+
position?: ToastPosition;
|
|
9
|
+
/** Whether to show the close (X) button on toasts */
|
|
10
|
+
closeButton?: boolean;
|
|
11
|
+
/** Whether toasts expand on hover */
|
|
12
|
+
expand?: boolean;
|
|
13
|
+
/** Duration in milliseconds before auto-dismiss (default: 4000) */
|
|
14
|
+
duration?: number;
|
|
15
|
+
/** Maximum number of visible toasts */
|
|
16
|
+
visibleToasts?: number;
|
|
17
|
+
/** Gap between toasts in pixels */
|
|
18
|
+
gap?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Toaster = ({
|
|
22
|
+
position = "top-right",
|
|
23
|
+
closeButton = true,
|
|
24
|
+
expand = false,
|
|
25
|
+
duration = 4000,
|
|
26
|
+
visibleToasts = 3,
|
|
27
|
+
gap = 8,
|
|
28
|
+
}: ToasterProps) => {
|
|
29
|
+
// Sync shared state with Toaster's closeButton prop
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
toastState.showCloseButton = closeButton;
|
|
32
|
+
}, [closeButton]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<SonnerToaster
|
|
36
|
+
position={position}
|
|
37
|
+
closeButton={false}
|
|
38
|
+
expand={expand}
|
|
39
|
+
duration={duration}
|
|
40
|
+
visibleToasts={visibleToasts}
|
|
41
|
+
gap={gap}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
Toaster.displayName = "Toaster";
|