Repository: mapbox/mr-ui
Pull request #200 (2022-10-04 20:35:27)

Add toast UI from radix
**Describe changes:**
Added a toast component that dismisses after a delay or on a user action.

<img width="1052" alt="image" src="https://user-images.githubusercontent.com/10137547/193920215-5ee4225c-7eb3-411f-8494-b1e36d6a96a6.png">

QA checklist:
- [x] Toast should be dismissed after default 5000 ms unless overidden
- [x] Consumer can customize visibility of close button and cta button
- [x] Toast should be vertically centered on screen and responsively shrink as viewport is narrowed
- [x] Documentation is clear and helpful

Reviewer: @tristen 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b67047b..8c8c00e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@ Changelog
 
 ## HEAD
 
+- Support `disabled` option in modal actions. [#199](https://github.com/mapbox/mr-ui/pull/199)
+
+
 ## 2.0.0
 
 - [fix] added new styling for ControlToggleGroup component that is consistent with actual usage.
diff --git a/package-lock.json b/package-lock.json
index 78dd778..4274770 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,7 +6,7 @@
   "packages": {
     "": {
       "name": "@mapbox/mr-ui",
-      "version": "2.0.0-beta.19",
+      "version": "2.0.0",
       "license": "BSD-2-Clause",
       "dependencies": {
         "@mapbox/mbx-assembly": "^1.3.0",
@@ -20,6 +20,7 @@
         "@radix-ui/react-slider": "^1.0.0",
         "@radix-ui/react-switch": "^1.0.0",
         "@radix-ui/react-tabs": "^1.0.0",
+        "@radix-ui/react-toast": "^1.0.0",
         "@radix-ui/react-tooltip": "^1.0.0",
         "@radix-ui/react-visually-hidden": "^1.0.0",
         "classnames": "^2.2.6",
@@ -60,7 +61,6 @@
         "react-app-rewired": "^2.2.1",
         "react-docgen": "^5.4.3",
         "react-dom": "^16.14.0",
-        "react-helmet": "^5.2.0",
         "react-scripts": "5.0.1",
         "typescript": "^4.7.4",
         "web-vitals": "^2.1.4"
@@ -4324,6 +4324,30 @@
         "react-dom": "^16.8 || ^17.0 || ^18.0"
       }
     },
+    "node_modules/@radix-ui/react-toast": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.0.0.tgz",
+      "integrity": "sha512-mdoF6rahgushdev0OX+9a7JKoH0xZAZBo2Ktf/s779S7EnkZeL3/MFiRIV5LpRP5CtASmfdSD3FLnEvG1RHRtQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/primitive": "1.0.0",
+        "@radix-ui/react-collection": "1.0.0",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-context": "1.0.0",
+        "@radix-ui/react-dismissable-layer": "1.0.0",
+        "@radix-ui/react-portal": "1.0.0",
+        "@radix-ui/react-presence": "1.0.0",
+        "@radix-ui/react-primitive": "1.0.0",
+        "@radix-ui/react-use-callback-ref": "1.0.0",
+        "@radix-ui/react-use-controllable-state": "1.0.0",
+        "@radix-ui/react-use-layout-effect": "1.0.0",
+        "@radix-ui/react-visually-hidden": "1.0.0"
+      },
+      "peerDependencies": {
+        "react": "^16.8 || ^17.0 || ^18.0",
+        "react-dom": "^16.8 || ^17.0 || ^18.0"
+      }
+    },
     "node_modules/@radix-ui/react-tooltip": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.0.tgz",
@@ -20224,25 +20248,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/react-fast-compare": {
-      "version": "2.0.4",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/react-helmet": {
-      "version": "5.2.1",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "object-assign": "^4.1.1",
-        "prop-types": "^15.5.4",
-        "react-fast-compare": "^2.0.2",
-        "react-side-effect": "^1.1.0"
-      },
-      "peerDependencies": {
-        "react": ">=15.0.0"
-      }
-    },
     "node_modules/react-is": {
       "version": "17.0.2",
       "dev": true,
@@ -21303,17 +21308,6 @@
         "browserslist": ">= 4.21.0"
       }
     },
