diff --git a/.changeset/eighty-snails-watch.md b/.changeset/eighty-snails-watch.md new file mode 100644 index 0000000000..5cdbc7c84e --- /dev/null +++ b/.changeset/eighty-snails-watch.md @@ -0,0 +1,6 @@ +--- +"@twilio-paste/toast": minor +"@twilio-paste/core": minor +--- + +[Toast]: adds ability to display multiple toasts from a single call diff --git a/packages/paste-core/components/toast/__tests__/useToaster.spec.tsx b/packages/paste-core/components/toast/__tests__/useToaster.spec.tsx index 1007ed3361..3d33d205d3 100644 --- a/packages/paste-core/components/toast/__tests__/useToaster.spec.tsx +++ b/packages/paste-core/components/toast/__tests__/useToaster.spec.tsx @@ -27,6 +27,25 @@ describe("useToaster", () => { expect(result.current.toasts.length).toEqual(1); }); + it("should push multiple toasts by calling it twice", () => { + const { result } = renderHook(() => useToaster()); + expect(result.current.toasts).toEqual([]); + + act(() => { + result.current.push({ + message: "hi", + variant: "error", + }); + // eslint-disable-next-line unicorn/no-array-push-push + result.current.push({ + message: "hi", + variant: "error", + }); + }); + + expect(result.current.toasts.length).toEqual(2); + }); + it("should generate an id when none is passed", () => { const { result } = renderHook(() => useToaster()); diff --git a/packages/paste-core/components/toast/src/useToaster.ts b/packages/paste-core/components/toast/src/useToaster.ts index 49c164e2e9..d1af9cf0e8 100644 --- a/packages/paste-core/components/toast/src/useToaster.ts +++ b/packages/paste-core/components/toast/src/useToaster.ts @@ -20,7 +20,7 @@ export const useToaster = (): UseToasterReturnedProps => { } }); }; - }, []); + }, [toasts]); const pop = (id: ToasterToast["id"]): void => { if (!isMounted.current) { @@ -50,7 +50,7 @@ export const useToaster = (): UseToasterReturnedProps => { } const generatedID = uid(newToast); - let timeOutId; + let timeOutId: number; /* * if you are setting a dismissAfter time, we need to grab a timeout id to use later if we need to clear the timeout * for that particular toast. We also need to make sure the time is an integer to prevent locking the browser @@ -58,18 +58,21 @@ export const useToaster = (): UseToasterReturnedProps => { if (newToast.dismissAfter != null && Number.isInteger(newToast.dismissAfter)) { timeOutId = window.setTimeout(pop, newToast.dismissAfter, newToast.id || generatedID); } - /* - * We set a new toast to always setFocus. For all the existing toasts in the stack, we need to clear setFocus - * without creating a new state object. If you create a new state object, you cause react spring to rerun - * all the animations for the entire stack. So we mutate existing state instead. - */ - const existingToasts = toasts.map((toast) => { - const tmpToast = toast; - tmpToast.setFocus = false; - return tmpToast; - }); + // add the new toast with a generatedID, timeoutid and setFocus to true. Allow for user to override - setToasts([{ id: generatedID, timeOutId, setFocus: true, ...newToast }, ...existingToasts]); + setToasts((state) => { + /* + * We set a new toast to always setFocus. For all the existing toasts in the stack, we need to clear setFocus + * without creating a new state object. If you create a new state object, you cause react spring to rerun + * all the animations for the entire stack. So we mutate existing state instead. + */ + const existingToasts = state.map((toast) => { + const tmpToast = toast; + tmpToast.setFocus = false; + return tmpToast; + }); + return [{ id: generatedID, timeOutId, setFocus: true, ...newToast }, ...existingToasts]; + }); }; return { toasts, push, pop };