From e5d5931fec82ce9422638dd9d6a3b62733097dfc Mon Sep 17 00:00:00 2001
From: yyh <92089059+lyzno1@users.noreply.github.com>
Date: Fri, 12 Jun 2026 17:48:56 +0800
Subject: [PATCH] fix: align toast stack with Base UI (#37382)
---
.../src/toast/__tests__/index.spec.tsx | 22 +++++++++
packages/dify-ui/src/toast/index.stories.tsx | 45 ++++++++++++++++++-
packages/dify-ui/src/toast/index.tsx | 4 +-
3 files changed, 68 insertions(+), 3 deletions(-)
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-[\'\']',
)}
>
-