diff --git a/package-lock.json b/package-lock.json
index 78dd7786..4274770d 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 7576c8b7..5e982232 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/toast/__snapshots__/toast.test.tsx.snap b/src/components/toast/__snapshots__/toast.test.tsx.snap
new file mode 100644
index 00000000..ba1c63e7
--- /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`] = `
+
+
+
+
+
+
+ -
+
+ Toast message
+
+
+
+
+
+
+
+
+
+
+
+
+`;
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 00000000..e2ee3e78
--- /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 (
+ {} }}
+ onExit={() => setOpen(false)}
+ >
+
+
+ );
+ };
+
+ return {renderToast()}
;
+}
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 00000000..d8cf9b49
--- /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 (
+ setOpen(false)}
+ >
+
+
+ );
+ };
+
+ return {renderToast()}
;
+}
diff --git a/src/components/toast/index.js b/src/components/toast/index.js
new file mode 100644
index 00000000..2277d21e
--- /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.test.tsx b/src/components/toast/toast.test.tsx
new file mode 100644
index 00000000..96c46e4c
--- /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: ,
+ onExit: mockedOnExit
+ };
+
+ test('renders basic', () => {
+ const { baseElement } = render();
+ expect(baseElement).toMatchSnapshot();
+ });
+
+ test('renders without close', () => {
+ render();
+ expect(screen.queryByTestId('toast-close')).toBeNull();
+ });
+
+ test('renders without action button', () => {
+ render(
+ trigger}
+ />
+ );
+ 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: ,
+ onExit: mockedOnExit,
+ duration: 1000
+ } as const;
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ test('fires onExit after duration', () => {
+ render();
+ expect(mockedOnExit).toHaveBeenCalledTimes(0);
+ jest.advanceTimersByTime(1001);
+ expect(mockedOnExit).toHaveBeenCalledTimes(1);
+ });
+ test('fires onExit when closing with close button', () => {
+ render();
+ fireEvent.click(screen.getByTestId('toast-close'));
+ expect(mockedOnExit).toHaveBeenCalledTimes(1);
+ });
+
+ test('fires onExit when pressing action button', () => {
+ render();
+ fireEvent.click(screen.getByTestId('toast-action'));
+ expect(mockedOnExit).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/src/components/toast/toast.tsx b/src/components/toast/toast.tsx
new file mode 100644
index 00000000..d6f1694e
--- /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 (
+
+ {children}
+
+
+ {content}
+
+
+ {action && (
+
+ {action.text}
+
+ )}
+ {closeButton && (
+
+
+
+ )}
+
+
+
+
+ );
+}
+
+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
+};