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 - +- **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} - > -
-

{children}

-
- -
-
- {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! -
); }