diff --git a/src/Toast/BaseToast.jsx b/src/Toast/BaseToast.jsx
new file mode 100644
index 0000000000..64eb1e28bb
--- /dev/null
+++ b/src/Toast/BaseToast.jsx
@@ -0,0 +1,103 @@
+/* eslint-disable react/prop-types */
+import React, { useEffect, useRef } from 'react';
+import PropTypes from 'prop-types';
+import { useIntl } from 'react-intl';
+import classNames from 'classnames';
+
+import Icon from '../Icon';
+import IconButton from '../IconButton';
+import Button from '../Button';
+import { Close } from '../../icons';
+
+function Toast({
+ id, message, onDismiss, actions, className, duration, ...rest
+}) {
+ const intl = useIntl();
+ const intlCloseLabel = intl.formatMessage({
+ id: 'pgn.Toast.closeLabel',
+ defaultMessage: 'Close',
+ description: 'Close label for Toast component',
+ });
+
+ const timerRef = useRef();
+
+ useEffect(() => {
+ timerRef.current = setTimeout(() => onDismiss(id), duration);
+
+ return () => clearTimeout(timerRef.current);
+ }, [id, onDismiss, duration]);
+
+ const clearTimer = () => {
+ clearTimeout(timerRef.current);
+ };
+
+ const startTimer = () => {
+ clearTimer();
+ timerRef.current = setTimeout(() => onDismiss(id), duration);
+ };
+
+ return (
+
+
+
{message}
+
onDismiss(id)}
+ variant="primary"
+ invertColors
+ />
+
+ {actions
+ ? (
+
+ {actions.map((action) => (
+
+ ))}
+
+ )
+ : null}
+
+ );
+}
+
+export default Toast;
+
+Toast.propTypes = {
+ id: PropTypes.number.isRequired,
+ message: PropTypes.string.isRequired,
+ onDismiss: PropTypes.func,
+ actions: PropTypes.arrayOf(
+ PropTypes.shape({
+ label: PropTypes.string.isRequired,
+ onClick: PropTypes.func,
+ href: PropTypes.string,
+ }),
+ ),
+ className: PropTypes.string,
+ duration: PropTypes.number,
+};
+
+Toast.defaultProps = {
+ onDismiss: () => {},
+ actions: null,
+ className: '',
+ duration: 5000,
+};
diff --git a/src/Toast/README.md b/src/Toast/README.md
index 9b36e17520..a1df2e2ebb 100644
--- a/src/Toast/README.md
+++ b/src/Toast/README.md
@@ -2,7 +2,7 @@
title: 'Toast'
type: 'component'
components:
-- Toast
+- ToastContainer
categories:
- Overlays
status: 'New'
@@ -11,85 +11,63 @@ devStatus: 'Done'
notes: ''
---
-``Toast`` is a pop-up style message that shows the user a brief, fleeting, dismissible message about a successful app process.
-
-``Toasts`` sit fixed to the bottom left of the window.
+`Toast` is a pop-up style message that shows the user a brief, fleeting, dismissible message about a successful app process.
## Behaviors
-
- - Auto-dismiss: Toast automatically dismisses after 5 seconds by default.
- - Disable timer: On hover of the Toast container. On hover or focus of dismiss icon or optional button
- - Re-enable timer: On mouse leave of the Toast container. On blur of dismiss icon or option button
- - Auto-dismiss timer: 5 - 15 second range.
-
+- **Customizable Appearance**: Choose the window position for toast.
+- **Auto-dismiss**: Toast automatically dismisses after a default duration of 5 seconds.
+- **Disable timer**: Pause the auto-dismiss timer on hover or focus of the Toast or the dismiss icon.
+- **Re-enable timer**: Resume the auto-dismiss timer on mouse leave or blur of the Toast component.
## Basic Usage
```jsx live
() => {
- const [show, setShow] = useState(false);
-
- return (
- <>
- setShow(false)}
- show={show}
- >
- Example of a basic Toast.
-
-
-
- >
- );
-}
-```
-
-## With Button
-
-```jsx live
-() => {
- const [show, setShow] = useState(false);
-
- return (
- <>
- console.log('You clicked the action button.')
- }}
- onClose={() => setShow(false)}
- show={show}
- >
- Success! Example of a Toast with a button.
-
-
-
- >
- );
-}
-```
+ const [position, setPosition] = useState('bottom-left');
+ const [timer, setTimer] = useState(5000);
+ const [message, setMessage] = useState('Example of a basic Toast.');
+ const [withActions, setWithActions] = useState('false');
-## With Link
-
-```jsx live
-() => {
- const [show, setShow] = useState(false);
+ const testAction = {
+ label: "Optional Button",
+ onClick: () => console.log('You clicked the action button.')
+ };
return (
<>
- setShow(false)}
- show={show}
- >
- Success! Example of a Toast with a link.
-
-
+ {/* start example form block */}
+ setPosition(value), name: 'Position', options: [
+ { value: "top-left", name: "top-left" },
+ { value: "top-right", name: "top-right" },
+ { value: "bottom-left", name: "bottom-left" },
+ { value: "bottom-right", name: "bottom-right" }]
+ },
+ { value: timer, setValue: (value) => setTimer(value), name: 'Duration (ms)', range: { min: 1000 , max: 10000, step: 1000 } },
+ { value: withActions, setValue: (value) => setWithActions(value), name: 'With actions', options: [
+ { value: 'true', name: "True" },
+ { value: 'false', name: "False" },
+ ]},
+ ]}
+ />
+ {/* end example form block */}
+
+
+ Message:
+
setMessage(e.target.value)}
+ />
+
+
+
>
);
}
diff --git a/src/Toast/Toast.test.jsx b/src/Toast/Toast.test.jsx
deleted file mode 100644
index 9e591054fc..0000000000
--- a/src/Toast/Toast.test.jsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import { IntlProvider } from 'react-intl';
-import userEvent from '@testing-library/user-event';
-
-import Toast from '.';
-
-/* eslint-disable-next-line react/prop-types */
-function ToastWrapper({ children, ...props }) {
- return (
-
-
- {children}
-
-
- );
-}
-
-describe('', () => {
- const onCloseHandler = () => {};
- const props = {
- onClose: onCloseHandler,
- show: true,
- };
- it('renders optional action as link', () => {
- render(
-
- Success message.
- ,
- );
-
- const toastLink = screen.getByRole('button', { name: 'Optional action' });
- expect(toastLink).toBeTruthy();
- });
- it('renders optional action as button', () => {
- render(
- {},
- }}
- >
- Success message.
- ,
- );
- const toastButton = screen.getByRole('button', { name: 'Optional action' });
- expect(toastButton.className).toContain('btn');
- });
- it('autohide is set to false on onMouseOver and true on onMouseLeave', async () => {
- render(
-
- Success message.
- ,
- );
- const toast = screen.getByTestId('toast');
- await userEvent.hover(toast);
- setTimeout(() => {
- expect(screen.getByText('Success message.')).toEqual(true);
- expect(toast).toHaveLength(1);
- }, 6000);
- await userEvent.unhover(toast);
- setTimeout(() => {
- expect(screen.getByText('Success message.')).toEqual(false);
- expect(toast).toHaveLength(1);
- }, 6000);
- });
- it('autohide is set to false onFocus and true onBlur', async () => {
- render(
-
- Success message.
- ,
- );
- const toast = screen.getByTestId('toast');
- toast.focus();
- setTimeout(() => {
- expect(screen.getByText('Success message.')).toEqual(true);
- expect(toast).toHaveLength(1);
- }, 6000);
- await userEvent.tab();
- setTimeout(() => {
- expect(screen.getByText('Success message.')).toEqual(false);
- expect(toast).toHaveLength(1);
- }, 6000);
- });
-});
diff --git a/src/Toast/ToastContainer.jsx b/src/Toast/ToastContainer.jsx
deleted file mode 100644
index 05049ae0f4..0000000000
--- a/src/Toast/ToastContainer.jsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-
-class ToastContainer extends React.Component {
- constructor(props) {
- super(props);
- this.toastRootName = 'toast-root';
- if (typeof document === 'undefined') {
- this.rootElement = null;
- } else if (document.getElementById(this.toastRootName)) {
- this.rootElement = document.getElementById(this.toastRootName);
- } else {
- const rootElement = document.createElement('div');
- rootElement.setAttribute('id', this.toastRootName);
- rootElement.setAttribute('class', 'toast-container');
- rootElement.setAttribute('role', 'alert');
- rootElement.setAttribute('aria-live', 'polite');
- rootElement.setAttribute('aria-atomic', 'true');
- this.rootElement = document.body.appendChild(rootElement);
- }
- }
-
- render() {
- if (this.rootElement) {
- return ReactDOM.createPortal(
- this.props.children,
- this.rootElement,
- );
- }
- return null;
- }
-}
-
-ToastContainer.propTypes = {
- /** Specifies contents of the component. */
- children: PropTypes.node.isRequired,
-};
-
-export default ToastContainer;
diff --git a/src/Toast/ToastContainer.scss b/src/Toast/ToastContainer.scss
deleted file mode 100644
index b29ba0e5dc..0000000000
--- a/src/Toast/ToastContainer.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-@import "variables";
-
-.toast-container {
- bottom: $toast-container-gutter-lg;
- left: $toast-container-gutter-lg;
- position: fixed;
- z-index: 2;
-
- [dir="rtl"] & {
- right: $toast-container-gutter-lg;
- left: 0;
- }
-
- @media only screen and (max-width: 768px) {
- bottom: $toast-container-gutter-sm;
- right: $toast-container-gutter-sm;
- left: $toast-container-gutter-sm;
-
- [dir="rtl"] & {
- left: $toast-container-gutter-sm;
- right: $toast-container-gutter-sm;
- }
- }
-}
diff --git a/src/Toast/constants/index.js b/src/Toast/constants/index.js
new file mode 100644
index 0000000000..6462c01108
--- /dev/null
+++ b/src/Toast/constants/index.js
@@ -0,0 +1,16 @@
+export const TOAST_POSITIONS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
+
+export const positionStyles = {
+ 'top-left': {
+ top: '0', left: '0', right: 'auto', bottom: 'auto',
+ },
+ 'top-right': {
+ top: '0', right: '0', left: 'auto', bottom: 'auto',
+ },
+ 'bottom-left': {
+ bottom: '0', left: '0', right: 'auto', top: 'auto',
+ },
+ 'bottom-right': {
+ bottom: '0', right: '0', left: 'auto', top: 'auto',
+ },
+};
diff --git a/src/Toast/index.jsx b/src/Toast/index.jsx
index 11461666c2..e5eb8ef1ac 100644
--- a/src/Toast/index.jsx
+++ b/src/Toast/index.jsx
@@ -1,111 +1,72 @@
-import React, { useState } from 'react';
-import classNames from 'classnames';
+/* eslint-disable react/prop-types */
+import React, { useState, useEffect, useRef } from 'react';
+import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
-import BaseToast from 'react-bootstrap/Toast';
-import { useIntl } from 'react-intl';
+import { toastEmitter } from './utils';
+import BaseToast from './BaseToast';
+import { positionStyles, TOAST_POSITIONS } from './constants';
-import { Close } from '../../icons';
-import ToastContainer from './ToastContainer';
-import Button from '../Button';
-import Icon from '../Icon';
-import IconButton from '../IconButton';
+function ToastContainer({ position: defaultPosition, className }) {
+ const [toasts, setToasts] = useState([]);
+ const portalDivRef = useRef(null);
-export const TOAST_CLOSE_LABEL_TEXT = 'Close';
-export const TOAST_DELAY = 5000;
+ if (!portalDivRef.current && typeof document !== 'undefined') {
+ portalDivRef.current = document.createElement('div');
+ portalDivRef.current.setAttribute('class', 'pgn__toast-portal');
+ portalDivRef.current.setAttribute('role', 'alert');
+ portalDivRef.current.setAttribute('aria-live', 'polite');
+ portalDivRef.current.setAttribute('aria-atomic', 'true');
+ document.body.appendChild(portalDivRef.current);
+ }
-function Toast({
- action, children, className, closeLabel, onClose, show, ...rest
-}) {
- const intl = useIntl();
- const [autoHide, setAutoHide] = useState(true);
- const intlCloseLabel = closeLabel || intl.formatMessage({
- id: 'pgn.Toast.closeLabel',
- defaultMessage: 'Close',
- description: 'Close label for Toast component',
- });
- return (
-
- setAutoHide(true)}
- onFocus={() => setAutoHide(false)}
- onMouseOut={() => setAutoHide(true)}
- onMouseOver={() => setAutoHide(false)}
- show={show}
- {...rest}
- >
-
- {action && (
-
- )}
-
-
- );
+ const removeToast = (id) => {
+ setToasts(currentToasts => currentToasts.filter(toast => toast.id !== id));
+ };
+
+ useEffect(() => {
+ const handleShowToast = ({
+ message, duration, actions, position,
+ }) => {
+ const id = Date.now();
+ setToasts(currentToasts => [...currentToasts, {
+ id, message, duration, actions, position: position || defaultPosition,
+ }]);
+ };
+
+ toastEmitter.subscribe('showToast', handleShowToast);
+
+ return () => {
+ toastEmitter.events.showToast = toastEmitter.events.showToast.filter(
+ callback => callback !== handleShowToast,
+ );
+ if (portalDivRef.current) {
+ document.body.removeChild(portalDivRef.current);
+ }
+ };
+ }, [defaultPosition]);
+
+ return portalDivRef.current ? ReactDOM.createPortal(
+ Object.keys(positionStyles).map(position => (
+
+ {toasts.filter(toast => toast.position === position).map(toast => (
+ removeToast(toast.id)} />
+ ))}
+
+ )),
+ portalDivRef.current,
+ ) : null;
}
-Toast.defaultProps = {
- action: null,
- closeLabel: undefined,
- delay: TOAST_DELAY,
- className: undefined,
-};
+export default ToastContainer;
+export { toast } from './utils';
-Toast.propTypes = {
- /** A string or an element that is rendered inside the main body of the `Toast`. */
- children: PropTypes.string.isRequired,
- /**
- * A function that is called on close. It can be used to perform
- * actions upon closing of the `Toast`, such as setting the "show"
- * element to false.
- * */
- onClose: PropTypes.func.isRequired,
- /** Boolean used to control whether the `Toast` shows */
- show: PropTypes.bool.isRequired,
- /**
- * Fields used to build optional action button.
- * `label` is a string rendered inside the button.
- * `href` is a link that will render the action button as an anchor tag.
- * `onClick` is a function that is called when the button is clicked.
- */
- action: PropTypes.shape({
- label: PropTypes.string.isRequired,
- href: PropTypes.string,
- onClick: PropTypes.func,
- }),
- /**
- * Alt text for the `Toast`'s dismiss button. Defaults to 'Close'.
- */
- closeLabel: PropTypes.string,
- /** Time in milliseconds for which the `Toast` will display. */
- delay: PropTypes.number,
- /** Class names for the `BaseToast` component */
+ToastContainer.propTypes = {
+ position: PropTypes.oneOf(TOAST_POSITIONS),
className: PropTypes.string,
};
-export default Toast;
+ToastContainer.defaultProps = {
+ position: 'bottom-left',
+ className: '',
+};
diff --git a/src/Toast/index.scss b/src/Toast/index.scss
index 58658f0e9c..873e5aa151 100644
--- a/src/Toast/index.scss
+++ b/src/Toast/index.scss
@@ -1,40 +1,32 @@
@import "variables";
-@import "~bootstrap/scss/toasts";
-.toast {
+.pgn__toast {
background-color: $toast-background-color;
box-shadow: $toast-box-shadow;
margin: 0;
- padding: 1rem;
+ padding: $spacer;
position: relative;
border-radius: $toast-border-radius;
- z-index: 2;
-
- &.show {
- display: flex;
- flex-direction: column;
- }
-
- .toast-header-btn-container {
- margin: -.25rem -.5rem;
- align-self: flex-start;
- }
+ display: flex;
+ flex-direction: column;
.btn {
margin-top: .75rem;
align-self: flex-start;
}
- .toast-header {
+ .pgn__toast__header {
+ display: flex;
align-items: center;
border-bottom: 0;
justify-content: space-between;
padding: 0;
- p {
+ .pgn__toast__message {
font-size: $small-font-size;
margin: 0;
padding-right: .75rem;
+ color: $toast-header-color;
}
& + .btn {
@@ -42,6 +34,11 @@
}
}
+ .pgn__toast__optional-actions {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
@media only screen and (max-width: 768px) {
max-width: 100%;
}
@@ -51,3 +48,12 @@
max-width: $toast-max-width;
}
}
+
+.pgn__toast-container {
+ display: flex;
+ flex-direction: column;
+ gap: .5rem;
+ padding: $spacer;
+ position: fixed;
+ z-index: 3000;
+}
diff --git a/src/Toast/tests/EventEmitter.test.js b/src/Toast/tests/EventEmitter.test.js
new file mode 100644
index 0000000000..cc5df569c5
--- /dev/null
+++ b/src/Toast/tests/EventEmitter.test.js
@@ -0,0 +1,59 @@
+import { toastEmitter } from '../utils';
+
+describe('EventEmitter', () => {
+ test('subscribes and emits an event', () => {
+ const mockCallback = jest.fn();
+ toastEmitter.subscribe('testEvent', mockCallback);
+
+ toastEmitter.emit('testEvent', 'testData');
+ expect(mockCallback).toHaveBeenCalledWith('testData');
+ });
+
+ test('emits an event with data', () => {
+ const mockCallback = jest.fn();
+ toastEmitter.subscribe('testEvent', mockCallback);
+
+ const testData = { key: 'value' };
+ toastEmitter.emit('testEvent', testData);
+ expect(mockCallback).toHaveBeenCalledWith(testData);
+ });
+
+ test('handles multiple subscriptions to the same event', () => {
+ const mockCallback1 = jest.fn();
+ const mockCallback2 = jest.fn();
+
+ toastEmitter.subscribe('testEvent', mockCallback1);
+ toastEmitter.subscribe('testEvent', mockCallback2);
+
+ toastEmitter.emit('testEvent');
+ expect(mockCallback1).toHaveBeenCalled();
+ expect(mockCallback2).toHaveBeenCalled();
+ });
+
+ test('emits an event with no subscribers', () => {
+ const mockCallback = jest.fn();
+
+ toastEmitter.emit('testEvent');
+ expect(mockCallback).not.toHaveBeenCalled();
+ });
+
+ test('handles multiple different events', () => {
+ const mockCallback1 = jest.fn();
+ const mockCallback2 = jest.fn();
+
+ toastEmitter.subscribe('testEvent1', mockCallback1);
+ toastEmitter.subscribe('testEvent2', mockCallback2);
+
+ toastEmitter.emit('testEvent1');
+ expect(mockCallback1).toHaveBeenCalled();
+ expect(mockCallback2).not.toHaveBeenCalled();
+ });
+
+ test('emits an undefined event', () => {
+ const mockCallback = jest.fn();
+ toastEmitter.subscribe('testEvent', mockCallback);
+
+ toastEmitter.emit('undefinedEvent');
+ expect(mockCallback).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/Toast/tests/Toast.test.jsx b/src/Toast/tests/Toast.test.jsx
new file mode 100644
index 0000000000..dac469a745
--- /dev/null
+++ b/src/Toast/tests/Toast.test.jsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import { render, act, screen } from '@testing-library/react';
+import { IntlProvider } from 'react-intl';
+import userEvent from '@testing-library/user-event';
+
+import ToastContainer from '..';
+import { toast } from '../utils';
+
+jest.useFakeTimers();
+
+function ToastWrapper(props) {
+ return (
+
+
+
+ );
+}
+
+describe('', () => {
+ const mockOnDismiss = jest.fn();
+ const props = {
+ onDismiss: mockOnDismiss,
+ message: 'Success message.',
+ duration: 5000,
+ };
+
+ it('renders Toasts when emitted', () => {
+ render();
+ act(() => {
+ toast({ message: 'Toast 1', duration: 5000 });
+ });
+ expect(screen.queryByText('Toast 1')).toBeInTheDocument();
+ });
+
+ it('removes Toasts after duration', () => {
+ render();
+ act(() => {
+ toast({ message: 'Toast 2', duration: 5000 });
+ jest.advanceTimersByTime(5000);
+ });
+ expect(screen.queryByText('Toast 2')).not.toBeInTheDocument();
+ });
+
+ it('renders multiple toasts', () => {
+ render();
+
+ act(() => {
+ toast({ message: 'Toast 1', duration: 5000 });
+ toast({ message: 'Toast 2', duration: 5000 });
+ });
+
+ expect(screen.queryByText('Toast 1')).toBeInTheDocument();
+ expect(screen.queryByText('Toast 2')).toBeInTheDocument();
+ });
+
+ it('renders optional action as button', () => {
+ render();
+ act(() => {
+ toast({
+ actions: [{
+ label: 'Optional action',
+ onClick: () => {},
+ }],
+ });
+ });
+
+ const toastButton = screen.getByRole('button', { name: 'Optional action' });
+ expect(toastButton).toBeInTheDocument();
+ });
+
+ it('pauses and resumes timer on hover', async () => {
+ render();
+ act(() => {
+ toast({ message: 'Hover Test', duration: 5000 });
+ });
+ const toastElement = screen.getByText('Hover Test');
+ await userEvent.hover(toastElement);
+ act(() => {
+ jest.advanceTimersByTime(3000);
+ });
+
+ expect(screen.queryByText('Hover Test')).toBeInTheDocument();
+
+ await userEvent.unhover(toastElement);
+ act(() => {
+ jest.advanceTimersByTime(5000);
+ });
+
+ expect(screen.queryByText('Hover Test')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/Toast/utils/EventEmitter.js b/src/Toast/utils/EventEmitter.js
new file mode 100644
index 0000000000..4639f50875
--- /dev/null
+++ b/src/Toast/utils/EventEmitter.js
@@ -0,0 +1,22 @@
+class EventEmitter {
+ constructor() {
+ this.events = {};
+ }
+
+ subscribe(event, callback) {
+ if (!this.events[event]) {
+ this.events[event] = [];
+ }
+ this.events[event].push(callback);
+ }
+
+ emit(event, data) {
+ const eventSubscribers = this.events[event];
+ if (eventSubscribers) {
+ eventSubscribers.forEach(callback => callback(data));
+ }
+ }
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export const toastEmitter = new EventEmitter();
diff --git a/src/Toast/utils/index.jsx b/src/Toast/utils/index.jsx
new file mode 100644
index 0000000000..abbbc7cbf2
--- /dev/null
+++ b/src/Toast/utils/index.jsx
@@ -0,0 +1,2 @@
+export { toast } from './toast';
+export { toastEmitter } from './EventEmitter';
diff --git a/src/Toast/utils/toast.js b/src/Toast/utils/toast.js
new file mode 100644
index 0000000000..b1118d41c5
--- /dev/null
+++ b/src/Toast/utils/toast.js
@@ -0,0 +1,27 @@
+import { toastEmitter } from './EventEmitter';
+/**
+ * Represents an action for a toast notification.
+ *
+ * @typedef {Object} ToastAction
+ * @property {string} [href] - If present, the action is rendered as an anchor tag (``).
+ * @property {Function} [onClick] - The function to call when the action is clicked.
+ * @property {string} label - The text label for the action.
+ */
+
+/**
+ * Displays a toast notification.
+ *
+ * @param {string} message - The message to be displayed in the toast.
+ * @param {number} duration - The duration for which the toast should be visible.
+ * @param {ToastAction[]} actions - An array of action objects for the toast.
+ * @param {string} position - The position where the toast will be displayed.
+*/
+
+// eslint-disable-next-line import/prefer-default-export
+export const toast = ({
+ message, duration, actions, position,
+}) => {
+ toastEmitter.emit('showToast', {
+ message, duration, actions, position,
+ });
+};
diff --git a/src/index.js b/src/index.js
index a25410d8f9..a7da756891 100644
--- a/src/index.js
+++ b/src/index.js
@@ -133,7 +133,7 @@ export {
TabPane,
} from './Tabs';
export { default as TextArea } from './TextArea';
-export { default as Toast, TOAST_CLOSE_LABEL_TEXT, TOAST_DELAY } from './Toast';
+export { default as ToastContainer, toast } from './Toast';
export { default as Tooltip } from './Tooltip';
export { default as ValidationFormGroup } from './ValidationFormGroup';
export { default as TransitionReplace } from './TransitionReplace';
diff --git a/src/index.scss b/src/index.scss
index 41a8e68e6c..147a01e3fd 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -46,7 +46,6 @@
@import "./IconButton";
@import "./IconButtonToggle";
@import "./Toast";
-@import "./Toast/ToastContainer";
@import "./SelectableBox";
@import "./ProductTour/Checkpoint";
@import "./Sticky";
diff --git a/www/gatsby-browser.jsx b/www/gatsby-browser.jsx
index 8b9fe8c574..ce53143da5 100644
--- a/www/gatsby-browser.jsx
+++ b/www/gatsby-browser.jsx
@@ -1,4 +1,5 @@
const React = require('react');
+const { ToastContainer } = require('~paragon-react');
const { SettingsContextProvider } = require('./src/context/SettingsContext');
const { InsightsContextProvider } = require('./src/context/InsightsContext');
@@ -7,6 +8,7 @@ exports.wrapRootElement = ({ element }) => (
{element}
+
);
diff --git a/www/src/components/CodeBlock.tsx b/www/src/components/CodeBlock.tsx
index d5b7460e1e..8be3735543 100644
--- a/www/src/components/CodeBlock.tsx
+++ b/www/src/components/CodeBlock.tsx
@@ -31,7 +31,7 @@ import HipsterIpsum from './exampleComponents/HipsterIpsum';
import ExamplePropsForm from './exampleComponents/ExamplePropsForm';
const {
- Collapsible, Toast, IconButton, Icon,
+ Collapsible, IconButton, Icon, toast,
} = ParagonReact;
export type CollapsibleLiveEditorTypes = {
@@ -125,14 +125,13 @@ function CodeBlock({
}: ICodeBlock) {
const intl = useIntl();
const language: any = className ? className.replace(/language-/, '') : 'jsx';
- const [showToast, setShowToast] = useState(false);
const [codeExample, setCodeExample] = useState(children);
const handleCodeChange = (e) => setCodeExample(e.target.value);
const handleCopyCodeExample = () => {
navigator.clipboard.writeText(codeExample);
- setShowToast(true);
+ toast({ message: 'Code example copied to clipboard!', duration: 2000 });
};
if (live) {
@@ -168,13 +167,6 @@ function CodeBlock({
- setShowToast(false)}
- show={showToast}
- delay={2000}
- >
- Code example copied to clipboard!
-
);
}
diff --git a/www/src/components/IconsTable.tsx b/www/src/components/IconsTable.tsx
index a95113b73d..a10e805bdd 100644
--- a/www/src/components/IconsTable.tsx
+++ b/www/src/components/IconsTable.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
-import { Icon, SearchField, Toast } from '~paragon-react';
+import { Icon, SearchField, toast } from '~paragon-react';
import * as IconComponents from '~paragon-icons';
import { ICON_COPIED_EVENT, sendUserAnalyticsEvent } from '../../segment-events';
@@ -67,7 +67,6 @@ function IconsTable({ iconNames }) {
const [tableWidth, setTableWidth] = useState(0);
const [data, setData] = useState({ iconsList: iconNames, rowsCount: ROWS_PER_WINDOW });
const [currentIcon, setCurrentIcon] = useState(iconNames[0]);
- const [showToast, setShowToast] = useState(false);
const currentIconImport = `import { ${currentIcon} } from '@openedx/paragon/icons';`;
const { rowsCount, iconsList } = data;
@@ -75,7 +74,7 @@ function IconsTable({ iconNames }) {
const copyToClipboard = (content) => {
navigator.clipboard.writeText(content);
- setShowToast(true);
+ toast({ message: 'Copied to clipboard!', duration: 2000 });
sendUserAnalyticsEvent(ICON_COPIED_EVENT, { name: currentIcon });
};
@@ -178,13 +177,6 @@ function IconsTable({ iconNames }) {
- setShowToast(false)}
- show={showToast}
- delay={2000}
- >
- Copied to clipboard!
-
>
);
}
diff --git a/www/src/pages/foundations/elevation.jsx b/www/src/pages/foundations/elevation.jsx
index 0b696ed65d..3242c1ec68 100644
--- a/www/src/pages/foundations/elevation.jsx
+++ b/www/src/pages/foundations/elevation.jsx
@@ -5,7 +5,7 @@ import {
Button,
Form,
Input,
- Toast,
+ toast,
Icon,
IconButtonWithTooltip,
} from '~paragon-react';
@@ -28,11 +28,9 @@ const controlsProps = [
];
function BoxShadowNode() {
- const [showToast, setShowToast] = useState(false);
-
const isBoxShadowCopied = (level, side) => {
navigator.clipboard.writeText(`@include pgn-box-shadow(${level}, "${side}");`);
- setShowToast(true);
+ toast({ message: 'Box-shadow copied to clipboard!', duration: 2000 });
};
const boxShadowCells = boxShadowLevels.map(level => (
@@ -56,14 +54,6 @@ function BoxShadowNode() {
return (
{ boxShadowCells }
- setShowToast(false)}
- show={showToast}
- delay={2000}
- >
- Box-shadow copied to clipboard!
-
);
}