diff --git a/src/Toast/EventEmitter.js b/src/Toast/EventEmitter.js new file mode 100644 index 0000000000..4639f50875 --- /dev/null +++ b/src/Toast/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/README.md b/src/Toast/README.md index 9b36e17520..4ac7129133 100644 --- a/src/Toast/README.md +++ b/src/Toast/README.md @@ -2,7 +2,8 @@ title: 'Toast' type: 'component' components: -- Toast +- ToastContainer +- toast categories: - Overlays status: 'New' @@ -11,85 +12,88 @@ devStatus: 'Done' notes: '' --- -``Toast`` is a pop-up style message that shows the user a brief, fleeting, dismissible message about a successful app process. +`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. +## Features + +- **Customizable Appearance**: Choose the window position for toast. +- **Interactive**: Includes a close button for manual dismissal. +- **Auto-dismiss**: Disappears automatically after a set duration. +- **Hover Interactivity**: Auto-dismiss timer pauses on hover or focus, allowing users to interact with the content. ## Behaviors - +- 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); + const [position, setPosition] = useState('bottom-left'); + const [timer, setTimer] = useState(5000); + const [message, setMessage] = useState('Example of a basic Toast.'); + const [actions, setActions] = useState([]); - return ( - <> - console.log('You clicked the action button.') - }} - onClose={() => setShow(false)} - show={show} - > - Success! Example of a Toast with a button. - - - - - ); -} -``` - -## 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. - - - +
+ Message: + setMessage(e.target.value)} + /> +
+ +
+ Duration (ms): + setTimer(Number(e.target.value))} /> +
+ +
+ Position: + setPosition(e.target.value)} + > + + + + + +
+ +
+ Add and remove actions: + +

Total added: {actions.length}

