diff --git a/.changeset/great-keys-admire.md b/.changeset/great-keys-admire.md
new file mode 100644
index 0000000000..0d7f86fbe3
--- /dev/null
+++ b/.changeset/great-keys-admire.md
@@ -0,0 +1,5 @@
+---
+"@heroui/toast": patch
+---
+
+Support multiple ToastProvider with unique IDs
diff --git a/apps/docs/content/components/toast/index.ts b/apps/docs/content/components/toast/index.ts
index 183dbe6181..0a35a45dc8 100644
--- a/apps/docs/content/components/toast/index.ts
+++ b/apps/docs/content/components/toast/index.ts
@@ -6,6 +6,7 @@ import placement from "./placement";
import usage from "./usage";
import customCloseIcon from "./custom-close-icon";
import close from "./close";
+import multipleToastProvider from "./multiple-ToastProvider";
export const toastContent = {
color,
@@ -16,4 +17,5 @@ export const toastContent = {
placement,
usage,
customCloseIcon,
+ multipleToastProvider,
};
diff --git a/apps/docs/content/components/toast/multiple-ToastProvider.raw.jsx b/apps/docs/content/components/toast/multiple-ToastProvider.raw.jsx
new file mode 100644
index 0000000000..cf94145441
--- /dev/null
+++ b/apps/docs/content/components/toast/multiple-ToastProvider.raw.jsx
@@ -0,0 +1,42 @@
+import {addToast, Button, ToastProvider} from "@heroui/react";
+import React from "react";
+
+export default function App() {
+ const topToasterId = "multi-toaster-top";
+ const bottomToasterId = "multi-toaster-bottom";
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/docs/content/components/toast/multiple-ToastProvider.ts b/apps/docs/content/components/toast/multiple-ToastProvider.ts
new file mode 100644
index 0000000000..e00175cb4f
--- /dev/null
+++ b/apps/docs/content/components/toast/multiple-ToastProvider.ts
@@ -0,0 +1,9 @@
+import App from "./multiple-ToastProvider.raw.jsx?raw";
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/docs/components/toast.mdx b/apps/docs/content/docs/components/toast.mdx
index afe3cd16d5..8f4a695e5d 100644
--- a/apps/docs/content/docs/components/toast.mdx
+++ b/apps/docs/content/docs/components/toast.mdx
@@ -134,6 +134,15 @@ You can pass a custom close icon to the toast by passing the `closeIcon` prop an
files={toastContent.customCloseIcon}
/>
+### Multiple ToastProvider
+
+You can have multiple ToastProvider with different configurations and use `toasterId` to distinguish them. By passing `toasterId` to `addToast` function, you can add a toast to the corresponding `ToastProvider`.
+
+
+
### Global Toast Props
You can pass global toast props to the `ToastProvider` to apply to all toasts.
diff --git a/packages/components/toast/__tests__/toast.test.tsx b/packages/components/toast/__tests__/toast.test.tsx
index c846b2dcab..e16e7f11b9 100644
--- a/packages/components/toast/__tests__/toast.test.tsx
+++ b/packages/components/toast/__tests__/toast.test.tsx
@@ -174,4 +174,75 @@ describe("Toast", () => {
expect(loadingIcon).toBeTruthy();
});
+
+ it("should work with multiple ToastProvider", async () => {
+ const leftToasterId = "left";
+ const leftTitle = "Left Toast Title";
+ const leftDescription = "Left Toast Description";
+
+ const rightToasterId = "right";
+ const rightTitle = "Right Toast Title";
+ const rightDescription = "Right Toast Description";
+
+ render(
+ <>
+
+
+
+
+ >,
+ );
+
+ const leftButton = screen.getByTestId("left-button");
+ const rightButton = screen.getByTestId("right-button");
+
+ await user.click(leftButton);
+ await user.click(rightButton);
+ const regions = await screen.findAllByRole("region");
+
+ expect(regions).toHaveLength(2);
+
+ const leftRegion = regions.find(
+ (region) => region.getAttribute("data-placement") === "bottom-left",
+ );
+ const rightRegion = regions.find(
+ (region) => region.getAttribute("data-placement") === "bottom-right",
+ );
+
+ // check for left ToastProvider
+ expect(leftRegion).toHaveAttribute("data-placement", "bottom-left");
+ expect(leftRegion).toContainHTML(leftTitle);
+ expect(leftRegion).toContainHTML(leftDescription);
+ expect(leftRegion).not.toContainHTML(rightTitle);
+ expect(leftRegion).not.toContainHTML(rightDescription);
+
+ // check for right ToastProvider
+ expect(rightRegion).toHaveAttribute("data-placement", "bottom-right");
+ expect(rightRegion).toContainHTML(rightTitle);
+ expect(rightRegion).toContainHTML(rightDescription);
+ expect(rightRegion).not.toContainHTML(leftTitle);
+ expect(rightRegion).not.toContainHTML(leftDescription);
+ });
});
diff --git a/packages/components/toast/src/toast-provider.tsx b/packages/components/toast/src/toast-provider.tsx
index d203129a08..cc74e7c9a9 100644
--- a/packages/components/toast/src/toast-provider.tsx
+++ b/packages/components/toast/src/toast-provider.tsx
@@ -10,7 +10,8 @@ import {ToastRegion} from "./toast-region";
const loadFeatures = () => import("framer-motion").then((res) => res.domMax);
-let globalToastQueue: ToastQueue | null = null;
+const defaultToasterId = "heroui";
+let globalToastQueues: Record> = {};
interface ToastProviderProps {
maxVisibleToasts?: number;
@@ -19,17 +20,22 @@ interface ToastProviderProps {
toastProps?: ToastProps;
toastOffset?: number;
regionProps?: RegionProps;
+ toasterId?: string;
}
-export const getToastQueue = () => {
- if (!globalToastQueue) {
- globalToastQueue = new ToastQueue({
+export function getToastQueue(): ToastQueue;
+export function getToastQueue(args: {toasterId: string}): ToastQueue;
+export function getToastQueue(args?: {toasterId: string}) {
+ const toasterId = args?.toasterId || defaultToasterId;
+
+ if (!globalToastQueues[toasterId]) {
+ globalToastQueues[toasterId] = new ToastQueue({
maxVisibleToasts: Infinity,
});
}
- return globalToastQueue;
-};
+ return globalToastQueues[toasterId];
+}
export const ToastProvider = ({
placement = "bottom-right",
@@ -37,9 +43,10 @@ export const ToastProvider = ({
maxVisibleToasts = 3,
toastOffset = 0,
toastProps = {},
+ toasterId = defaultToasterId,
regionProps,
}: ToastProviderProps) => {
- const toastQueue = useToastQueue(getToastQueue());
+ const toastQueue = useToastQueue(getToastQueue({toasterId}));
const globalContext = useProviderContext();
const disableAnimation = disableAnimationProp ?? globalContext?.disableAnimation ?? false;
@@ -60,29 +67,36 @@ export const ToastProvider = ({
);
};
-export const addToast = ({...props}: ToastProps & ToastOptions) => {
- if (!globalToastQueue) {
+export const addToast = ({toasterId = defaultToasterId, ...props}: ToastProps & ToastOptions) => {
+ if (!globalToastQueues[toasterId]) {
return null;
}
- return globalToastQueue.add(props);
+ return globalToastQueues[toasterId].add(props);
};
-export const closeToast = (key: string) => {
- if (!globalToastQueue) {
+export function closeToast(key: string): void;
+export function closeToast(args: {key: string; toasterId?: string}): void;
+export function closeToast(args: string | {key: string; toasterId?: string}) {
+ const {key, toasterId} =
+ typeof args === "string"
+ ? {key: args, toasterId: defaultToasterId}
+ : {key: args.key, toasterId: args.toasterId || defaultToasterId};
+
+ if (!globalToastQueues[toasterId]) {
return;
}
- globalToastQueue.close(key);
-};
+ globalToastQueues[toasterId].close(key);
+}
-export const closeAll = () => {
- if (!globalToastQueue) {
+export const closeAll = (toasterId = defaultToasterId) => {
+ if (!globalToastQueues[toasterId]) {
return;
}
- const keys = globalToastQueue.visibleToasts.map((toast) => toast.key);
+ const keys = globalToastQueues[toasterId].visibleToasts.map((toast) => toast.key);
keys.map((key) => {
- globalToastQueue?.close(key);
+ globalToastQueues[toasterId]?.close(key);
});
};
diff --git a/packages/components/toast/src/use-toast.ts b/packages/components/toast/src/use-toast.ts
index faf659d1dd..a8bf4dd766 100644
--- a/packages/components/toast/src/use-toast.ts
+++ b/packages/components/toast/src/use-toast.ts
@@ -110,6 +110,11 @@ export interface ToastProps extends ToastVariantProps {
* @default "default"
*/
severity?: "default" | "primary" | "secondary" | "success" | "warning" | "danger";
+ /**
+ * The id of the ToastProvider. The decides which ToastProvider will handle the toast.
+ * @default "heroui"
+ */
+ toasterId?: string;
}
interface Props extends Omit, "title">, ToastProps {
diff --git a/packages/components/toast/stories/toast.stories.tsx b/packages/components/toast/stories/toast.stories.tsx
index ae1656af92..4060df9a09 100644
--- a/packages/components/toast/stories/toast.stories.tsx
+++ b/packages/components/toast/stories/toast.stories.tsx
@@ -425,6 +425,52 @@ const CloseToastTemplate = (args: ToastProps) => {
);
};
+const MultiToasterTemplate = (args) => {
+ const topToasterId = "multi-toaster-top";
+ const bottomToasterId = "multi-toaster-bottom";
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+};
+
export const Default = {
render: Template,
args: {
@@ -528,3 +574,10 @@ export const CloseToast = {
...defaultProps,
},
};
+
+export const MultiToaster = {
+ render: MultiToasterTemplate,
+ args: {
+ ...defaultProps,
+ },
+};