-    "node_modules/react-side-effect": {
-      "version": "1.2.0",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "shallowequal": "^1.0.1"
-      },
-      "peerDependencies": {
-        "react": "^0.13.0 || ^0.14.0 || ^15.0.0 || ^16.0.0"
-      }
-    },
     "node_modules/react-style-singleton": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -22303,11 +22297,6 @@
       "version": "1.2.1",
       "license": "MIT"
     },
-    "node_modules/shallowequal": {
-      "version": "1.1.0",
-      "dev": true,
-      "license": "MIT"
-    },
     "node_modules/shebang-command": {
       "version": "1.2.0",
       "dev": true,
@@ -28194,6 +28183,26 @@
         "@radix-ui/react-use-controllable-state": "1.0.0"
       }
     },
+    "@radix-ui/react-toast": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.0.0.tgz",
+      "integrity": "sha512-mdoF6rahgushdev0OX+9a7JKoH0xZAZBo2Ktf/s779S7EnkZeL3/MFiRIV5LpRP5CtASmfdSD3FLnEvG1RHRtQ==",
+      "requires": {
+        "@babel/runtime": "^7.13.10",
+        "@radix-ui/primitive": "1.0.0",
+        "@radix-ui/react-collection": "1.0.0",
+        "@radix-ui/react-compose-refs": "1.0.0",
+        "@radix-ui/react-context": "1.0.0",
+        "@radix-ui/react-dismissable-layer": "1.0.0",
+        "@radix-ui/react-portal": "1.0.0",
+        "@radix-ui/react-presence": "1.0.0",
+        "@radix-ui/react-primitive": "1.0.0",
+        "@radix-ui/react-use-callback-ref": "1.0.0",
+        "@radix-ui/react-use-controllable-state": "1.0.0",
+        "@radix-ui/react-use-layout-effect": "1.0.0",
+        "@radix-ui/react-visually-hidden": "1.0.0"
+      }
+    },
     "@radix-ui/react-tooltip": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.0.tgz",
@@ -38421,20 +38430,6 @@
       "version": "6.0.11",
       "dev": true
     },
-    "react-fast-compare": {
-      "version": "2.0.4",
-      "dev": true
-    },
-    "react-helmet": {
-      "version": "5.2.1",
-      "dev": true,
-      "requires": {
-        "object-assign": "^4.1.1",
-        "prop-types": "^15.5.4",
-        "react-fast-compare": "^2.0.2",
-        "react-side-effect": "^1.1.0"
-      }
-    },
     "react-is": {
       "version": "17.0.2",
       "dev": true
@@ -38955,13 +38950,6 @@
         }
       }
     },
-    "react-side-effect": {
-      "version": "1.2.0",
-      "dev": true,
-      "requires": {
-        "shallowequal": "^1.0.1"
-      }
-    },
     "react-style-singleton": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -39610,10 +39598,6 @@
     "shallow-equal": {
       "version": "1.2.1"
     },
-    "shallowequal": {
-      "version": "1.1.0",
-      "dev": true
-    },
     "shebang-command": {
       "version": "1.2.0",
       "dev": true,
diff --git a/package.json b/package.json
index 7576c8b..5e98223 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
     "@radix-ui/react-tabs": "^1.0.0",
     "@radix-ui/react-tooltip": "^1.0.0",
     "@radix-ui/react-visually-hidden": "^1.0.0",
+    "@radix-ui/react-toast": "^1.0.0",
     "classnames": "^2.2.6",
     "clipboard": "^2.0.0",
     "debounce": "^1.1.0",
diff --git a/src/components/modal/modal-actions.tsx b/src/components/modal/modal-actions.tsx
index c1d11c1..d7bcd62 100644
--- a/src/components/modal/modal-actions.tsx
+++ b/src/components/modal/modal-actions.tsx
@@ -7,14 +7,17 @@ interface Props {
     text: string;
     callback: () => void;
     destructive?: boolean;
+    disabled?: boolean;
   },
   secondaryAction?: {
     text: string;
     callback: () => void;
+    disabled?: boolean;
   },
   tertiaryAction?: {
     text: string;
     callback: () => void;
+    disabled?: boolean;
   }
 }
 
