{children}
diff --git a/web/app/components/app/log/__tests__/list.spec.tsx b/web/app/components/app/log/__tests__/list.spec.tsx
index 25512ed689..fe589b599a 100644
--- a/web/app/components/app/log/__tests__/list.spec.tsx
+++ b/web/app/components/app/log/__tests__/list.spec.tsx
@@ -84,10 +84,6 @@ vi.mock('@/app/components/app/store', () => ({
}),
}))
-vi.mock('@/app/components/base/tooltip', () => ({
- default: ({ children }: { children: ReactNode }) => <>{children}>,
-}))
-
vi.mock('@/app/components/base/drawer', () => ({
default: ({ children, isOpen, onClose }: { children: ReactNode, isOpen: boolean, onClose: () => void }) => (
isOpen
diff --git a/web/app/components/app/overview/__tests__/app-card.spec.tsx b/web/app/components/app/overview/__tests__/app-card.spec.tsx
index a6bacce887..1e9ba71a4f 100644
--- a/web/app/components/app/overview/__tests__/app-card.spec.tsx
+++ b/web/app/components/app/overview/__tests__/app-card.spec.tsx
@@ -1,4 +1,4 @@
-import type { ReactElement, ReactNode } from 'react'
+import type { ReactElement } from 'react'
import type { AppDetailResponse } from '@/models/app'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
@@ -98,15 +98,6 @@ vi.mock('../../app-access-control', () => ({
),
}))
-vi.mock('@/app/components/base/tooltip', () => ({
- default: ({ children, popupContent }: { children: ReactNode, popupContent?: ReactNode }) => (
-
- {children}
- {popupContent}
-
- ),
-}))
-
const mockWindowOpen = vi.fn()
Object.defineProperty(window, 'open', {
writable: true,
diff --git a/web/app/components/app/workflow-log/detail.tsx b/web/app/components/app/workflow-log/detail.tsx
index e191bfb794..05cd6f1676 100644
--- a/web/app/components/app/workflow-log/detail.tsx
+++ b/web/app/components/app/workflow-log/detail.tsx
@@ -1,9 +1,9 @@
'use client'
import type { FC } from 'react'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiCloseLine, RiPlayLargeLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useStore } from '@/app/components/app/store'
-import TooltipPlus from '@/app/components/base/tooltip'
import { WorkflowContextProvider } from '@/app/components/workflow/context'
import Run from '@/app/components/workflow/run'
import { useRouter } from '@/next/navigation'
@@ -33,19 +33,23 @@ const DetailPanel: FC
= ({ runID, onClose, canReplay = false }) => {
{t('runDetail.workflowTitle', { ns: 'appLog' })}
{canReplay && (
-
-
-
+
+
+
+
+ )}
+ />
+
+ {t('runDetail.testWithParams', { ns: 'appLog' })}
+
+
)}
diff --git a/web/app/components/apps/__tests__/app-card.spec.tsx b/web/app/components/apps/__tests__/app-card.spec.tsx
index c841617474..d61ca306ae 100644
--- a/web/app/components/apps/__tests__/app-card.spec.tsx
+++ b/web/app/components/apps/__tests__/app-card.spec.tsx
@@ -296,11 +296,6 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', () => {
}
})
-// Tooltip uses portals - minimal mock preserving popup content as title attribute
-vi.mock('@/app/components/base/tooltip', () => ({
- default: ({ children, popupContent }: { children: React.ReactNode, popupContent: React.ReactNode }) => React.createElement('div', { title: popupContent }, children),
-}))
-
// AppCardTags has tag API dependencies - mock for isolated testing
vi.mock('@/features/tag-management/components/app-card-tags', () => ({
AppCardTags: ({ tags }: { tags?: { id: string, name: string }[] }) => {
diff --git a/web/app/components/base/chat/chat/citation/progress-tooltip.tsx b/web/app/components/base/chat/chat/citation/progress-tooltip.tsx
index 75211b706e..88d7caca52 100644
--- a/web/app/components/base/chat/chat/citation/progress-tooltip.tsx
+++ b/web/app/components/base/chat/chat/citation/progress-tooltip.tsx
@@ -23,14 +23,10 @@ const ProgressTooltip: FC = ({
onOpenChange={setOpen}
>
setOpen(true)}
- onMouseLeave={() => setOpen(false)}
- />
- )}
+ data-testid="progress-trigger-content"
+ className="flex grow items-center border-0 bg-transparent p-0 text-left"
+ onMouseEnter={() => setOpen(true)}
+ onMouseLeave={() => setOpen(false)}
>
= ({
{t('chat.citation.hitScore', { ns: 'common' })}
diff --git a/web/app/components/base/chat/chat/citation/tooltip.tsx b/web/app/components/base/chat/chat/citation/tooltip.tsx
index e1d76a9383..2d69419df0 100644
--- a/web/app/components/base/chat/chat/citation/tooltip.tsx
+++ b/web/app/components/base/chat/chat/citation/tooltip.tsx
@@ -26,14 +26,10 @@ const Tooltip: FC = ({
onOpenChange={setOpen}
>
setOpen(true)}
- onMouseLeave={() => setOpen(false)}
- />
- )}
+ data-testid="tooltip-trigger-content"
+ className="mr-6 flex items-center border-0 bg-transparent p-0 text-left"
+ onMouseEnter={() => setOpen(true)}
+ onMouseLeave={() => setOpen(false)}
>
{icon}
{data}
@@ -41,7 +37,6 @@ const Tooltip: FC = ({
{text}
diff --git a/web/app/components/base/copy-icon/index.tsx b/web/app/components/base/copy-icon/index.tsx
index b0b4635a39..a770430580 100644
--- a/web/app/components/base/copy-icon/index.tsx
+++ b/web/app/components/base/copy-icon/index.tsx
@@ -1,8 +1,8 @@
'use client'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useClipboard } from '@/hooks/use-clipboard'
-import Tooltip from '../tooltip'
type Props = {
content: string
@@ -25,14 +25,25 @@ const CopyIcon = ({ content }: Props) => {
const safeTooltipText = tooltipText || ''
return (
-
-
- {!copied
- ? ()
- : ()}
-
+
+
+ {!copied
+ ? ()
+ : ()}
+
+ )}
+ />
+
+ {safeTooltipText}
+
)
}
diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param.tsx
index 0335587af0..16cbefe87a 100644
--- a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param.tsx
+++ b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param.tsx
@@ -1,7 +1,7 @@
'use client'
import type { FC } from 'react'
import * as React from 'react'
-import Tooltip from '@/app/components/base/tooltip'
+import { Infotip } from '@/app/components/base/infotip'
export const Item: FC<{ title: string, tooltip: string, children: React.JSX.Element }> = ({
title,
@@ -12,11 +12,9 @@ export const Item: FC<{ title: string, tooltip: string, children: React.JSX.Elem
{children}
diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/param-config-content.spec.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/param-config-content.spec.tsx
index 754bde98a6..b4d5beefa6 100644
--- a/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/param-config-content.spec.tsx
+++ b/web/app/components/base/features/new-feature-panel/text-to-speech/__tests__/param-config-content.spec.tsx
@@ -110,8 +110,7 @@ describe('ParamConfigContent', () => {
const languageLabel = screen.getByText(/voice\.voiceSettings\.language/)
expect(languageLabel)!.toBeInTheDocument()
- const tooltip = languageLabel.parentElement as HTMLElement
- expect(tooltip.querySelector('svg'))!.toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /voice\.voiceSettings\.resolutionTooltip/ }))!.toBeInTheDocument()
})
it('should display language listbox button', () => {
diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx
index 199cbecccb..f7c3b738a9 100644
--- a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx
+++ b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx
@@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'
import { replace } from 'string-ts'
import AudioBtn from '@/app/components/base/audio-btn'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
-import Tooltip from '@/app/components/base/tooltip'
+import { Infotip } from '@/app/components/base/infotip'
import { languages } from '@/i18n-config/language'
import { usePathname } from '@/next/navigation'
import { useAppVoices } from '@/service/use-apps'
@@ -89,17 +89,16 @@ const VoiceParamConfig = ({
{t('voice.voiceSettings.language', { ns: 'appDebug' })}
-
- {t('voice.voiceSettings.resolutionTooltip', { ns: 'appDebug' }).split('\n').map(item => (
-
- {item}
-
- ))}
+
+ {t('voice.voiceSettings.resolutionTooltip', { ns: 'appDebug' }).split('\n').map(item => (
+
+ {item}
- )}
- />
+ ))}
+
) => {
+ const handleClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
onClick?.(file)
}, [onClick, file])
return (
-
-
+
+ {
+ isImage
+ ? (
+
+ )
+ : (
+
+ )
+ }
+
)}
- onClick={handleClick}
- >
- {
- isImage
- ? (
-
- )
- : (
-
- )
- }
-
+ />
+
+ {name}
+
)
}
diff --git a/web/app/components/base/form/components/__tests__/label.spec.tsx b/web/app/components/base/form/components/__tests__/label.spec.tsx
index a3f564dafe..99471e5171 100644
--- a/web/app/components/base/form/components/__tests__/label.spec.tsx
+++ b/web/app/components/base/form/components/__tests__/label.spec.tsx
@@ -41,8 +41,8 @@ describe('Label', () => {
const tooltipText = 'Test Tooltip'
render()
- await user.hover(screen.getByTestId('test-input-tooltip'))
- expect(screen.getByText(tooltipText)).toBeInTheDocument()
+ await user.hover(screen.getByRole('button', { name: tooltipText }))
+ expect(await screen.findByText(tooltipText)).toBeInTheDocument()
})
it('should hide optional text when required is true', () => {
diff --git a/web/app/components/base/form/components/label.tsx b/web/app/components/base/form/components/label.tsx
index cd23043593..8987e350ff 100644
--- a/web/app/components/base/form/components/label.tsx
+++ b/web/app/components/base/form/components/label.tsx
@@ -1,6 +1,6 @@
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
-import Tooltip from '../../tooltip'
+import { Infotip } from '../../infotip'
export type LabelProps = {
htmlFor: string
@@ -33,13 +33,9 @@ const Label = ({
{!isRequired && showOptional && {t('label.optional', { ns: 'common' })}
}
{isRequired && *
}
{tooltip && (
- {tooltip}
- }
- triggerClassName="ml-0.5 w-4 h-4"
- triggerTestId={`${htmlFor}-tooltip`}
- />
+
+ {tooltip}
+
)}
)
diff --git a/web/app/components/base/input-with-copy/index.tsx b/web/app/components/base/input-with-copy/index.tsx
index 2da2e547c2..38a7ceed9c 100644
--- a/web/app/components/base/input-with-copy/index.tsx
+++ b/web/app/components/base/input-with-copy/index.tsx
@@ -1,11 +1,11 @@
'use client'
import type { InputProps } from '../input'
import { cn } from '@langgenius/dify-ui/cn'
+import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useClipboard } from '@/hooks/use-clipboard'
import ActionButton from '../action-button'
-import Tooltip from '../tooltip'
type InputWithCopyProps = {
showCopyButton?: boolean
@@ -64,18 +64,24 @@ const InputWithCopy = React.forwardRef((
onMouseLeave={reset}
data-testid="copy-button-wrapper"
>
-
-
- {copied
- ? ()
- : ()}
-
+
+
+ {copied
+ ? ()
+ : ()}
+
+ )}
+ />
+
+ {safeTooltipText}
+
)}
diff --git a/web/app/components/base/tooltip/TooltipManager.ts b/web/app/components/base/tooltip/TooltipManager.ts
deleted file mode 100644
index b0138af4b3..0000000000
--- a/web/app/components/base/tooltip/TooltipManager.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-class TooltipManager {
- private activeCloser: (() => void) | null = null
-
- register(closeFn: () => void) {
- if (this.activeCloser)
- this.activeCloser()
- this.activeCloser = closeFn
- }
-
- clear(closeFn: () => void) {
- if (this.activeCloser === closeFn)
- this.activeCloser = null
- }
-
- /**
- * Closes the currently active tooltip by calling its closer function
- * and clearing the reference to it
- */
- closeActiveTooltip() {
- if (this.activeCloser) {
- this.activeCloser()
- this.activeCloser = null
- }
- }
-}
-
-export const tooltipManager = new TooltipManager()
diff --git a/web/app/components/base/tooltip/__tests__/TooltipManager.spec.ts b/web/app/components/base/tooltip/__tests__/TooltipManager.spec.ts
deleted file mode 100644
index 406c48259a..0000000000
--- a/web/app/components/base/tooltip/__tests__/TooltipManager.spec.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import { tooltipManager } from '../TooltipManager'
-
-describe('TooltipManager', () => {
- // Test the singleton instance directly
- let manager: typeof tooltipManager
-
- beforeEach(() => {
- // Get fresh reference to the singleton
- manager = tooltipManager
- // Clean up any active tooltip by calling closeActiveTooltip
- // This ensures each test starts with a clean state
- manager.closeActiveTooltip()
- })
-
- describe('register', () => {
- it('should register a close function', () => {
- const closeFn = vi.fn()
- manager.register(closeFn)
- expect(closeFn).not.toHaveBeenCalled()
- })
-
- it('should call the existing close function when registering a new one', () => {
- const firstCloseFn = vi.fn()
- const secondCloseFn = vi.fn()
-
- manager.register(firstCloseFn)
- manager.register(secondCloseFn)
-
- expect(firstCloseFn).toHaveBeenCalledTimes(1)
- expect(secondCloseFn).not.toHaveBeenCalled()
- })
-
- it('should replace the active closer with the new one', () => {
- const firstCloseFn = vi.fn()
- const secondCloseFn = vi.fn()
-
- // Register first function
- manager.register(firstCloseFn)
-
- // Register second function - this should call firstCloseFn and replace it
- manager.register(secondCloseFn)
-
- // Verify firstCloseFn was called during register (replacement behavior)
- expect(firstCloseFn).toHaveBeenCalledTimes(1)
-
- // Now close the active tooltip - this should call secondCloseFn
- manager.closeActiveTooltip()
-
- // Verify secondCloseFn was called, not firstCloseFn
- expect(secondCloseFn).toHaveBeenCalledTimes(1)
- })
- })
-
- describe('clear', () => {
- it('should not clear if the close function does not match', () => {
- const closeFn = vi.fn()
- const otherCloseFn = vi.fn()
-
- manager.register(closeFn)
- manager.clear(otherCloseFn)
-
- manager.closeActiveTooltip()
- expect(closeFn).toHaveBeenCalledTimes(1)
- })
-
- it('should clear the close function if it matches', () => {
- const closeFn = vi.fn()
-
- manager.register(closeFn)
- manager.clear(closeFn)
-
- manager.closeActiveTooltip()
- expect(closeFn).not.toHaveBeenCalled()
- })
-
- it('should not call the close function when clearing', () => {
- const closeFn = vi.fn()
-
- manager.register(closeFn)
- manager.clear(closeFn)
-
- expect(closeFn).not.toHaveBeenCalled()
- })
- })
-
- describe('closeActiveTooltip', () => {
- it('should do nothing when no active closer is registered', () => {
- expect(() => manager.closeActiveTooltip()).not.toThrow()
- })
-
- it('should call the active closer function', () => {
- const closeFn = vi.fn()
- manager.register(closeFn)
-
- manager.closeActiveTooltip()
-
- expect(closeFn).toHaveBeenCalledTimes(1)
- })
-
- it('should clear the active closer after calling it', () => {
- const closeFn = vi.fn()
- manager.register(closeFn)
-
- manager.closeActiveTooltip()
- manager.closeActiveTooltip()
-
- expect(closeFn).toHaveBeenCalledTimes(1)
- })
-
- it('should handle multiple register and close cycles', () => {
- const closeFn1 = vi.fn()
- const closeFn2 = vi.fn()
- const closeFn3 = vi.fn()
-
- manager.register(closeFn1)
- manager.closeActiveTooltip()
-
- manager.register(closeFn2)
- manager.closeActiveTooltip()
-
- manager.register(closeFn3)
- manager.closeActiveTooltip()
-
- expect(closeFn1).toHaveBeenCalledTimes(1)
- expect(closeFn2).toHaveBeenCalledTimes(1)
- expect(closeFn3).toHaveBeenCalledTimes(1)
- })
- })
-})
diff --git a/web/app/components/base/tooltip/__tests__/content.spec.tsx b/web/app/components/base/tooltip/__tests__/content.spec.tsx
deleted file mode 100644
index fa5d86756e..0000000000
--- a/web/app/components/base/tooltip/__tests__/content.spec.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { render, screen } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import { describe, expect, it, vi } from 'vitest'
-import { ToolTipContent } from '../content'
-
-describe('ToolTipContent', () => {
- it('should render children correctly', () => {
- render(
-