diff --git a/fixtures/page_objects/dashboard_detail.py b/fixtures/page_objects/dashboard_detail.py index 2393667bcd61..22651bdfad86 100644 --- a/fixtures/page_objects/dashboard_detail.py +++ b/fixtures/page_objects/dashboard_detail.py @@ -83,7 +83,7 @@ def save_dashboard(self): # until the API call finishes. Since the loading indicator isn't used # we can't rely on self.wait_until_loaded(). The UI shows a # success toast, however if a previous step of a test shows a success - # toast, a wait_until([data-test-id="toast-success"]) will return + # toast, a wait for the status role will return # immediately due to the previous toast still being in the DOM. # Since clicking the save dasboard button is removed once the API # call is complete, we can wait for that as a signal diff --git a/fixtures/page_objects/issue_list.py b/fixtures/page_objects/issue_list.py index b53e56983666..45985cafb530 100644 --- a/fixtures/page_objects/issue_list.py +++ b/fixtures/page_objects/issue_list.py @@ -30,8 +30,9 @@ def resolve_issues(self): self.browser.click('[aria-label="Resolve"]') def wait_for_issue_removal(self): - self.browser.click_when_visible('[data-test-id="toast-success"]') - self.browser.wait_until_not('[data-test-id="toast-success"]') + toast_selector = '[role="status"]' + self.browser.click_when_visible(f'{toast_selector} [aria-label="Dismiss"]') + self.browser.wait_until_not(toast_selector) def wait_for_issue(self): self.browser.wait_until('[data-test-id="group"]') diff --git a/package.json b/package.json index 5eed5e53f3c9..2bce0d99d89e 100644 --- a/package.json +++ b/package.json @@ -212,6 +212,7 @@ "remark-gfm": "^4.0.1", "remark-mdx-frontmatter": "^5.2.0", "screenfull": "^6.0.2", + "sonner": "2.0.8", "sprintf-js": "1.0.3", "style-loader": "4.0.0", "swc-plugin-component-annotate": "1.18.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8647328bf056..7000e1ca753c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -500,6 +500,9 @@ importers: screenfull: specifier: ^6.0.2 version: 6.0.2 + sonner: + specifier: 2.0.8 + version: 2.0.8(@types/react@19.2.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) sprintf-js: specifier: 1.0.3 version: 1.0.3 @@ -8110,6 +8113,16 @@ packages: solid-js@1.9.14: resolution: {integrity: sha512-sAEXC0Kk0S1EDg+8ysEWJDbYhA3RRoEjwuySUGlKIemeo0I5YZfOyumNjNs9Sv3y2nmhD+0rW66ag2HsMuQiGQ==} + sonner@2.0.8: + resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + source-map-generator@2.0.2: resolution: {integrity: sha512-unCl5BQhF/us51DiT7SvlSY3QUPhyfAdHJxd8l7FXdwzqxli0UDMV2dEuei2SeGp3Z4rB/AJ9zKi1mGOp2K2ww==} engines: {node: '>=20'} @@ -17370,6 +17383,13 @@ snapshots: seroval: 1.5.6 seroval-plugins: 1.5.6(seroval@1.5.6) + sonner@2.0.8(@types/react@19.2.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.1 + source-map-generator@2.0.2: {} source-map-js@1.2.1: {} diff --git a/static/app/actionCreators/indicator.tsx b/static/app/actionCreators/indicator.tsx index ad6e30ad4d16..608201360450 100644 --- a/static/app/actionCreators/indicator.tsx +++ b/static/app/actionCreators/indicator.tsx @@ -1,10 +1,10 @@ import {isValidElement} from 'react'; import * as Sentry from '@sentry/react'; -import type {FormModel} from 'sentry/components/forms/model'; -import {DEFAULT_TOAST_DURATION} from 'sentry/constants'; +import {toast, type ToastOptions} from '@sentry/scraps/toast'; + +import {IconRefresh} from 'sentry/icons'; import {t} from 'sentry/locale'; -import {IndicatorStore} from 'sentry/stores/indicatorStore'; import {isDemoModeActive} from 'sentry/utils/demoMode'; type IndicatorType = 'loading' | 'error' | 'success' | 'undo' | ''; @@ -18,27 +18,17 @@ interface IndicatorOptions { type UndoIndicatorOptions = IndicatorOptions & {undo: () => void}; -interface UndoableIndicatorOptions extends IndicatorOptions { - formModel: { - id: string; - model: FormModel; - }; -} - -export type Indicator = { - id: string | number; - message: React.ReactNode; - options: IndicatorOptions; - type: IndicatorType; - clearId?: null | number; -}; - // Clears all indicators +/** + * @deprecated Use `toast.dismiss()` from `@sentry/scraps/toast` instead. + */ export function clearIndicators() { - IndicatorStore.clear(); + toast.dismiss(); } -// Note previous IndicatorStore.add behavior was to default to "loading" if no type was supplied +/** + * @deprecated Use the namespaced API from `@sentry/scraps/toast` instead. + */ export function addMessage( msg: React.ReactNode, type: 'undo', @@ -54,7 +44,7 @@ export function addMessage( type: IndicatorType, options: IndicatorOptions = {} ): void { - const {duration: optionsDuration, append, ...rest} = options; + const {duration: optionsDuration, disableDismiss, undo} = options; // XXX: Debug for https://sentry.io/organizations/sentry/issues/1595204979/ if ( @@ -70,17 +60,43 @@ export function addMessage( ); } - // use default only if undefined, as 0 is a valid duration - const duration = - optionsDuration === undefined ? DEFAULT_TOAST_DURATION : optionsDuration; + const toastOptions: ToastOptions = { + dismissible: disableDismiss !== true, + }; - const action = append ? 'append' : 'add'; - // XXX: This differs from `IndicatorStore.add` since it won't return the indicator that is created - // because we are firing an action. You can just add a new message and it will, by default, - // replace active indicator - IndicatorStore[action](msg, type, {...rest, duration}); + if (optionsDuration !== undefined) { + toastOptions.duration = + optionsDuration === null || optionsDuration === 0 ? Infinity : optionsDuration; + } + + if (typeof undo === 'function') { + toastOptions.action = { + label: t('Undo'), + icon: , + onClick: undo, + }; + } + + switch (type) { + case 'loading': + toast.loading(msg, toastOptions); + break; + case 'error': + toast.error(msg, toastOptions); + break; + case 'success': + toast.success(msg, toastOptions); + break; + case 'undo': + case '': + toast.message(msg, toastOptions); + break; + } } +/** + * @deprecated Use `toast.loading()` from `@sentry/scraps/toast` instead. + */ export function addLoadingMessage( msg: React.ReactNode = t('Saving changes...'), options?: IndicatorOptions @@ -88,6 +104,9 @@ export function addLoadingMessage( return addMessage(msg, 'loading', options); } +/** + * @deprecated Use `toast.error()` from `@sentry/scraps/toast` instead. + */ export function addErrorMessage(msg: React.ReactNode, options?: IndicatorOptions) { if (isDemoModeActive()) { return addMessage(t('This action is not allowed in demo mode.'), 'error', options); @@ -108,9 +127,12 @@ export function addErrorMessage(msg: React.ReactNode, options?: IndicatorOptions ); } +/** + * @deprecated Use `toast.success()` from `@sentry/scraps/toast` instead. + */ export function addSuccessMessage( msg: React.ReactNode, - options?: IndicatorOptions | UndoableIndicatorOptions + options?: IndicatorOptions | UndoIndicatorOptions ) { return addMessage(msg, 'success', options); } diff --git a/static/app/bootstrap/processInitQueue.spec.tsx b/static/app/bootstrap/processInitQueue.spec.tsx index b0c1d7c500cb..28bd08a14132 100644 --- a/static/app/bootstrap/processInitQueue.spec.tsx +++ b/static/app/bootstrap/processInitQueue.spec.tsx @@ -10,7 +10,6 @@ import {TeamFixture} from 'sentry-fixture/team'; import {screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import {processInitQueue} from 'sentry/bootstrap/processInitQueue'; -import {IndicatorStore} from 'sentry/stores/indicatorStore'; import {SentryInitRenderReactComponent} from 'sentry/types/system'; describe('processInitQueue', () => { @@ -45,21 +44,6 @@ describe('processInitQueue', () => { expect(await screen.findByText('Very Strong')).toBeInTheDocument(); }); - it('renders indicators', async () => { - window.__onSentryInit = [ - { - component: SentryInitRenderReactComponent.INDICATORS, - container: '#indicator-container', - name: 'renderReact', - }, - ]; - - IndicatorStore.add('Indicator Alert', 'success'); - - render(
); - processInitQueue(); - expect(await screen.findByText('Indicator Alert')).toBeInTheDocument(); - }); it('renders setup wizard', async () => { window.__onSentryInit = [ { diff --git a/static/app/bootstrap/processInitQueue.tsx b/static/app/bootstrap/processInitQueue.tsx index ddf4fd4fe084..12baf1e74547 100644 --- a/static/app/bootstrap/processInitQueue.tsx +++ b/static/app/bootstrap/processInitQueue.tsx @@ -19,8 +19,6 @@ import {renderOnDomReady} from './renderOnDomReady'; const queryClient = new QueryClient(DEFAULT_QUERY_CLIENT_CONFIG); const COMPONENT_MAP = { - [SentryInitRenderReactComponent.INDICATORS]: () => - import(/* webpackChunkName: "Indicators" */ 'sentry/components/indicators'), [SentryInitRenderReactComponent.SETUP_WIZARD]: () => import(/* webpackChunkName: "SetupWizard" */ 'sentry/views/setupWizard'), [SentryInitRenderReactComponent.WEB_AUTHN_ASSSERT]: () => diff --git a/static/app/components/core/toast/index.tsx b/static/app/components/core/toast/index.tsx index 629e36e24415..f10e862da6ea 100644 --- a/static/app/components/core/toast/index.tsx +++ b/static/app/components/core/toast/index.tsx @@ -1 +1,4 @@ -export {Toast} from './toast'; +export {toast} from './toastApi'; +export {ToastProvider} from './toaster'; +export {DEFAULT_TOAST_DURATION} from './types'; +export type {ToastAction, ToastOptions, ToastVariant} from './types'; diff --git a/static/app/components/core/toast/toast.mdx b/static/app/components/core/toast/toast.mdx index 1a19e7a4a865..31a858106289 100644 --- a/static/app/components/core/toast/toast.mdx +++ b/static/app/components/core/toast/toast.mdx @@ -1,6 +1,6 @@ --- title: Toast -description: Temporary notification messages that provide feedback for user actions, with support for success, error, loading, and undoable states. +description: Temporary notifications that provide feedback for user actions. category: status source: '@sentry/scraps/toast' resources: @@ -11,270 +11,119 @@ resources: WAI-ARIA Alert: https://www.w3.org/WAI/ARIA/apg/patterns/alert/ --- -import {Toast} from '@sentry/scraps/toast'; +import {Button} from '@sentry/scraps/button'; +import {Flex} from '@sentry/scraps/layout'; +import {toast} from '@sentry/scraps/toast'; +import {IconRefresh} from 'sentry/icons'; import * as Storybook from 'sentry/stories'; export const documentation = import('!!type-loader!@sentry/scraps/toast'); -`` is a notification component that displays temporary messages to provide feedback for user actions. Toasts appear briefly to confirm actions, report errors, show loading states, or offer undo functionality. +Use the namespaced `toast` API to show temporary notifications. The shared app provider renders the +toaster, so callers only need to invoke a method. -In practice, you won't typically render `` components directly. Instead, use the action creators `addSuccessMessage` and `addErrorMessage` from `static/app/actionCreators/indicator.tsx` to display toasts. - -```jsx -import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; - -// Show success toast -addSuccessMessage('Project settings saved successfully'); + + + + + + + + -// Show error toast -addErrorMessage('Failed to save settings. Please try again.'); +```tsx +toast.success('Changes saved'); +toast.error('Could not save changes'); +toast.loading('Uploading file...', {duration: 2000}); +toast.message('This is a notification'); ``` -## Toast Types +## Variants -Toasts come in four types, each serving a different purpose: +The API provides four variants: -- **success**: Confirms successful operations (green checkmark) -- **error**: Reports errors or failures (red error icon) -- **loading**: Indicates ongoing operations (spinner) -- **undo**: Provides an undo action for reversible operations - - - {}} - /> - {}} - /> - {}} - /> - {}} - /> - +- `success` confirms a completed action. +- `error` reports a failed action. +- `loading` indicates work in progress. +- `message` displays a neutral notification. -```jsx -// Success -addSuccessMessage('Settings saved'); +Toasts use the current visual theme and keep the existing success, error, and loading icons. -// Error -addErrorMessage('Failed to load data'); +## Options -// Loading -addLoadingMessage('Uploading file...'); +Pass a `ToastOptions` object as the second argument: -// Undo -addSuccessMessage('Item deleted', {undo: handleUndo}); +```tsx +toast.error('Could not save changes', { + duration: 10000, + dismissible: false, +}); ``` -## Undoable Actions +- `duration` is in milliseconds. It defaults to 6000. Use `Infinity` for a persistent toast. +- `dismissible` defaults to `true`. When `false`, the toast has no close button and ignores swipes. +- `id` updates an existing toast with the same id instead of adding another toast. +- `onDismiss` runs when Sonner dismisses the toast. + +## Actions -For reversible actions, provide an `undo` callback in the options. This displays an "Undo" button in the toast. +Use `action` for a button next to the message. The toast closes after the action runs. - - {}, - }, - }} - onDismiss={() => {}} - /> - {}, + -```jsx -addSuccessMessage('Project deleted', { - undo: () => { - restoreProject(projectId); +```tsx +toast.success('Item deleted', { + action: { + label: 'Undo', + icon: , + onClick: () => toast.message('Item restored'), }, }); ``` -## Dismissible - -All toasts are dismissible by default—users can click the "×" button to close them. Toasts also auto-dismiss after a few seconds (except loading toasts). - -```jsx -addSuccessMessage('Your changes have been saved'); -// Toast appears and auto-dismisses after ~4 seconds -``` - -## Duration - -Control how long toasts remain visible using the `duration` option: - -```jsx -// Short duration (2 seconds) -addSuccessMessage('Copied to clipboard', {duration: 2000}); - -// Longer duration (10 seconds) -addErrorMessage('Important error message', {duration: 10000}); - -// Persistent (no auto-dismiss) -addLoadingMessage('Processing large file...', {duration: Infinity}); -``` - -## Usage Patterns - -### Action Confirmations - -Confirm successful operations: - -```jsx -const handleSave = async () => { - try { - await saveProject(data); - addSuccessMessage('Project saved successfully'); - } catch (error) { - addErrorMessage('Failed to save project'); - } -}; -``` - -### Error Reporting +## Dismissing toasts -Inform users of errors with actionable guidance: +Toasts dismiss automatically after their duration. Users can also click the `Dismiss` button or swipe +a dismissible toast. Toasts of the same variant stack up to three at once; showing a different variant +dismisses the previous variant's active toasts. -```jsx -try { - await uploadFile(file); - addSuccessMessage(`${file.name} uploaded`); -} catch (error) { - addErrorMessage(`Failed to upload ${file.name}. File may be too large.`); -} -``` - -### Deletions with Undo - -Allow users to undo destructive actions: - -```jsx -const handleDelete = async id => { - const backup = items.find(item => item.id === id); - await deleteItem(id); +```tsx +// Dismiss one toast by id +const id = toast.loading('Working...', {duration: Infinity}); +toast.dismiss(id); - addSuccessMessage('Item deleted', { - undo: async () => { - await restoreItem(backup); - addSuccessMessage('Item restored'); - }, - }); -}; +// Dismiss every toast +toast.dismiss(); ``` -### Loading States - -Show progress for long-running operations: - -```jsx -const handleExport = async () => { - addLoadingMessage('Generating export...'); - - try { - await generateExport(); - addSuccessMessage('Export complete'); - } catch (error) { - addErrorMessage('Export failed'); - } -}; -``` - -## Best Practices - -**Message Content** - -- Keep messages concise (1-2 sentences maximum) -- Use clear, specific language -- Avoid technical jargon -- Include actionable next steps for errors - -**Frequency** - -- Don't overwhelm users with too many toasts -- Batch similar notifications when possible -- Use toasts for important feedback, not every minor action - -**Timing** - -- Success toasts: 3-4 seconds (default) -- Error toasts: 6-8 seconds (users need time to read) -- Loading toasts: Until operation completes -- Undo toasts: 5-8 seconds (give time to react) - ## Accessibility -Toasts use the [WAI-ARIA Alert pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alert/) for announcing messages to screen readers: - -- [2.2.4 Interruptions](https://www.w3.org/TR/WCAG22/#interruptions): Toasts can be dismissed and auto-dismiss -- Screen readers announce toast content when it appears -- Undo buttons are keyboard accessible - -### Developer Responsibilities - -**Message Clarity** - -- Write clear, specific messages -- Ensure messages make sense when read aloud -- Avoid relying solely on icons to convey meaning - -**Timing** - -- Allow sufficient time for users to read and react -- Don't auto-dismiss error messages too quickly -- Give adequate time to click undo buttons - -**Frequency Control** - -- Avoid showing multiple toasts simultaneously -- Queue or batch notifications when appropriate -- Don't interrupt critical user workflows with non-urgent toasts - -**Undo Actions** +The toaster uses a polite live region to announce new notifications. Error toasts use the `alert` +role. Other toasts use the `status` role. -- Ensure undo callbacks work correctly -- Provide feedback when undo completes -- Handle errors in undo operations +### Developer responsibilities -For more information, see the [WAI-ARIA Alert practices](https://www.w3.org/WAI/ARIA/apg/patterns/alert/). +- Write short, clear messages that make sense when read aloud. +- Do not use only an icon or color to communicate the result. +- Give users enough time to read the message and use its action. Use `duration: Infinity` when the + toast must stay visible. +- Avoid sending many toasts at the same time. Group repeated notifications when possible. +- Give each action a clear label and show feedback after the action runs. +- Use persistent inline feedback for critical information that users must review. diff --git a/static/app/components/core/toast/toast.spec.tsx b/static/app/components/core/toast/toast.spec.tsx new file mode 100644 index 000000000000..e0d42a33f23f --- /dev/null +++ b/static/app/components/core/toast/toast.spec.tsx @@ -0,0 +1,136 @@ +import { + act, + render, + screen, + userEvent, + waitFor, + waitForElementToBeRemoved, +} from 'sentry-test/reactTestingLibrary'; + +import {toast} from '@sentry/scraps/toast'; + +describe('Toast', () => { + it.each([ + ['success', 'status', 'Success', () => toast.success('Success')], + ['error', 'alert', 'Error', () => toast.error('Error')], + ['loading', 'status', 'Loading', () => toast.loading('Loading')], + ['default', 'status', 'Message', () => toast.message('Message')], + ] as const)('renders the %s variant', async (_variant, role, message, showToast) => { + render(
); + + act(() => void showToast()); + + expect(await screen.findByRole(role)).toHaveTextContent(message); + }); + + it('does not dismiss when the toast body is clicked', async () => { + render(
); + act(() => void toast.message('Dismiss me', {duration: Infinity})); + + const toastElement = await screen.findByRole('status'); + await userEvent.click(toastElement); + + expect(toastElement).toBeInTheDocument(); + }); + + it('dismisses when the close button is clicked', async () => { + render(
); + act(() => void toast.message('Dismiss me', {duration: Infinity})); + + await userEvent.click(await screen.findByRole('button', {name: 'Dismiss'})); + + await waitForElementToBeRemoved(() => screen.queryByRole('status')); + }); + + it('does not dismiss when dismissible is false', async () => { + render(
); + act(() => void toast.message('Keep me', {duration: Infinity, dismissible: false})); + + const toastElement = await screen.findByRole('status'); + await userEvent.click(toastElement); + + expect(toastElement).toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Dismiss'})).not.toBeInTheDocument(); + }); + + it('dismisses all toasts', async () => { + render(
); + act(() => { + toast.error('First error', {duration: Infinity}); + toast.error('Second error', {duration: Infinity}); + toast.error('Third error', {duration: Infinity}); + }); + + expect(await screen.findByText('First error')).toBeInTheDocument(); + expect(screen.getByText('Second error')).toBeInTheDocument(); + expect(screen.getByText('Third error')).toBeInTheDocument(); + + act(() => void toast.dismiss()); + + await waitFor(() => { + expect(screen.queryByText('First error')).not.toBeInTheDocument(); + expect(screen.queryByText('Second error')).not.toBeInTheDocument(); + expect(screen.queryByText('Third error')).not.toBeInTheDocument(); + }); + }); + + it('dismisses automatically after the configured duration', async () => { + jest.useFakeTimers(); + + try { + render(
); + act(() => void toast.message('Temporary', {duration: 1000})); + expect(await screen.findByRole('status')).toHaveTextContent('Temporary'); + + act(() => jest.advanceTimersByTime(1000)); + act(() => jest.runAllTimers()); + + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + } finally { + jest.useRealTimers(); + } + }); + + it('stacks toasts of the same variant', async () => { + render(
); + act(() => { + toast.error('First error', {duration: Infinity}); + toast.error('Second error', {duration: Infinity}); + toast.error('Third error', {duration: Infinity}); + }); + + expect(await screen.findByText('First error')).toBeInTheDocument(); + expect(screen.getByText('Second error')).toBeInTheDocument(); + expect(screen.getByText('Third error')).toBeInTheDocument(); + expect(screen.getAllByRole('alert')).toHaveLength(3); + }); + + it('dismisses toasts when the variant changes', async () => { + render(
); + act(() => void toast.loading('Loading', {duration: Infinity})); + + expect(await screen.findByText('Loading')).toBeInTheDocument(); + + act(() => void toast.success('Success', {duration: Infinity})); + + await waitForElementToBeRemoved(() => screen.queryByText('Loading')); + expect(screen.getByText('Success')).toBeInTheDocument(); + }); + + it('runs an action and dismisses the toast', async () => { + const onClick = jest.fn(); + render(
); + act( + () => + void toast.message('Undoable', { + duration: Infinity, + action: {label: 'Undo', onClick}, + }) + ); + + await userEvent.click(await screen.findByRole('button', {name: 'Undo'})); + + expect(onClick).toHaveBeenCalledTimes(1); + await waitForElementToBeRemoved(() => screen.queryByText('Undoable')); + }); +}); diff --git a/static/app/components/core/toast/toast.tsx b/static/app/components/core/toast/toast.tsx index ac1ddc72e37c..56fabe2eb613 100644 --- a/static/app/components/core/toast/toast.tsx +++ b/static/app/components/core/toast/toast.tsx @@ -1,100 +1,93 @@ import styled from '@emotion/styled'; -import * as Sentry from '@sentry/react'; -import classNames from 'classnames'; -import {motion, type HTMLMotionProps, type Transition} from 'framer-motion'; import {Button} from '@sentry/scraps/button'; import {Container, Flex} from '@sentry/scraps/layout'; import {useTranslation} from '@sentry/scraps/translationContext'; -import type {Indicator} from 'sentry/actionCreators/indicator'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {TextOverflow} from 'sentry/components/textOverflow'; -import {IconCheckmark, IconRefresh, IconWarning} from 'sentry/icons'; +import {IconCheckmark, IconClose, IconWarning} from 'sentry/icons'; import type {Theme} from 'sentry/utils/theme'; +import {unreachable} from 'sentry/utils/unreachable'; + +import type {ToastAction, ToastVariant} from './types'; interface ToastProps { - indicator: Indicator; - onDismiss: (indicator: Indicator, event: React.MouseEvent) => void; + message: React.ReactNode; + variant: ToastVariant; + action?: ToastAction; + onDismiss?: () => void; } -export function Toast({indicator, onDismiss, ...props}: ToastProps) { +export function Toast({message, variant, action, onDismiss}: ToastProps) { const {t} = useTranslation(); return ( - onDismiss(indicator, e) - } - data-test-id={indicator.type ? `toast-${indicator.type}` : 'toast'} - className={classNames('ref-toast', `ref-${indicator.type}`)} - type={indicator.type} - {...TOAST_TRANSITION} - {...props} - > - + + - {indicator.message} + {message} - {indicator.options.undo && typeof indicator.options.undo === 'function' ? ( + {action ? ( ) : null} + {onDismiss ? ( + +