+ + + + + +
+ + + + + ); } 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 index 05049ae0f4..915a4e8b01 100644 --- a/src/Toast/ToastContainer.jsx +++ b/src/Toast/ToastContainer.jsx @@ -1,40 +1,82 @@ -import React from 'react'; +/* eslint-disable react/prop-types */ +import React, { useState, useEffect, useRef } 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); - } +import { toastEmitter } from './EventEmitter'; +import Toast from '.'; + +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', + }, +}; + +function ToastContainer({ position, className }) { + const [toasts, setToasts] = useState([]); + const portalDivRef = useRef(null); + const positionStyle = positionStyles[position] || positionStyles['bottom-left']; + + if (!portalDivRef.current) { + portalDivRef.current = document.createElement('div'); + portalDivRef.current.setAttribute('class', 'toast-portal'); + portalDivRef.current.setAttribute('role', 'alert'); + portalDivRef.current.setAttribute('aria-live', 'polite'); + portalDivRef.current.setAttribute('aria-atomic', 'true'); + document.body.appendChild(portalDivRef.current); } - render() { - if (this.rootElement) { - return ReactDOM.createPortal( - this.props.children, - this.rootElement, + const removeToast = (id) => { + setToasts(currentToasts => currentToasts.filter(toast => toast.id !== id)); + }; + + useEffect(() => { + const handleShowToast = ({ message, duration, actions }) => { + const id = Date.now(); + setToasts(currentToasts => [...currentToasts, { + id, message, duration, actions, + }]); + }; + + toastEmitter.subscribe('showToast', handleShowToast); + + return () => { + toastEmitter.events.showToast = toastEmitter.events.showToast.filter( + callback => callback !== handleShowToast, ); - } - return null; - } + if (portalDivRef.current) { + document.body.removeChild(portalDivRef.current); + } + }; + }, []); + + return portalDivRef.current ? ReactDOM.createPortal( +
+ {toasts.map(toast => ( + removeToast(toast.id)} className={className} /> + ))} +
, + portalDivRef.current, + ) : null; } +export default ToastContainer; + ToastContainer.propTypes = { - /** Specifies contents of the component. */ - children: PropTypes.node.isRequired, + position: PropTypes.oneOf(['top-left', 'top-right', 'bottom-left', 'bottom-right']), + className: PropTypes.string, }; -export default ToastContainer; +ToastContainer.defaultProps = { + position: 'bottom-left', + className: '', +}; 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/index.jsx b/src/Toast/index.jsx index 11461666c2..a5162393c4 100644 --- a/src/Toast/index.jsx +++ b/src/Toast/index.jsx @@ -1,111 +1,104 @@ -import React, { useState } from 'react'; -import classNames from 'classnames'; +/* eslint-disable react/prop-types */ +import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; - -import BaseToast from 'react-bootstrap/Toast'; import { useIntl } from 'react-intl'; +import classNames from 'classnames'; -import { Close } from '../../icons'; -import ToastContainer from './ToastContainer'; -import Button from '../Button'; import Icon from '../Icon'; import IconButton from '../IconButton'; - -export const TOAST_CLOSE_LABEL_TEXT = 'Close'; -export const TOAST_DELAY = 5000; +import Button from '../Button'; +import { Close } from '../../icons'; function Toast({ - action, children, className, closeLabel, onClose, show, ...rest + id, message, onDismiss, actions, className, duration, ...rest }) { const intl = useIntl(); - const [autoHide, setAutoHide] = useState(true); - const intlCloseLabel = closeLabel || intl.formatMessage({ + 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 ( - - setAutoHide(true)} - onFocus={() => setAutoHide(false)} - onMouseOut={() => setAutoHide(true)} - onMouseOver={() => setAutoHide(false)} - show={show} - {...rest} - > -
-

{children}

-
- +
+
+

{message}

+ + onDismiss(id)} + variant="primary" + invertColors + /> +
+ {actions + ? ( +
+ {actions.map((action) => ( + + ))}
-
- {action && ( - - )} - - + ) + : null} +
); } -Toast.defaultProps = { - action: null, - closeLabel: undefined, - delay: TOAST_DELAY, - className: undefined, -}; +export default Toast; 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 */ + 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, }; -export default Toast; +Toast.defaultProps = { + onDismiss: () => {}, + actions: null, + className: '', + duration: 5000, +}; diff --git a/src/Toast/index.scss b/src/Toast/index.scss index 58658f0e9c..b114cfc349 100644 --- a/src/Toast/index.scss +++ b/src/Toast/index.scss @@ -1,5 +1,4 @@ @import "variables"; -@import "~bootstrap/scss/toasts"; .toast { background-color: $toast-background-color; @@ -8,33 +7,27 @@ padding: 1rem; 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; - } + z-index: 1000; + display: flex; + flex-direction: column; .btn { margin-top: .75rem; align-self: flex-start; } - .toast-header { + .toast__header { + display: flex; align-items: center; border-bottom: 0; justify-content: space-between; padding: 0; - p { + .toast__message { font-size: $small-font-size; margin: 0; padding-right: .75rem; + color: $toast-header-color; } & + .btn { @@ -42,6 +35,11 @@ } } + .toast__optional-actions { + display: flex; + flex-wrap: wrap; + } + @media only screen and (max-width: 768px) { max-width: 100%; } @@ -51,3 +49,11 @@ max-width: $toast-max-width; } } + +.toast-container { + display: flex; + flex-direction: column; + gap: .5rem; + 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..2e21ac5646 --- /dev/null +++ b/src/Toast/tests/EventEmitter.test.js @@ -0,0 +1,59 @@ +import { toastEmitter } from '../EventEmitter'; + +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..f8a7671663 --- /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 '../ToastContainer'; +import { toast } from '../toast'; + +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/toast.js b/src/Toast/toast.js new file mode 100644 index 0000000000..0d525461c7 --- /dev/null +++ b/src/Toast/toast.js @@ -0,0 +1,6 @@ +import { toastEmitter } from './EventEmitter'; + +// eslint-disable-next-line import/prefer-default-export +export const toast = ({ message, duration, actions }) => { + toastEmitter.emit('showToast', { message, duration, actions }); +}; diff --git a/src/ToastNew/README.md b/src/ToastNew/README.md deleted file mode 100644 index 9b36e17520..0000000000 --- a/src/ToastNew/README.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: 'Toast' -type: 'component' -components: -- Toast -categories: -- Overlays -status: 'New' -designStatus: 'Done' -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. - -## 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.
  • -
- -## 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. - - - - - ); -} -``` - -## With Link - -```jsx live -() => { - const [show, setShow] = useState(false); - - return ( - <> - setShow(false)} - show={show} - > - Success! Example of a Toast with a link. - - - - - ); -} -``` diff --git a/src/ToastNew/Toast.jsx b/src/ToastNew/Toast.jsx deleted file mode 100644 index 76117427d7..0000000000 --- a/src/ToastNew/Toast.jsx +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, { useEffect } from 'react'; -import { useToast } from './ToastContext'; - -function Toast({ id, content, options }) { - const { removeToast } = useToast(); - - useEffect(() => { - const timer = setTimeout(() => { - removeToast(id); - }, options.duration || 3000); - - return () => clearTimeout(timer); - }, [id, options.duration, removeToast]); - - return ( -
- {content} - -
- ); -} - -export const ToastFunction = () => { - const { addToast } = useToast(); - - return (content, options) => { - addToast(content, options); - }; -}; - -export default Toast; diff --git a/src/ToastNew/ToastContainer.jsx b/src/ToastNew/ToastContainer.jsx deleted file mode 100644 index 82773d7427..0000000000 --- a/src/ToastNew/ToastContainer.jsx +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from 'react'; -import { ToastProvider, useToast } from './ToastContext'; -import Toast from './Toast'; - -function ToastContainer({ config }) { - const { toasts } = useToast(); - - return ( - -
- {toasts.map((toast) => ( - - ))} -
-
- ); -} - -export default ToastContainer; diff --git a/src/ToastNew/ToastContext.jsx b/src/ToastNew/ToastContext.jsx deleted file mode 100644 index 82cd889a5a..0000000000 --- a/src/ToastNew/ToastContext.jsx +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, { createContext, useReducer, useContext } from 'react'; - -const ToastContext = createContext(); - -const initialState = { - toasts: [], -}; - -const reducer = (state, action) => { - switch (action.type) { - case 'ADD_TOAST': - return { ...state, toasts: [...state.toasts, action.payload] }; - case 'REMOVE_TOAST': - return { ...state, toasts: state.toasts.filter((toast) => toast.id !== action.payload) }; - default: - return state; - } -}; - -function ToastProvider({ children }) { - const [state, dispatch] = useReducer(reducer, initialState); - - const addToast = (content, options = {}) => { - const id = Date.now(); - const toast = { id, content, options }; - dispatch({ type: 'ADD_TOAST', payload: toast }); - }; - - const removeToast = (id) => { - dispatch({ type: 'REMOVE_TOAST', payload: id }); - }; - - return ( - - {children} - - ); -} - -const useToast = () => { - const context = useContext(ToastContext); - if (!context) { - throw new Error('useToast must be used within a ToastProvider'); - } - return context; -}; - -export { ToastProvider, useToast }; diff --git a/src/index.js b/src/index.js index 13ee0c82aa..7eb3625c49 100644 --- a/src/index.js +++ b/src/index.js @@ -133,9 +133,8 @@ 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 } from './ToastNew/ToastContainer'; -export { ToastProvider } from './ToastNew/ToastContext'; +export { default as ToastContainer } from './Toast/ToastContainer'; +export { toast } from './Toast/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";