diff --git a/packages/dify-ui/src/toast/__tests__/index.spec.tsx b/packages/dify-ui/src/toast/__tests__/index.spec.tsx index 0b06c6e1be8..7e9227e362e 100644 --- a/packages/dify-ui/src/toast/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/toast/__tests__/index.spec.tsx @@ -69,6 +69,28 @@ describe('@langgenius/dify-ui/toast', () => { dispatchToastMouseOut(viewport) }) + it('should clamp varying-height toasts to the frontmost stack height when collapsed', async () => { + const screen = await render() + + toast.info('Long background toast', { + description: 'This longer toast intentionally spans multiple lines so it would overflow the collapsed stack without matching the frontmost toast height.', + }) + toast.success('Short front toast', { + description: 'Short message.', + }) + + await expect.element(screen.getByText('Short front toast')).toBeInTheDocument() + await expect.element(screen.getByText('Long background toast')).toBeInTheDocument() + await expect.element(screen.getByRole('region', { name: 'Notifications' })).toHaveAttribute('aria-live', 'polite') + await expect.element(screen.getByRole('dialog', { name: 'Short front toast' })).toBeInTheDocument() + await expect.element(screen.getByRole('dialog', { name: 'Long background toast' })).toBeInTheDocument() + + const longToastContent = screen.getByText('Long background toast').element().closest('[class*="transition-opacity"]') + expect(longToastContent).toHaveAttribute('data-behind') + expect(longToastContent).toHaveClass('h-full') + expect(longToastContent?.parentElement).toHaveClass('h-full') + }) + it('should render a neutral toast when called directly', async () => { const screen = await render() diff --git a/packages/dify-ui/src/toast/index.stories.tsx b/packages/dify-ui/src/toast/index.stories.tsx index 772d0c456ce..b0ead00c9fd 100644 --- a/packages/dify-ui/src/toast/index.stories.tsx +++ b/packages/dify-ui/src/toast/index.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import type { ReactNode } from 'react' +import { useRef } from 'react' import { toast, ToastHost } from '.' const buttonClassName = 'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-2 text-sm text-text-secondary shadow-xs transition-colors hover:bg-state-base-hover' @@ -117,6 +118,15 @@ const StackExamples = () => { }) } + const createVaryingHeightStack = () => { + toast.info('Long background toast', { + description: 'This longer toast intentionally spans multiple lines so the collapsed stack can be checked against the shorter frontmost toast height without panel overflow.', + }) + toast.success('Short front toast', { + description: 'Short message.', + }) + } + return ( { + ) } @@ -192,11 +205,14 @@ const PromiseExamples = () => { const ActionExamples = () => { const createActionToast = () => { - toast.warning('Project archived', { + let archivedToastId = '' + archivedToastId = toast.warning('Project archived', { description: 'You can restore it from workspace settings for the next 30 days.', + timeout: 10000, actionProps: { children: 'Undo', onClick: () => { + toast.dismiss(archivedToastId) toast.success('Project restored', { description: 'The workspace is active again.', }) @@ -233,6 +249,32 @@ const ActionExamples = () => { ) } +const DeduplicateExamples = () => { + const saveCountRef = useRef(0) + + const saveDraft = () => { + saveCountRef.current += 1 + toast.success('Draft saved', { + id: 'draft-save-status', + description: saveCountRef.current === 1 + ? 'Click again while this toast is visible to update the same mounted toast.' + : `Same toast updated ${saveCountRef.current} times.`, + }) + } + + return ( + + + + ) +} + const UpdateExamples = () => { const createUpdatableToast = () => { const toastId = toast.info('Import started', { @@ -292,6 +334,7 @@ const ToastDocsDemo = () => { + diff --git a/packages/dify-ui/src/toast/index.tsx b/packages/dify-ui/src/toast/index.tsx index 269e18fa658..cab9841a327 100644 --- a/packages/dify-ui/src/toast/index.tsx +++ b/packages/dify-ui/src/toast/index.tsx @@ -166,12 +166,12 @@ function ToastCard({ 'after:pointer-events-auto after:absolute after:top-full after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full after:content-[\'\']', )} > -
+