@@ -34,6 +37,7 @@ export default function ModalActions({
           variant="secondary"
           size="medium"
           onClick={secondaryAction.callback}
+          disabled={secondaryAction.disabled}
           data-test="secondary-modal-action"
           passthroughProps={{ 'aria-label': secondaryAction.text }}
         >
@@ -58,6 +62,7 @@ export default function ModalActions({
           aria-label={tertiaryAction.text}
           variant="tertiary"
           onClick={tertiaryAction.callback}
+          disabled={tertiaryAction.disabled}
           data-test="tertiary-modal-action"
         >
           {tertiaryAction.text}
@@ -77,6 +82,7 @@ export default function ModalActions({
           variant={primaryButtonVariant}
           size="medium"
           onClick={primaryAction.callback}
+          disabled={primaryAction.disabled}
           data-test="primary-modal-action"
           passthroughProps={{
             tabIndex: 0,
@@ -96,14 +102,17 @@ ModalActions.propTypes = {
   primaryAction: PropTypes.shape({
     text: PropTypes.string.isRequired,
     callback: PropTypes.func.isRequired,
-    destructive: PropTypes.bool
+    destructive: PropTypes.bool,
+    disabled: PropTypes.bool
   }).isRequired,
   secondaryAction: PropTypes.shape({
     text: PropTypes.string.isRequired,
-    callback: PropTypes.func.isRequired
+    callback: PropTypes.func.isRequired,
+    disabled: PropTypes.bool
   }),
   tertiaryAction: PropTypes.shape({
     text: PropTypes.string.isRequired,
-    callback: PropTypes.func.isRequired
+    callback: PropTypes.func.isRequired,
+    disabled: PropTypes.bool
   })
 };
diff --git a/src/components/modal/modal.tsx b/src/components/modal/modal.tsx
index 74ed3b3..ae6cd9e 100644
--- a/src/components/modal/modal.tsx
+++ b/src/components/modal/modal.tsx
@@ -20,14 +20,17 @@ interface Props {
     text: string;
     callback: () => void;
     destructive?: boolean;
+    disabled?: boolean;
   },
   secondaryAction?: {
     text: string;
     callback: () => void;
+    disabled?: boolean;
   },
   tertiaryAction?: {
     text: string;
     callback: () => void;
+    disabled?: boolean;
   }
 }
 
@@ -181,9 +184,8 @@ Modal.propTypes = {
    */
   onExit: PropTypes.func,
   /**
-   * A function that should the primary application node, which should be
-   * `aria-hidden` when the modal is open. By default, returns
-   * `document.getElementById('app')`.
+   * Modal container size. Options are `small`, `large`, or `auto`. If `auto`
+   * is provided, a width is not specified.
    */
   size: PropTypes.oneOf(['small', 'large', 'auto']),
   /**
diff --git a/src/components/toast/examples/toast-example-basic.tsx b/src/components/toast/examples/toast-example-basic.tsx
new file mode 100644
index 0000000..e2ee3e7
--- /dev/null
+++ b/src/components/toast/examples/toast-example-basic.tsx
@@ -0,0 +1,27 @@
+/*
+A toast message with an action.
+*/
+import React, { ReactElement, useState } from 'react';
+import Toast from '../toast';
+
+export default function Example() {
+  const [open, setOpen] = useState(false);
+  const renderToast = (): ReactElement => {
+    return (
+      <Toast
+        content="3 files have been moved to 'Your folder'"
+        duration={3000}
+        closeButton={false}
+        active={open}
+        action={{ text: 'Open folder', callback: () => {} }}
+        onExit={() => setOpen(false)}
+      >
+        <button className="btn" onClick={() => setOpen(true)}>
+          Trigger toast
+        </button>
+      </Toast>
+    );
+  };
+
+  return <div className="flex">{renderToast()}</div>;
+}
diff --git a/src/components/toast/examples/toast-example-overflow.tsx b/src/components/toast/examples/toast-example-overflow.tsx
new file mode 100644
index 0000000..d8cf9b4
--- /dev/null
+++ b/src/components/toast/examples/toast-example-overflow.tsx
@@ -0,0 +1,26 @@
+/*
+A simple toast message without an action button, with a close button, and with a long message that truncates.
+*/
+import React, { ReactElement, useState } from 'react';
+import Toast from '../toast';
+
+export default function Example() {
+  const [open, setOpen] = useState(false);
+  const renderToast = (): ReactElement => {
+    return (
+      <Toast
+        content="This is a much longer toast message that will likely truncate eventually with enough text"
+        duration={5000}
+        closeButton={true}
+        active={open}
+        onExit={() => setOpen(false)}
+      >
+        <button className="btn" onClick={() => setOpen(true)}>
+          Another toast
+        </button>
+      </Toast>
+    );
+  };
+
+  return <div className="flex">{renderToast()}</div>;
+}
diff --git a/src/components/toast/index.js b/src/components/toast/index.js
new file mode 100644
index 0000000..2277d21
--- /dev/null
+++ b/src/components/toast/index.js
@@ -0,0 +1,3 @@
+import main from './toast';
+
+export default main;
diff --git a/src/components/toast/toast.tsx b/src/components/toast/toast.tsx
new file mode 100644
index 0000000..d6f1694
--- /dev/null
+++ b/src/components/toast/toast.tsx
@@ -0,0 +1,111 @@
+import React, { ReactElement, ReactNode } from 'react';
+import PropTypes from 'prop-types';
+import * as ToastPrimitive from '@radix-ui/react-toast';
+import Icon from '../icon';
+
+interface Props {
+  content: string;
+  children: ReactNode;
+  active: boolean;
+  onExit: () => void;
+  action?: {
+    text: string;
+    callback: () => void;
+  };
+  duration?: number;
+  closeButton?: boolean;
+}
+
+export default function Toast({
+  content,
+  children,
+  active,
+  onExit,
+  duration = 5000,
+  action,
+  closeButton = true
+}: Props): ReactElement {
+  let actionBtnClass = closeButton ? '' : 'pr12';
+  return (
+    <ToastPrimitive.Provider swipeDirection="down" duration={duration}>
+      {children}
+      <ToastPrimitive.Root
+        type="foreground"
+        open={active}
+        onOpenChange={onExit}
+        className="bg-gray-dark round wmax600 w-11/12 flex flex--center-cross row flex--space-between-main py12 pl12 hmin60"
+      >
+        <ToastPrimitive.Description className="color-gray-lighter txt-truncate w-auto mr12">
+          {content}
+        </ToastPrimitive.Description>
+        <span
+          className={`flex flex-row flex--center-cross ${actionBtnClass} flex-child-no-shrink`}
+        >
+          {action && (
+            <ToastPrimitive.Action
+              altText="Open"
+              className="btn"
+              type="button"
+              onClick={action.callback}
+              data-testid="toast-action"
+            >
+              {action.text}
+            </ToastPrimitive.Action>
+          )}
+          {closeButton && (
+            <ToastPrimitive.Close
+              className="btn btn--transparent color-gray-light"
+              type="button"
+              aria-label="Close"
+              data-testid="toast-close"
+            >
+              <Icon name="close" />
+            </ToastPrimitive.Close>
+          )}
+        </span>
+      </ToastPrimitive.Root>
+      <ToastPrimitive.Viewport className="fixed left bottom mb24 w-full flex flex--center-main" />
+    </ToastPrimitive.Provider>
+  );
+}
+
+Toast.propTypes = {
+  /**
+   * The trigger element.
+   */
+  children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired,
+  /**
+   * The toast content. This can either be a string, valid JSX, or a function
+   * returning either. Content text will truncate.
+   */
+  content: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired,
+  /**
+   * A function called when popover is dismissed. You need to use this callback
+   * to remove the Toast from the rendered page.
+   */
+  onExit: PropTypes.func.isRequired,
+  /**
+   * Triggers the active state of the toast. When true, the toast appears.
+   */
+  active: PropTypes.bool.isRequired,
+  /**
+   * The primary action for the toast.
+   *
+   * The value is an object with the following properties:
+   * - `text`: **(required)** The text of the button.
+   * - `callback`: **(required)** Invoked when the button is clicked.
+   */
+  action: PropTypes.shape({
+    text: PropTypes.string.isRequired,
+    callback: PropTypes.func.isRequired
+  }),
+  /**
+   * The duration the toast should appear for. Recommended toast duration is 5 seconds with 1 extra second for every additional 300 characters in the toast body.
+   */
+  duration: PropTypes.number,
+  /**
+   * When `true` the toast will have a separate close button (in addition to the call-to-action button).
+   * When `false` toast will only have action button.
+   */
+  closeButton: PropTypes.bool
+};


Test patch:
diff --git a/src/components/modal/modal-actions.test.tsx b/src/components/modal/modal-actions.test.tsx
index 07b3dfa..bc0060a 100644
--- a/src/components/modal/modal-actions.test.tsx
+++ b/src/components/modal/modal-actions.test.tsx
@@ -94,6 +94,30 @@ describe('ModalActions', () => {
       fireEvent.click(actions[2]);
       expect(mockedTertiaryCallback).toHaveBeenCalledTimes(1);
     });
+
+    test('disabled callbacks', () => {
+      const mockedPrimaryCallback = jest.fn();
+      const mockedSecondaryCallback = jest.fn();
+      const mockedTertiaryCallback = jest.fn();
+      render(
+        <ModalActions
+          primaryAction={{ text: 'Okay', disabled: true, callback: mockedPrimaryCallback }}
+          secondaryAction={{ text: 'Cancel', disabled: true, callback: mockedSecondaryCallback }}
+          tertiaryAction={{ text: 'Exit', disabled: true, callback: mockedTertiaryCallback }}
+        />
+      );
+
+      const actions = screen.queryAllByRole('button');
+
+      fireEvent.click(actions[0]);
+      expect(mockedPrimaryCallback).not.toHaveBeenCalled();
+
+      fireEvent.click(actions[1]);
+      expect(mockedSecondaryCallback).not.toHaveBeenCalled();
+
+      fireEvent.click(actions[2]);
+      expect(mockedTertiaryCallback).not.toHaveBeenCalled();
+    });
   });
 
   describe('primary and tertiary', () => {
diff --git a/src/components/toast/__snapshots__/toast.test.tsx.snap b/src/components/toast/__snapshots__/toast.test.tsx.snap
new file mode 100644
index 0000000..ba1c63e
--- /dev/null
+++ b/src/components/toast/__snapshots__/toast.test.tsx.snap
@@ -0,0 +1,88 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Toast render with options renders basic 1`] = `
+<body>
+  <div>
+    <button>
+      Trigger
+    </button>
+    <div
+      aria-label="Notifications (F8)"
+      role="region"
+      style=""
+      tabindex="-1"
+    >
+      <span
+        aria-hidden="true"
+        style="position: fixed; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
+        tabindex="0"
+      />
+      <ol
+        class="fixed left bottom mb24 w-full flex flex--center-main"
+        tabindex="-1"
+      >
+        <li
+          class="bg-gray-dark round wmax600 w-11/12 flex flex--center-cross row flex--space-between-main py12 pl12 hmin60"
+          data-radix-collection-item=""
+          data-state="open"
+          data-swipe-direction="down"
+          style="user-select: none;"
+          tabindex="0"
+        >
+          <div
+            class="color-gray-lighter txt-truncate w-auto mr12"
+          >
+            Toast message
+          </div>
+          <span
+            class="flex flex-row flex--center-cross  flex-child-no-shrink"
+          >
+            <button
+              class="btn"
+              data-testid="toast-action"
+              type="button"
+            >
+              Open folder
+            </button>
+            <button
+              aria-label="Close"
+              class="btn btn--transparent color-gray-light"
+              data-testid="toast-close"
+              type="button"
+            >
+              <svg
+                aria-hidden="true"
+                class="events-none icon"
+                data-testid="icon-close"
+                focusable="false"
+                style="width: 18px; height: 18px;"
+              >
+                <use
+                  xlink:href="#icon-close"
+                  xmlns:xlink="http://www.w3.org/1999/xlink"
+                />
+              </svg>
+              <span
+                style="position: absolute; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
+              >
+                close
+              </span>
+            </button>
+          </span>
+        </li>
+      </ol>
+      <span
+        aria-hidden="true"
+        style="position: fixed; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
+        tabindex="0"
+      />
+    </div>
+  </div>
+  <span
+    aria-atomic="true"
+    aria-live="assertive"
+    role="status"
+    style="position: absolute; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
+  />
+</body>
+`;
diff --git a/src/components/toast/toast.test.tsx b/src/components/toast/toast.test.tsx
new file mode 100644
index 0000000..96c46e4
--- /dev/null
+++ b/src/components/toast/toast.test.tsx
@@ -0,0 +1,81 @@
+import Toast from './toast';
+import { fireEvent, render, screen } from '@testing-library/react';
+
+describe('Toast', () => {
+  describe('render with options', () => {
+    let mockedOnExit = jest.fn();
+    let props = {
+      content: 'Toast message',
+      active: true,
+      action: {
+        text: 'Open folder',
+        callback: jest.fn()
+      },
+      children: <button>Trigger</button>,
+      onExit: mockedOnExit
+    };
+
+    test('renders basic', () => {
+      const { baseElement } = render(<Toast {...props}></Toast>);
+      expect(baseElement).toMatchSnapshot();
+    });
+
+    test('renders without close', () => {
+      render(<Toast {...props} closeButton={false} />);
+      expect(screen.queryByTestId('toast-close')).toBeNull();
+    });
+
+    test('renders without action button', () => {
+      render(
+        <Toast
+          content="toast without action"
+          active={true}
+          onExit={mockedOnExit}
+          children={<div>trigger</div>}
+        />
+      );
+      expect(screen.queryByTestId('toast-action')).toBeNull();
+    });
+  });
+
+  describe('toast dismissed', () => {
+    let mockedOnExit = jest.fn();
+    const props = {
+      content: 'Toast message',
+      active: true,
+      action: {
+        text: 'Open folder',
+        callback: jest.fn()
+      },
+      children: <button>Trigger</button>,
+      onExit: mockedOnExit,
+      duration: 1000
+    } as const;
+
+    beforeEach(() => {
+      jest.useFakeTimers();
+    });
+
+    afterEach(() => {
+      jest.useRealTimers();
+    });
+
+    test('fires onExit after duration', () => {
+      render(<Toast {...props}></Toast>);
+      expect(mockedOnExit).toHaveBeenCalledTimes(0);
+      jest.advanceTimersByTime(1001);
+      expect(mockedOnExit).toHaveBeenCalledTimes(1);
+    });
+    test('fires onExit when closing with close button', () => {
+      render(<Toast {...props}></Toast>);
+      fireEvent.click(screen.getByTestId('toast-close'));
+      expect(mockedOnExit).toHaveBeenCalledTimes(1);
+    });
+
+    test('fires onExit when pressing action button', () => {
+      render(<Toast {...props}></Toast>);
+      fireEvent.click(screen.getByTestId('toast-action'));
+      expect(mockedOnExit).toHaveBeenCalledTimes(1);
+    });
+  });
+});