From 2bb1f0906b67dc573aa3c2d4abb0fac62100c309 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Sat, 9 May 2026 13:26:21 +0800 Subject: [PATCH] refactor(web): migrate legacy tooltip callers (#35961) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- eslint-suppressions.json | 174 +-------- packages/dify-ui/README.md | 7 + .../billing/cloud-plan-payment-flow.test.tsx | 22 +- .../specific-groups-or-members.tsx | 15 +- .../app/configuration/config-var/index.tsx | 12 +- .../config/agent/agent-setting/item-panel.tsx | 13 +- .../app/log/__tests__/list.spec.tsx | 4 - .../app/overview/__tests__/app-card.spec.tsx | 11 +- .../components/app/workflow-log/detail.tsx | 32 +- .../apps/__tests__/app-card.spec.tsx | 5 - .../__tests__/progress-tooltip.spec.tsx | 8 +- .../chat/citation/__tests__/tooltip.spec.tsx | 16 +- .../chat/chat/citation/progress-tooltip.tsx | 18 +- .../base/chat/chat/citation/tooltip.tsx | 29 +- web/app/components/base/copy-icon/index.tsx | 29 +- .../annotation-reply/config-param.tsx | 10 +- .../__tests__/param-config-content.spec.tsx | 3 +- .../text-to-speech/param-config-content.tsx | 21 +- web/app/components/base/file-thumb/index.tsx | 68 ++-- .../form/components/__tests__/label.spec.tsx | 4 +- .../components/base/form/components/label.tsx | 12 +- .../components/base/input-with-copy/index.tsx | 32 +- .../components/base/tooltip/TooltipManager.ts | 27 -- .../tooltip/__tests__/TooltipManager.spec.ts | 129 ------- .../base/tooltip/__tests__/content.spec.tsx | 49 --- .../base/tooltip/__tests__/index.spec.tsx | 333 ------------------ web/app/components/base/tooltip/content.tsx | 22 -- .../components/base/tooltip/index.stories.tsx | 60 ---- web/app/components/base/tooltip/index.tsx | 231 ------------ .../cloud-plan-item/__tests__/index.spec.tsx | 2 +- .../list/__tests__/index.spec.tsx | 7 +- .../list/item/__tests__/index.spec.tsx | 7 +- .../list/item/__tests__/tooltip.spec.tsx | 7 +- .../cloud-plan-item/list/item/tooltip.tsx | 21 +- .../documents/components/operations.tsx | 42 ++- .../file-list/list/__tests__/item.spec.tsx | 6 - .../online-drive/file-list/list/item.tsx | 70 ++-- .../processing/embedding-process/index.tsx | 21 +- .../detail/completed/display-toggle.tsx | 41 ++- .../detail/completed/segment-card/index.tsx | 66 ++-- .../metadata/edit-metadata-batch/modal.tsx | 16 +- .../dataset-metadata-drawer.tsx | 6 +- ...itch-credential-in-load-balancing.spec.tsx | 8 +- .../model-auth/config-provider.tsx | 12 +- .../switch-credential-in-load-balancing.tsx | 12 +- .../__tests__/status-indicators.spec.tsx | 18 +- .../status-indicators.tsx | 50 ++- .../__tests__/popup-item.spec.tsx | 4 - .../provider-added-card/model-list-item.tsx | 16 +- .../subscription-list/subscription-card.tsx | 35 +- .../__tests__/reasoning-config-form.spec.tsx | 8 +- .../components/reasoning-config-form.tsx | 55 +-- .../tool-selector/components/tool-item.tsx | 38 +- .../components/plugins/plugin-item/index.tsx | 20 +- .../components/panel/input-field/index.tsx | 10 +- .../config-credentials.tsx | 32 +- .../workflow-tool/__tests__/index.spec.tsx | 16 - .../block-selector/__tests__/tabs.spec.tsx | 21 +- .../workflow/block-selector/tabs.tsx | 31 +- .../mcp-tool-not-support-tooltip.tsx | 21 +- .../components/switch-plugin-version.tsx | 157 +++++---- .../workflow/nodes/iteration-start/index.tsx | 22 +- .../search-method-option.tsx | 13 +- .../metadata/metadata-filter/index.tsx | 12 +- .../workflow/nodes/loop-start/index.tsx | 22 +- .../nodes/parameter-extractor/panel.tsx | 18 +- .../__tests__/integration.spec.tsx | 4 +- .../__tests__/node.spec.tsx | 23 +- .../components/advanced-setting.tsx | 18 +- .../nodes/question-classifier/node.tsx | 23 +- .../components/trigger-form/item.tsx | 18 +- .../trigger-webhook/__tests__/panel.spec.tsx | 4 - .../workflow/nodes/trigger-webhook/panel.tsx | 58 +-- .../panel/env-panel/variable-modal.tsx | 18 +- web/app/components/workflow/run/node.tsx | 15 +- .../workflow/variable-inspect/listening.tsx | 48 +-- .../components/nodes/base.tsx | 22 +- .../nodes/iteration-start/index.tsx | 12 +- .../components/nodes/loop-start/index.tsx | 12 +- web/docs/overlay-migration.md | 6 +- web/eslint.constants.mjs | 7 - 81 files changed, 841 insertions(+), 1806 deletions(-) delete mode 100644 web/app/components/base/tooltip/TooltipManager.ts delete mode 100644 web/app/components/base/tooltip/__tests__/TooltipManager.spec.ts delete mode 100644 web/app/components/base/tooltip/__tests__/content.spec.tsx delete mode 100644 web/app/components/base/tooltip/__tests__/index.spec.tsx delete mode 100644 web/app/components/base/tooltip/content.tsx delete mode 100644 web/app/components/base/tooltip/index.stories.tsx delete mode 100644 web/app/components/base/tooltip/index.tsx diff --git a/eslint-suppressions.json b/eslint-suppressions.json index cb41ef5f83..2326e92d2f 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -272,11 +272,6 @@ "count": 1 } }, - "web/app/components/app/app-access-control/specific-groups-or-members.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/app-publisher/features-wrapper.tsx": { "ts/no-explicit-any": { "count": 4 @@ -323,11 +318,6 @@ "count": 4 } }, - "web/app/components/app/configuration/config-var/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/configuration/config-var/select-var-type.tsx": { "ts/no-explicit-any": { "count": 1 @@ -341,11 +331,6 @@ "count": 1 } }, - "web/app/components/app/configuration/config/agent/agent-setting/item-panel.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/configuration/config/agent/agent-tools/index.tsx": { "ts/no-explicit-any": { "count": 9 @@ -593,11 +578,6 @@ "count": 2 } }, - "web/app/components/app/workflow-log/detail.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/workflow-log/filter.tsx": { "react-refresh/only-export-components": { "count": 1 @@ -967,11 +947,6 @@ "count": 1 } }, - "web/app/components/base/features/new-feature-panel/annotation-reply/config-param.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx": { "ts/no-explicit-any": { "count": 3 @@ -1005,11 +980,6 @@ "count": 2 } }, - "web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/features/types.ts": { "erasable-syntax-only/enums": { "count": 2 @@ -2096,11 +2066,6 @@ "count": 4 } }, - "web/app/components/datasets/documents/components/operations.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/components/rename-modal.tsx": { "no-restricted-imports": { "count": 1 @@ -2116,11 +2081,6 @@ "count": 1 } }, - "web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/item.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx": { "react/set-state-in-effect": { "count": 5 @@ -2166,11 +2126,6 @@ "count": 2 } }, - "web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/create-from-pipeline/steps/index.ts": { "no-barrel-files/no-barrel-files": { "count": 3 @@ -2196,11 +2151,6 @@ "count": 1 } }, - "web/app/components/datasets/documents/detail/completed/display-toggle.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/detail/completed/hooks/index.ts": { "no-barrel-files/no-barrel-files": { "count": 5 @@ -2217,11 +2167,6 @@ "count": 1 } }, - "web/app/components/datasets/documents/detail/completed/segment-card/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/documents/detail/context.ts": { "ts/no-explicit-any": { "count": 1 @@ -2310,7 +2255,7 @@ }, "web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts": { @@ -2338,7 +2283,7 @@ }, "web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx": { "no-restricted-imports": { - "count": 3 + "count": 2 } }, "web/app/components/datasets/metadata/metadata-dataset/select-metadata-modal.tsx": { @@ -2571,11 +2516,6 @@ "count": 4 } }, - "web/app/components/header/account-setting/model-provider-page/model-auth/config-provider.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/header/account-setting/model-provider-page/model-auth/hooks/index.ts": { "no-barrel-files/no-barrel-files": { "count": 6 @@ -2602,9 +2542,6 @@ } }, "web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 3 } @@ -2630,9 +2567,6 @@ } }, "web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 2 } @@ -2647,11 +2581,6 @@ "count": 2 } }, - "web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx": { "ts/no-explicit-any": { "count": 5 @@ -2900,11 +2829,6 @@ "count": 2 } }, - "web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-detail-panel/subscription-list/types.ts": { "erasable-syntax-only/enums": { "count": 1 @@ -2915,11 +2839,6 @@ "count": 7 } }, - "web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/index.ts": { "no-barrel-files/no-barrel-files": { "count": 2 @@ -2934,9 +2853,6 @@ } }, "web/app/components/plugins/plugin-item/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -3028,11 +2944,6 @@ "count": 1 } }, - "web/app/components/rag-pipeline/components/panel/input-field/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/index.tsx": { "ts/no-explicit-any": { "count": 1 @@ -3193,7 +3104,7 @@ }, "web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "web/app/components/tools/edit-custom-collection-modal/get-schema.tsx": { @@ -3380,11 +3291,6 @@ "count": 1 } }, - "web/app/components/workflow/block-selector/tabs.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx": { "ts/no-explicit-any": { "count": 1 @@ -3651,11 +3557,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/_base/components/memory-config.tsx": { "unicorn/prefer-number-properties": { "count": 1 @@ -3691,11 +3592,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/_base/components/variable/match-schema-type.ts": { "ts/no-explicit-any": { "count": 8 @@ -4050,11 +3946,6 @@ "count": 5 } }, - "web/app/components/workflow/nodes/iteration-start/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/iteration/default.ts": { "ts/no-explicit-any": { "count": 1 @@ -4075,11 +3966,6 @@ "count": 4 } }, - "web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/search-method-option.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/type.ts": { "ts/no-explicit-any": { "count": 2 @@ -4113,11 +3999,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/knowledge-retrieval/default.ts": { "ts/no-explicit-any": { "count": 1 @@ -4240,11 +4121,6 @@ "count": 7 } }, - "web/app/components/workflow/nodes/loop-start/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/loop/components/condition-list/condition-input.tsx": { "ts/no-explicit-any": { "count": 1 @@ -4306,11 +4182,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/parameter-extractor/panel.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/parameter-extractor/types.ts": { "erasable-syntax-only/enums": { "count": 2 @@ -4329,11 +4200,6 @@ "count": 9 } }, - "web/app/components/workflow/nodes/question-classifier/components/advanced-setting.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/question-classifier/components/class-item.tsx": { "react/set-state-in-effect": { "count": 1 @@ -4352,11 +4218,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/question-classifier/node.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/question-classifier/use-config.ts": { "react/set-state-in-effect": { "count": 2 @@ -4464,9 +4325,6 @@ } }, "web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -4522,11 +4380,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/trigger-webhook/panel.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/utils.ts": { "ts/no-explicit-any": { "count": 1 @@ -4637,9 +4490,6 @@ } }, "web/app/components/workflow/panel/env-panel/variable-modal.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react/set-state-in-effect": { "count": 4 }, @@ -4870,9 +4720,6 @@ } }, "web/app/components/workflow/variable-inspect/listening.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 2 } @@ -4911,26 +4758,11 @@ "count": 5 } }, - "web/app/components/workflow/workflow-preview/components/nodes/base.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/workflow-preview/components/nodes/constants.ts": { "ts/no-explicit-any": { "count": 1 } }, - "web/app/components/workflow/workflow-preview/components/nodes/iteration-start/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/workflow/workflow-preview/components/nodes/loop-start/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx": { "erasable-syntax-only/enums": { "count": 1 diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md index 2915fe5db7..c78faede89 100644 --- a/packages/dify-ui/README.md +++ b/packages/dify-ui/README.md @@ -99,6 +99,13 @@ See `[web/docs/overlay-migration.md](../../web/docs/overlay-migration.md)` for t - Never create an extra manual portal on top of our primitives — use the exported content / portal parts such as `DialogContent`, `PopoverContent`, and `DrawerPortal`. Base UI handles focus management, scroll-locking, and dismissal. - When a primitive needs additional presentation chrome (e.g. a custom backdrop), add it **inside** the exported component, not at call sites. +### Tooltip, infotip, and popover semantics + +- Use `Tooltip` only for short, non-interactive visual labels. The trigger must already have visible text or an `aria-label`; the tooltip is not the accessible name and must not contain links, buttons, forms, or structured prose. +- Use `Popover` for explanatory content, long text, rich layout, or anything users may need to reach on touch or with assistive technology. In `web/`, the `Infotip` wrapper is the preferred pattern for a `?` help glyph backed by `Popover`. +- Pick a `placement` and let the primitive own spacing. Avoid per-call-site offsets unless the component API explicitly needs a measured layout exception. +- When passing a Base UI trigger `render` prop, render a real ` - + + + + + )} + /> + + {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/__tests__/progress-tooltip.spec.tsx b/web/app/components/base/chat/chat/citation/__tests__/progress-tooltip.spec.tsx index a47123aafd..f53e7d15c3 100644 --- a/web/app/components/base/chat/chat/citation/__tests__/progress-tooltip.spec.tsx +++ b/web/app/components/base/chat/chat/citation/__tests__/progress-tooltip.spec.tsx @@ -55,7 +55,7 @@ describe('ProgressTooltip', () => { await user.hover(screen.getByTestId('progress-trigger-content')) - expect(screen.getByTestId('progress-tooltip-popup')).toBeInTheDocument() + expect(await screen.findByTestId('progress-tooltip-popup')).toBeInTheDocument() }) it('should hide the tooltip popup on mouse leave', async () => { @@ -74,7 +74,7 @@ describe('ProgressTooltip', () => { await user.hover(screen.getByTestId('progress-trigger-content')) - expect(screen.getByTestId('progress-tooltip-popup')).toHaveTextContent(/hitScore/i) + expect(await screen.findByTestId('progress-tooltip-popup')).toHaveTextContent(/hitScore/i) }) it('should show the data value inside the tooltip popup', async () => { @@ -83,7 +83,7 @@ describe('ProgressTooltip', () => { await user.hover(screen.getByTestId('progress-trigger-content')) - expect(screen.getByTestId('progress-tooltip-popup')).toHaveTextContent('0.8') + expect(await screen.findByTestId('progress-tooltip-popup')).toHaveTextContent('0.8') }) }) @@ -126,7 +126,7 @@ describe('ProgressTooltip', () => { await user.unhover(screen.getByTestId('progress-trigger-content')) await user.hover(screen.getByTestId('progress-trigger-content')) - expect(screen.getByTestId('progress-tooltip-popup')).toBeInTheDocument() + expect(await screen.findByTestId('progress-tooltip-popup')).toBeInTheDocument() }) it('should keep tooltip closed without any interaction', () => { diff --git a/web/app/components/base/chat/chat/citation/__tests__/tooltip.spec.tsx b/web/app/components/base/chat/chat/citation/__tests__/tooltip.spec.tsx index 45ac4b4fb4..58a3c5c654 100644 --- a/web/app/components/base/chat/chat/citation/__tests__/tooltip.spec.tsx +++ b/web/app/components/base/chat/chat/citation/__tests__/tooltip.spec.tsx @@ -41,7 +41,7 @@ describe('Tooltip', () => { await user.hover(screen.getByTestId('tooltip-trigger-content')) - expect(screen.getByTestId('tooltip-popup')).toHaveTextContent('Word Count') + expect(await screen.findByTestId('tooltip-popup')).toHaveTextContent('Word Count') }) it('should render the data value inside the tooltip popup', async () => { @@ -50,7 +50,7 @@ describe('Tooltip', () => { await user.hover(screen.getByTestId('tooltip-trigger-content')) - expect(screen.getByTestId('tooltip-popup')).toHaveTextContent('99') + expect(await screen.findByTestId('tooltip-popup')).toHaveTextContent('99') }) it('should render a string data value inside the tooltip popup', async () => { @@ -59,7 +59,7 @@ describe('Tooltip', () => { await user.hover(screen.getByTestId('tooltip-trigger-content')) - expect(screen.getByTestId('tooltip-popup')).toHaveTextContent('abc1234') + expect(await screen.findByTestId('tooltip-popup')).toHaveTextContent('abc1234') }) it('should render both text and data together inside the tooltip popup', async () => { @@ -68,7 +68,7 @@ describe('Tooltip', () => { await user.hover(screen.getByTestId('tooltip-trigger-content')) - const popup = screen.getByTestId('tooltip-popup') + const popup = await screen.findByTestId('tooltip-popup') expect(popup).toHaveTextContent('Characters') expect(popup).toHaveTextContent('55') }) @@ -90,10 +90,10 @@ describe('Tooltip', () => { const user = userEvent.setup() const { rerender } = render(} />) await user.hover(screen.getByTestId('tooltip-trigger-content')) - expect(screen.getByTestId('tooltip-popup')).toHaveTextContent('Original') + expect(await screen.findByTestId('tooltip-popup')).toHaveTextContent('Original') rerender(} />) - expect(screen.getByTestId('tooltip-popup')).toHaveTextContent('Updated') + expect(await screen.findByTestId('tooltip-popup')).toHaveTextContent('Updated') }) }) @@ -104,7 +104,7 @@ describe('Tooltip', () => { await user.hover(screen.getByTestId('tooltip-trigger-content')) - expect(screen.getByTestId('tooltip-popup')).toBeInTheDocument() + expect(await screen.findByTestId('tooltip-popup')).toBeInTheDocument() }) it('should hide the tooltip popup on mouse leave', async () => { @@ -125,7 +125,7 @@ describe('Tooltip', () => { await user.unhover(screen.getByTestId('tooltip-trigger-content')) await user.hover(screen.getByTestId('tooltip-trigger-content')) - expect(screen.getByTestId('tooltip-popup')).toBeInTheDocument() + expect(await screen.findByTestId('tooltip-popup')).toBeInTheDocument() }) }) 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..be9a4b2661 100644 --- a/web/app/components/base/chat/chat/citation/progress-tooltip.tsx +++ b/web/app/components/base/chat/chat/citation/progress-tooltip.tsx @@ -4,7 +4,6 @@ import { TooltipContent, TooltipTrigger, } from '@langgenius/dify-ui/tooltip' -import { useState } from 'react' import { useTranslation } from 'react-i18next' type ProgressTooltipProps = { @@ -15,22 +14,12 @@ const ProgressTooltip: FC = ({ data, }) => { const { t } = useTranslation() - const [open, setOpen] = useState(false) return ( - + setOpen(true)} - onMouseLeave={() => setOpen(false)} - /> - )} + data-testid="progress-trigger-content" + className="flex grow items-center border-0 bg-transparent p-0 text-left" >
= ({ {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..f3460abd22 100644 --- a/web/app/components/base/chat/chat/citation/tooltip.tsx +++ b/web/app/components/base/chat/chat/citation/tooltip.tsx @@ -1,39 +1,27 @@ import type { FC } from 'react' import { - Tooltip as DifyTooltip, + Tooltip, TooltipContent, TooltipTrigger, } from '@langgenius/dify-ui/tooltip' import * as React from 'react' -import { useState } from 'react' -type TooltipProps = { +type CitationTooltipProps = { data: number | string text: string icon: React.ReactNode } -const Tooltip: FC = ({ +const CitationTooltip: FC = ({ data, text, icon, }) => { - const [open, setOpen] = useState(false) - return ( - + setOpen(true)} - onMouseLeave={() => setOpen(false)} - /> - )} + data-testid="tooltip-trigger-content" + className="mr-6 flex items-center border-0 bg-transparent p-0 text-left" > {icon} {data} @@ -41,15 +29,14 @@ const Tooltip: FC = ({ {text} {' '} {data} - + ) } -export default Tooltip +export default CitationTooltip 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
{title}
- {tooltip}
- } - /> + + {tooltip} +
{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(
- } - 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( - - Tooltip body text - , - ) - expect(screen.getByTestId('tooltip-content')).toBeInTheDocument() - expect(screen.getByTestId('tooltip-content-body')).toHaveTextContent('Tooltip body text') - expect(screen.queryByTestId('tooltip-content-title')).not.toBeInTheDocument() - expect(screen.queryByTestId('tooltip-content-action')).not.toBeInTheDocument() - }) - - it('should render title when provided', () => { - render( - - Tooltip body text - , - ) - expect(screen.getByTestId('tooltip-content-title')).toHaveTextContent('Tooltip Title') - }) - - it('should render action when provided', () => { - render( - Action Text}> - Tooltip body text - , - ) - expect(screen.getByTestId('tooltip-content-action')).toHaveTextContent('Action Text') - }) - - it('should handle action click', async () => { - const user = userEvent.setup() - const handleActionClick = vi.fn() - render( - Action Text}> - Tooltip body text - , - ) - - await user.click(screen.getByText('Action Text')) - expect(handleActionClick).toHaveBeenCalledTimes(1) - }) -}) diff --git a/web/app/components/base/tooltip/__tests__/index.spec.tsx b/web/app/components/base/tooltip/__tests__/index.spec.tsx deleted file mode 100644 index 39f8f1b503..0000000000 --- a/web/app/components/base/tooltip/__tests__/index.spec.tsx +++ /dev/null @@ -1,333 +0,0 @@ -import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' -import * as React from 'react' -import Tooltip from '../index' -import { tooltipManager } from '../TooltipManager' - -afterEach(() => { - cleanup() - vi.clearAllTimers() - vi.useRealTimers() -}) - -describe('Tooltip', () => { - describe('Rendering', () => { - it('should render default tooltip with question icon', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - expect(trigger).not.toBeNull() - expect(trigger?.querySelector('svg')).not.toBeNull() // question icon - }) - - it('should render with custom children', () => { - const { getByText } = render( - - - , - ) - expect(getByText('Hover me').textContent).toBe('Hover me') - }) - - it('should render correctly when asChild is false', () => { - const { container } = render( - - Trigger - , - ) - const trigger = container.querySelector('.custom-parent-trigger') - expect(trigger).not.toBeNull() - }) - - it('should render with a fallback question icon when children are null', () => { - const { container } = render( - - {null} - , - ) - const trigger = container.querySelector('.custom-fallback-trigger') - expect(trigger).not.toBeNull() - expect(trigger?.querySelector('svg')).not.toBeNull() - }) - }) - - describe('Disabled state', () => { - it('should not show tooltip when disabled', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - act(() => { - fireEvent.mouseEnter(trigger!) - }) - expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument() - }) - }) - - describe('Trigger methods', () => { - beforeEach(() => { - vi.useFakeTimers() - }) - - it('should open on hover when triggerMethod is hover', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - act(() => { - fireEvent.mouseEnter(trigger!) - }) - expect(screen.queryByText('Tooltip content')).toBeInTheDocument() - }) - - it('should close on mouse leave when triggerMethod is hover and needsDelay is false', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - act(() => { - fireEvent.mouseEnter(trigger!) - fireEvent.mouseLeave(trigger!) - }) - expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument() - }) - - it('should toggle on click when triggerMethod is click', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - act(() => { - fireEvent.click(trigger!) - }) - expect(screen.queryByText('Tooltip content')).toBeInTheDocument() - - // Test toggle off - act(() => { - fireEvent.click(trigger!) - }) - expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument() - }) - - it('should do nothing on mouse enter if triggerMethod is click', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - act(() => { - fireEvent.mouseEnter(trigger!) - }) - expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument() - }) - - it('should delay closing on mouse leave when needsDelay is true', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - - act(() => { - fireEvent.mouseEnter(trigger!) - }) - expect(screen.getByText('Tooltip content')).toBeInTheDocument() - - act(() => { - fireEvent.mouseLeave(trigger!) - }) - // Shouldn't close immediately - expect(screen.getByText('Tooltip content')).toBeInTheDocument() - - act(() => { - vi.advanceTimersByTime(350) - }) - // Should close after delay - expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument() - }) - - it('should not close if mouse enters popup before delay finishes', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - - act(() => { - fireEvent.mouseEnter(trigger!) - }) - - const popup = screen.getByText('Tooltip content') - expect(popup).toBeInTheDocument() - - act(() => { - fireEvent.mouseLeave(trigger!) - }) - - act(() => { - vi.advanceTimersByTime(150) - // Simulate mouse entering popup area itself during the delay timeframe - fireEvent.mouseEnter(popup) - }) - - act(() => { - vi.advanceTimersByTime(200) // Complete the 300ms original delay - }) - - // Should still be open because we are hovering the popup - expect(screen.getByText('Tooltip content')).toBeInTheDocument() - - // Now mouse leaves popup - act(() => { - fireEvent.mouseLeave(popup) - }) - - act(() => { - vi.advanceTimersByTime(350) - }) - // Should now close - expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument() - }) - - it('should do nothing on mouse enter/leave of popup when triggerMethod is not hover', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - - act(() => { - fireEvent.click(trigger!) - }) - - const popup = screen.getByText('Tooltip content') - - act(() => { - fireEvent.mouseEnter(popup) - fireEvent.mouseLeave(popup) - vi.advanceTimersByTime(350) - }) - - // Should still be open because click method requires another click to close, not hover leave - expect(screen.getByText('Tooltip content')).toBeInTheDocument() - }) - - it('should clear close timeout if trigger is hovered again before delay finishes', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - - act(() => { - fireEvent.mouseEnter(trigger!) - }) - expect(screen.getByText('Tooltip content')).toBeInTheDocument() - - act(() => { - fireEvent.mouseLeave(trigger!) - }) - - act(() => { - vi.advanceTimersByTime(150) - // Re-hover trigger before it closes - fireEvent.mouseEnter(trigger!) - }) - - act(() => { - vi.advanceTimersByTime(200) // Original 300ms would be up - }) - - // Should still be open because we reset it - expect(screen.getByText('Tooltip content')).toBeInTheDocument() - }) - - it('should test clear close timeout if trigger is hovered again before delay finishes and isHoverPopupRef is true', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - - act(() => { - fireEvent.mouseEnter(trigger!) - }) - - const popup = screen.getByText('Tooltip content') - expect(popup).toBeInTheDocument() - - act(() => { - fireEvent.mouseEnter(popup) - fireEvent.mouseLeave(trigger!) - }) - - act(() => { - vi.advanceTimersByTime(350) - }) - - // Should still be open because we are hovering the popup - expect(screen.getByText('Tooltip content')).toBeInTheDocument() - }) - }) - - describe('TooltipManager', () => { - it('should close active tooltips when triggered centrally, overriding other closes', () => { - const triggerClassName1 = 'custom-trigger-1' - const triggerClassName2 = 'custom-trigger-2' - - const { container } = render( -
- - -
, - ) - - const trigger1 = container.querySelector(`.${triggerClassName1}`) - const trigger2 = container.querySelector(`.${triggerClassName2}`) - - expect(trigger2).not.toBeNull() - - // Open first tooltip - act(() => { - fireEvent.mouseEnter(trigger1!) - }) - expect(screen.queryByText('Tooltip content 1')).toBeInTheDocument() - - // TooltipManager should keep track of it - // Next, immediately open the second one without leaving first (e.g., via TooltipManager) - // TooltipManager registers the newest one and closes the old one when doing full external operations, but internally the manager allows direct closing - - act(() => { - tooltipManager.closeActiveTooltip() - }) - - expect(screen.queryByText('Tooltip content 1')).not.toBeInTheDocument() - - // Safe to call again - expect(() => tooltipManager.closeActiveTooltip()).not.toThrow() - }) - }) - - describe('Styling and positioning', () => { - it('should apply custom trigger className', () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - expect(trigger?.className).toContain('custom-trigger') - }) - - it('should pass triggerTestId to the fallback icon wrapper', () => { - render() - expect(screen.getByTestId('test-tooltip-icon')).toBeInTheDocument() - }) - - it('should apply custom popup className', async () => { - const triggerClassName = 'custom-trigger' - const { container } = render() - const trigger = container.querySelector(`.${triggerClassName}`) - act(() => { - fireEvent.mouseEnter(trigger!) - }) - expect((await screen.findByText('Tooltip content'))?.className).toContain('custom-popup') - }) - - it('should apply noDecoration when specified', async () => { - const triggerClassName = 'custom-trigger' - const { container } = render( - , - ) - const trigger = container.querySelector(`.${triggerClassName}`) - act(() => { - fireEvent.mouseEnter(trigger!) - }) - expect((await screen.findByText('Tooltip content'))?.className).not.toContain('bg-components-panel-bg') - }) - }) -}) diff --git a/web/app/components/base/tooltip/content.tsx b/web/app/components/base/tooltip/content.tsx deleted file mode 100644 index 191ee933f1..0000000000 --- a/web/app/components/base/tooltip/content.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { FC, PropsWithChildren, ReactNode } from 'react' - -type ToolTipContentProps = { - title?: ReactNode - action?: ReactNode -} & PropsWithChildren - -export const ToolTipContent: FC = ({ - title, - action, - children, -}) => { - return ( -
- {!!title && ( -
{title}
- )} -
{children}
- {!!action &&
{action}
} -
- ) -} diff --git a/web/app/components/base/tooltip/index.stories.tsx b/web/app/components/base/tooltip/index.stories.tsx deleted file mode 100644 index 69d0c5d2b6..0000000000 --- a/web/app/components/base/tooltip/index.stories.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/nextjs-vite' -import Tooltip from '.' - -const TooltipGrid = () => { - return ( -
-
Hover tooltips
-
- - - - - - Right tooltip - - -
-
Click tooltips
-
- - - - - - Plain content - - -
-
- ) -} - -const meta = { - title: 'Base/Feedback/Tooltip', - component: TooltipGrid, - parameters: { - layout: 'centered', - docs: { - description: { - component: 'Portal-based tooltip component supporting hover and click triggers, custom placements, and decorated content.', - }, - }, - }, - tags: ['autodocs'], -} satisfies Meta - -export default meta -type Story = StoryObj - -export const Playground: Story = {} diff --git a/web/app/components/base/tooltip/index.tsx b/web/app/components/base/tooltip/index.tsx deleted file mode 100644 index 85c63cdeaf..0000000000 --- a/web/app/components/base/tooltip/index.tsx +++ /dev/null @@ -1,231 +0,0 @@ -'use client' -import type { Placement } from '@langgenius/dify-ui/popover' -/** - * @deprecated Use `@langgenius/dify-ui/tooltip` instead. - * This component will be removed after migration is complete. - * See: https://github.com/langgenius/dify/issues/32767 - */ -import type { FC } from 'react' -import { cn } from '@langgenius/dify-ui/cn' -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@langgenius/dify-ui/popover' -import { RiQuestionLine } from '@remixicon/react' -import { useBoolean } from 'ahooks' -import * as React from 'react' -import { useCallback, useEffect, useRef, useState } from 'react' -import { tooltipManager } from './TooltipManager' - -type TooltipOffset = number | { - mainAxis?: number - crossAxis?: number -} - -type TooltipProps = { - position?: Placement - triggerMethod?: 'hover' | 'click' - triggerClassName?: string - triggerTestId?: string - disabled?: boolean - popupContent?: React.ReactNode - children?: React.ReactNode - popupClassName?: string - portalContentClassName?: string - noDecoration?: boolean - offset?: TooltipOffset - needsDelay?: boolean - asChild?: boolean -} - -const Tooltip: FC = ({ - position = 'top', - triggerMethod = 'hover', - triggerClassName, - triggerTestId, - disabled = false, - popupContent, - children, - popupClassName, - portalContentClassName, - noDecoration, - offset, - asChild = true, - needsDelay = true, -}) => { - const [open, setOpen] = useState(false) - const resolvedOffset = offset ?? 8 - const sideOffset = typeof resolvedOffset === 'number' ? resolvedOffset : (resolvedOffset.mainAxis ?? 0) - const alignOffset = typeof resolvedOffset === 'number' ? 0 : (resolvedOffset.crossAxis ?? 0) - const [isHoverPopup, { - setTrue: setHoverPopup, - setFalse: setNotHoverPopup, - }] = useBoolean(false) - - const isHoverPopupRef = useRef(isHoverPopup) - useEffect(() => { - isHoverPopupRef.current = isHoverPopup - }, [isHoverPopup]) - - const [isHoverTrigger, { - setTrue: setHoverTrigger, - setFalse: setNotHoverTrigger, - }] = useBoolean(false) - - const isHoverTriggerRef = useRef(isHoverTrigger) - useEffect(() => { - isHoverTriggerRef.current = isHoverTrigger - }, [isHoverTrigger]) - - const closeTimeoutRef = useRef | null>(null) - const clearCloseTimeout = useCallback(() => { - if (closeTimeoutRef.current) { - clearTimeout(closeTimeoutRef.current) - closeTimeoutRef.current = null - } - }, []) - - useEffect(() => { - return () => { - clearCloseTimeout() - } - }, [clearCloseTimeout]) - - const close = () => setOpen(false) - const handleOpenChange = (nextOpen: boolean) => { - if (disabled) { - setOpen(false) - return - } - if (triggerMethod === 'click') - setOpen(nextOpen) - else if (!nextOpen) - setOpen(false) - } - - const handleLeave = (isTrigger: boolean) => { - if (isTrigger) - setNotHoverTrigger() - else - setNotHoverPopup() - - // give time to move to the popup - if (needsDelay) { - clearCloseTimeout() - closeTimeoutRef.current = setTimeout(() => { - closeTimeoutRef.current = null - if (!isHoverPopupRef.current && !isHoverTriggerRef.current) { - setOpen(false) - tooltipManager.clear(close) - } - }, 300) - } - else { - clearCloseTimeout() - setOpen(false) - tooltipManager.clear(close) - } - } - const handleTriggerMouseEnter = () => { - if (triggerMethod === 'hover') { - clearCloseTimeout() - setHoverTrigger() - tooltipManager.register(close) - setOpen(true) - } - } - const handleTriggerMouseLeave = () => { - if (triggerMethod === 'hover') - handleLeave(true) - } - const handlePopupMouseEnter = () => { - if (triggerMethod === 'hover') { - clearCloseTimeout() - setHoverPopup() - } - } - const handlePopupMouseLeave = () => { - if (triggerMethod === 'hover') - handleLeave(false) - } - - const fallbackTrigger = ( -
- -
- ) - const triggerContent = children || fallbackTrigger - const childElement = React.isValidElement>(triggerContent) - ? triggerContent - : fallbackTrigger - const nativeButton = typeof childElement.type !== 'string' || childElement.type === 'button' - - const renderAsChildTrigger = () => { - const childProps = childElement.props - return React.cloneElement(childElement, { - onMouseEnter: (event: React.MouseEvent) => { - childProps.onMouseEnter?.(event) - handleTriggerMouseEnter() - }, - onMouseLeave: (event: React.MouseEvent) => { - childProps.onMouseLeave?.(event) - handleTriggerMouseLeave() - }, - }) - } - const effectiveOpen = !disabled && open - - return ( - - {asChild - ? ( - - ) - : ( - - )} - > - {triggerContent} - - )} - {effectiveOpen && !!popupContent && ( - - {popupContent} - - )} - - ) -} - -export default React.memo(Tooltip) diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx index 615579bc6c..568a2656ba 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx @@ -241,7 +241,7 @@ describe('CloudPlanItem', () => { ) // Sandbox viewed from a higher plan is disabled, but let's verify no API calls - const button = screen.getByRole('button') + const button = screen.getByRole('button', { name: 'billing.plansCommon.startForFree' }) fireEvent.click(button) await waitFor(() => { diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/list/__tests__/index.spec.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/list/__tests__/index.spec.tsx index 5a06509355..e6a0d78273 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/list/__tests__/index.spec.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/list/__tests__/index.spec.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import * as React from 'react' import { Plan } from '../../../../../type' import List from '../index' @@ -12,11 +13,13 @@ describe('CloudPlanItem/List', () => { expect(screen.getByText('billing.plansCommon.startNodes.limited:{"count":2}')).toBeInTheDocument() }) - it('should show professional monthly quotas and tooltips', () => { + it('should show professional monthly quotas and tooltips', async () => { + const user = userEvent.setup() render() expect(screen.getByText('billing.plansCommon.messageRequest.titlePerMonth:{"count":5000}')).toBeInTheDocument() - expect(screen.getByText('billing.plansCommon.vectorSpaceTooltip')).toBeInTheDocument() + await user.hover(screen.getByRole('button', { name: 'billing.plansCommon.vectorSpaceTooltip' })) + expect(await screen.findByText('billing.plansCommon.vectorSpaceTooltip')).toBeInTheDocument() expect(screen.getByText('billing.plansCommon.workflowExecution.faster')).toBeInTheDocument() }) diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/__tests__/index.spec.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/__tests__/index.spec.tsx index e1aada80f8..f75b334fd9 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/__tests__/index.spec.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/__tests__/index.spec.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import Item from '../index' describe('Item', () => { @@ -20,14 +21,16 @@ describe('Item', () => { // Toggling the optional tooltip indicator describe('Tooltip behavior', () => { - it('should render tooltip content when tooltip text is provided', () => { + it('should render tooltip content when tooltip text is provided', async () => { + const user = userEvent.setup() const label = 'Workspace seats' const tooltip = 'Seats define how many teammates can join the workspace.' const { container } = render() expect(screen.getByText(label)).toBeInTheDocument() - expect(screen.getByText(tooltip)).toBeInTheDocument() + await user.hover(screen.getByRole('button', { name: tooltip })) + expect(await screen.findByText(tooltip)).toBeInTheDocument() expect(container.querySelector('.group')).not.toBeNull() }) diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/__tests__/tooltip.spec.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/__tests__/tooltip.spec.tsx index 86e4cb1061..c744fdb60e 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/__tests__/tooltip.spec.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/__tests__/tooltip.spec.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import Tooltip from '../tooltip' describe('Tooltip', () => { @@ -8,12 +9,14 @@ describe('Tooltip', () => { // Rendering the info tooltip container describe('Rendering', () => { - it('should render the content panel when provide with text', () => { + it('should render the content panel when hovered', async () => { + const user = userEvent.setup() const content = 'Usage resets on the first day of every month.' render() + await user.hover(screen.getByRole('button', { name: content })) - expect(() => screen.getByText(content)).not.toThrow() + expect(await screen.findByText(content)).toBeInTheDocument() }) }) diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/tooltip.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/tooltip.tsx index fe6aa9c2cb..be53ef6b1b 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/tooltip.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/tooltip.tsx @@ -1,3 +1,4 @@ +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { RiInfoI } from '@remixicon/react' import * as React from 'react' @@ -11,14 +12,20 @@ const Tooltip = ({ if (!content) return null return ( -
-
- {content} -
-
+ + -
-
+ + + {content} + + ) } diff --git a/web/app/components/datasets/documents/components/operations.tsx b/web/app/components/datasets/documents/components/operations.tsx index 8692da927d..7dc184aee4 100644 --- a/web/app/components/datasets/documents/components/operations.tsx +++ b/web/app/components/datasets/documents/components/operations.tsx @@ -16,15 +16,16 @@ import { DropdownMenuContent, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { Switch } from '@langgenius/dify-ui/switch' import { toast } from '@langgenius/dify-ui/toast' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useBoolean, useDebounceFn } from 'ahooks' import { noop } from 'es-toolkit/function' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' -import Tooltip from '@/app/components/base/tooltip' import { IS_CE_EDITION } from '@/config' import { DataSourceType, DocumentActionType } from '@/models/datasets' import { useRouter } from '@/next/navigation' @@ -205,11 +206,12 @@ const Operations = ({ embeddingAvailable, datasetId, detail, selectedIds, onSele <> {archived ? ( - -
- -
-
+ + } /> + + {t('list.action.enableWarning', { ns: 'datasetDocuments' })} + + ) : handleSwitch(v ? 'enable' : 'disable')} size="md" />} @@ -217,16 +219,24 @@ const Operations = ({ embeddingAvailable, datasetId, detail, selectedIds, onSele )} {embeddingAvailable && ( <> - - + + router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)} + > + + + )} + /> + + {t('list.action.settings', { ns: 'datasetDocuments' })} + ({ ), })) -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => ( -
{children}
- ), -})) - vi.mock('../file-icon', () => ({ default: () => , })) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/item.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/item.tsx index 5018806265..8d9bfe0dff 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/item.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/item.tsx @@ -1,12 +1,10 @@ -import type { Placement } from '@floating-ui/react' import type { OnlineDriveFile } from '@/models/pipeline' -import { cn } from '@langgenius/dify-ui/cn' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import Checkbox from '@/app/components/base/checkbox' import Radio from '@/app/components/base/radio/ui' -import Tooltip from '@/app/components/base/tooltip' import { formatFileSize } from '@/utils/format' import FileIcon from './file-icon' @@ -33,14 +31,7 @@ const Item = ({ const isBucket = type === 'bucket' const isFolder = type === 'folder' - const Wrapper = disabled ? Tooltip : React.Fragment - const wrapperProps = disabled - ? { - popupContent: t('onlineDrive.notSupportedFileType', { ns: 'datasetPipeline' }), - position: 'top-end' as Placement, - offset: { mainAxis: 4, crossAxis: -104 }, - } - : {} + const disabledTip = t('onlineDrive.notSupportedFileType', { ns: 'datasetPipeline' }) const handleSelect = useCallback((e: React.MouseEvent | React.KeyboardEvent) => { e.stopPropagation() @@ -80,27 +71,44 @@ const Item = ({ onCheck={handleSelect} /> )} - -
+ + + + {name} + + {!isFolder && typeof size === 'number' && ( + {formatFileSize(size)} + )} + + + {disabledTip} + + + ) + : ( +
+ + + {name} + + {!isFolder && typeof size === 'number' && ( + {formatFileSize(size)} + )} +
)} - > - - - {name} - - {!isFolder && typeof size === 'number' && ( - {formatFileSize(size)} - )} -
-
) } diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx index 797f7b296a..58d77b387c 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx @@ -4,6 +4,7 @@ import type { InitialDocumentDetail } from '@/models/pipeline' import type { RETRIEVE_METHOD } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { RiAedFill, RiArrowRightLine, @@ -17,7 +18,6 @@ import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import NotionIcon from '@/app/components/base/notion-icon' -import Tooltip from '@/app/components/base/tooltip' import PriorityLabel from '@/app/components/billing/priority-label' import { Plan } from '@/app/components/billing/type' import UpgradeBtn from '@/app/components/billing/upgrade-btn' @@ -203,15 +203,18 @@ const EmbeddingProcess = ({
{`${getSourcePercent(indexingStatusDetail)}%`}
)} {indexingStatusDetail.indexing_status === 'error' && ( - - + + - - + + + {indexingStatusDetail.error} + + )} {indexingStatusDetail.indexing_status === 'completed' && ( diff --git a/web/app/components/datasets/documents/detail/completed/display-toggle.tsx b/web/app/components/datasets/documents/detail/completed/display-toggle.tsx index 6e961ac43f..6735399cd2 100644 --- a/web/app/components/datasets/documents/detail/completed/display-toggle.tsx +++ b/web/app/components/datasets/documents/detail/completed/display-toggle.tsx @@ -1,9 +1,9 @@ import type { FC } from 'react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiLineHeight } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' import { Collapse } from '@/app/components/base/icons/src/vender/knowledge' -import Tooltip from '@/app/components/base/tooltip' type DisplayToggleProps = { isCollapsed: boolean @@ -15,25 +15,30 @@ const DisplayToggle: FC = ({ toggleCollapsed, }) => { const { t } = useTranslation() + const label = isCollapsed ? t('segment.expandChunks', { ns: 'datasetDocuments' }) : t('segment.collapseChunks', { ns: 'datasetDocuments' }) return ( - - - + + + { + isCollapsed + ? + : + } + + )} + /> + + {label} + ) } diff --git a/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx b/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx index 1111bb6411..865ffbce15 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-card/index.tsx @@ -10,13 +10,13 @@ import { } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' import { Switch } from '@langgenius/dify-ui/switch' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiDeleteBinLine, RiEditLine } from '@remixicon/react' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import Divider from '@/app/components/base/divider' -import Tooltip from '@/app/components/base/tooltip' import ImageList from '@/app/components/datasets/common/image-list' import { ChunkingMode } from '@/models/datasets' import { formatNumber } from '@/utils/format' @@ -182,35 +182,43 @@ const SegmentCard: FC = ({ > {!archived && ( <> - -
{ - e.stopPropagation() - onClickEdit?.() - }} - > - -
+ + { + e.stopPropagation() + onClickEdit?.() + }} + > + + + )} + /> + Edit - -
{ - e.stopPropagation() - setShowModal(true) - }} - > - -
+ + { + e.stopPropagation() + setShowModal(true) + }} + > + + + )} + /> + Delete diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx index 8253f7faf6..f76284b36f 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx @@ -3,7 +3,6 @@ import type { FC } from 'react' import type { BuiltInMetadataItem, MetadataItemInBatchEdit, MetadataItemWithEdit } from '../types' import { Button } from '@langgenius/dify-ui/button' import { toast } from '@langgenius/dify-ui/toast' -import { RiQuestionLine } from '@remixicon/react' import { produce } from 'immer' import * as React from 'react' import { useCallback, useState } from 'react' @@ -11,8 +10,8 @@ import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import { useCreateMetaData } from '@/service/knowledge/use-metadata' import Checkbox from '../../../base/checkbox' +import { Infotip } from '../../../base/infotip' import Modal from '../../../base/modal' -import Tooltip from '../../../base/tooltip' import AddMetadataButton from '../add-metadata-button' import useCheckMetadataName from '../hooks/use-check-metadata-name' import SelectMetadataModal from '../metadata-dataset/select-metadata-modal' @@ -115,11 +114,14 @@ const EditMetadataBatchModal: FC = ({ datasetId, documentNum, list, onSav
setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} id="apply-to-all" />
{t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}
- {t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}
}> -
- -
-
+ + {t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })} +
} /> + + {t(`${i18nPrefix}.builtInDescription`, { ns: 'dataset' })} +
diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx index f9c923e6a1..73aa8f9bfc 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx @@ -1,5 +1,6 @@ import type { CustomModel, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations' import { fireEvent, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import SwitchCredentialInLoadBalancing from '../switch-credential-in-load-balancing' @@ -105,7 +106,8 @@ describe('SwitchCredentialInLoadBalancing', () => { expect(mockSetCustomModelCredential).toHaveBeenCalledWith(mockCredentials[0]) }) - it('should show tooltip when empty and custom credentials not allowed', () => { + it('should show tooltip when empty and custom credentials not allowed', async () => { + const user = userEvent.setup() const restrictedProvider = { ...mockProvider, allow_custom_token: false } render( { />, ) - fireEvent.mouseEnter(screen.getByText(/auth.credentialUnavailableInButton/)) - expect(screen.getByText('plugin.auth.credentialUnavailable'))!.toBeInTheDocument() + await user.hover(screen.getByRole('button', { name: /auth.credentialUnavailableInButton/ })) + expect(await screen.findByText('plugin.auth.credentialUnavailable'))!.toBeInTheDocument() }) // Empty credentials with allowed custom: no tooltip but still shows unavailable text diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/config-provider.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/config-provider.tsx index 4a268168ba..7529ef9afb 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/config-provider.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/config-provider.tsx @@ -5,6 +5,7 @@ import type { import { Button, } from '@langgenius/dify-ui/button' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiEqualizer2Line, } from '@remixicon/react' @@ -13,7 +14,6 @@ import { useCallback, } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import Authorized from './authorized' import { useCredentialStatus } from './hooks' @@ -53,11 +53,11 @@ const ConfigProvider = ({ ) if (notAllowCustomCredential && !hasCredential) { return ( - - {Item} + + + + {t('auth.credentialUnavailable', { ns: 'plugin' })} + ) } diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx index 58ffc180dd..8ccfc0a640 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx @@ -6,6 +6,7 @@ import type { } from '../declarations' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiArrowDownSLine } from '@remixicon/react' import { memo, @@ -13,7 +14,6 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' -import Tooltip from '@/app/components/base/tooltip' import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import Indicator from '@/app/components/header/indicator' import Authorized from './authorized' @@ -89,11 +89,11 @@ const SwitchCredentialInLoadBalancing = ({ ) if (empty && notAllowCustomCredential) { return ( - - {Item} + + + + {t('auth.credentialUnavailable', { ns: 'plugin' })} + ) } diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/status-indicators.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/status-indicators.spec.tsx index dc7c512f78..b204462ab5 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/status-indicators.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/status-indicators.spec.tsx @@ -21,10 +21,10 @@ describe('StatusIndicators', () => { installedPlugins = [{ name: 'demo-plugin', plugin_unique_identifier: 'demo@1.0.0' }] }) - const getTooltipTrigger = (container: HTMLElement) => { - const trigger = container.querySelector('[role="button"][aria-haspopup="dialog"]') + const getPopoverTrigger = (name: string) => { + const trigger = screen.getByRole('button', { name }) expect(trigger).toBeInTheDocument() - return trigger as HTMLElement + return trigger } it('should render nothing when model is available and enabled', () => { @@ -43,7 +43,7 @@ describe('StatusIndicators', () => { it('should render deprecated tooltip when provider model is disabled and in model list', async () => { const user = userEvent.setup() - const { container } = render( + render( { />, ) - await user.hover(getTooltipTrigger(container)) + await user.hover(getPopoverTrigger('nodes.agent.modelSelectorTooltips.deprecated')) expect(await screen.findByText('nodes.agent.modelSelectorTooltips.deprecated')).toBeInTheDocument() }) it('should render model-not-support tooltip when disabled model is not in model list and has no pluginInfo', async () => { const user = userEvent.setup() - const { container } = render( + render( { />, ) - await user.hover(getTooltipTrigger(container)) + await user.hover(getPopoverTrigger('nodes.agent.modelNotSupport.title')) expect(await screen.findByText('nodes.agent.modelNotSupport.title')).toBeInTheDocument() }) @@ -125,7 +125,7 @@ describe('StatusIndicators', () => { it('should render marketplace warning tooltip when provider is unavailable', async () => { const user = userEvent.setup() - const { container } = render( + render( { />, ) - await user.hover(getTooltipTrigger(container)) + await user.hover(getPopoverTrigger('nodes.agent.modelNotInMarketplace.title')) expect(await screen.findByText('nodes.agent.modelNotInMarketplace.title')).toBeInTheDocument() }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx index cca5846390..bc505657e2 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx @@ -1,5 +1,6 @@ +import type { ReactNode } from 'react' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { RiErrorWarningFill } from '@remixicon/react' -import Tooltip from '@/app/components/base/tooltip' import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/components/switch-plugin-version' import Link from '@/next/link' import { useInstalledPluginList } from '@/service/use-plugins' @@ -13,6 +14,28 @@ type StatusIndicatorsProps = { t: any } +type StatusPopoverProps = { + ariaLabel: string + content: ReactNode + children: ReactNode +} + +const StatusPopover = ({ ariaLabel, content, children }: StatusPopoverProps) => ( + + e.stopPropagation()} + > + {children} + + + {content} + + +) + const StatusIndicators = ({ needsConfiguration, modelProvider, inModelList, disabled, pluginInfo, t }: StatusIndicatorsProps) => { const { data: pluginList } = useInstalledPluginList() const renderTooltipContent = (title: string, description?: string, linkText?: string, linkHref?: string) => { @@ -48,27 +71,26 @@ const StatusIndicators = ({ needsConfiguration, modelProvider, inModelList, disa <> {inModelList ? ( - - + ) : !pluginInfo ? ( - - + ) : ( )} {!modelProvider && !pluginInfo && ( - - + )} ) diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx index 3c4fea6f51..e198853ddd 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx @@ -42,10 +42,6 @@ vi.mock('../feature-icon', () => ({ default: ({ feature }: { feature: string }) => {feature}, })) -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ children }: { children: ReactNode }) =>
{children}
, -})) - const mockCredentialPanelState = vi.hoisted(() => vi.fn()) vi.mock('../../provider-added-card/use-credential-panel-state', () => ({ useCredentialPanelState: mockCredentialPanelState, diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx index 305ef71c50..8ef0f11901 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx @@ -1,5 +1,6 @@ import type { ModelItem, ModelProvider } from '../declarations' import { cn } from '@langgenius/dify-ui/cn' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { Switch } from '@langgenius/dify-ui/switch' import { useQueryClient } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' @@ -7,7 +8,6 @@ import { memo, useCallback } from 'react' import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' -import Tooltip from '@/app/components/base/tooltip' import { Plan } from '@/app/components/billing/type' import { useAppContext } from '@/context/app-context' import { useProviderContext, useProviderContextSelector } from '@/context/provider-context' @@ -102,14 +102,12 @@ const ModelListItem = ({ model, provider, isConfigurable, onChange, onModifyLoad { model.deprecated ? ( - {t('modelProvider.modelHasBeenDeprecated', { ns: 'common' })} - } - offset={{ mainAxis: 4 }} - > - - + + } /> + + {t('modelProvider.modelHasBeenDeprecated', { ns: 'common' })} + + ) : (isCurrentWorkspaceManager && ( {
- - {data.endpoint} -
- )} - position="left" - > -
- {data.endpoint} -
-
+ {data.endpoint + ? ( + + + {data.endpoint} + + + {data.endpoint} + + + ) + : ( +
+ {data.endpoint} +
+ )}
·
{data.workflows_in_use > 0 ? t('subscription.list.item.usedByNum', { ns: 'pluginTrigger', num: data.workflows_in_use }) : t('subscription.list.item.noUsed', { ns: 'pluginTrigger' })} diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/reasoning-config-form.spec.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/reasoning-config-form.spec.tsx index 016eda373d..50db3887b0 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/reasoning-config-form.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/__tests__/reasoning-config-form.spec.tsx @@ -54,10 +54,6 @@ vi.mock('@langgenius/dify-ui/switch', () => ({ ), })) -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ children }: { children?: React.ReactNode }) => <>{children}, -})) - vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useLanguage: () => 'en_US', })) @@ -233,7 +229,7 @@ describe('ReasoningConfigForm', () => { it('should open schema modal for object fields and support app selection', () => { const onChange = vi.fn() - const { container } = render( + render( { />, ) - fireEvent.click(container.querySelector('div.ml-0\\.5.cursor-pointer')!) + fireEvent.click(screen.getByRole('button', { name: 'workflow.nodes.agent.clickToViewParameterSchema' })) expect(screen.getByTestId('schema-modal')).toHaveTextContent('Config') fireEvent.click(screen.getByTestId('close-schema')) diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx index e6af05065f..1baae6d3ca 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx @@ -9,6 +9,7 @@ import type { import { cn } from '@langgenius/dify-ui/cn' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select' import { Switch } from '@langgenius/dify-ui/switch' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiArrowRightUpLine, RiBracesLine, @@ -16,9 +17,8 @@ import { import { useBoolean } from 'ahooks' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { Infotip } from '@/app/components/base/infotip' import Input from '@/app/components/base/input' -// eslint-disable-next-line no-restricted-imports -- legacy tooltip migration is handled separately from this change -import Tooltip from '@/app/components/base/tooltip' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { AppSelector } from '@/app/components/plugins/plugin-detail-panel/app-selector' @@ -127,17 +127,16 @@ const ReasoningConfigForm: React.FC = ({ } = schema const auto = value[variable]?.auto const fieldTitle = getFieldTitle(label, language) - const tooltipContent = (tooltip && ( - - {tooltip[language] || tooltip.en_US} -
- )} - triggerClassName="ml-0.5 w-4 h-4" - asChild={false} - /> - )) + const tooltipText = tooltip?.[language] || tooltip?.en_US + const tooltipContent = tooltipText && ( + + {tooltipText} + + ) const varInput = value[variable]!.value const { isString, @@ -173,20 +172,22 @@ const ReasoningConfigForm: React.FC = ({ · {resolveTargetVarType(type)} {isShowJSONEditor && ( - - {t('nodes.agent.clickToViewParameterSchema', { ns: 'workflow' })} - - )} - asChild={false} - > -
showSchema(input_schema as SchemaRoot, fieldTitle!)} - > - -
+ + showSchema(input_schema as SchemaRoot, fieldTitle!)} + > + + + )} + /> + + {t('nodes.agent.clickToViewParameterSchema', { ns: 'workflow' })} + )} diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx index ba85957108..d92c59b457 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx @@ -1,8 +1,8 @@ 'use client' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { Switch } from '@langgenius/dify-ui/switch' -import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiDeleteBinLine, RiEqualizer2Line, @@ -14,7 +14,6 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import AppIcon from '@/app/components/base/app-icon' import { Group } from '@/app/components/base/icons/src/vender/other' -import { ToolTipContent } from '@/app/components/base/tooltip/content' import Indicator from '@/app/components/header/indicator' import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability' @@ -144,11 +143,14 @@ const ToolItem = ({ className="-mt-1" uniqueIdentifier={installInfo} tooltip={( - - {`${t('detailPanel.toolSelector.unsupportedContent', { ns: 'plugin' })} ${t('detailPanel.toolSelector.unsupportedContent2', { ns: 'plugin' })}`} - +
+
+ {t('detailPanel.toolSelector.unsupportedTitle', { ns: 'plugin' })} +
+
+ {`${t('detailPanel.toolSelector.unsupportedContent', { ns: 'plugin' })} ${t('detailPanel.toolSelector.unsupportedContent2', { ns: 'plugin' })}`} +
+
)} onChange={() => { onInstall?.() @@ -167,18 +169,18 @@ const ToolItem = ({ /> )} {isError && ( - - - - - )} - /> - + + + + + {errorTip} - - + + )} ) diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index 5843dffbe9..6e6aaf88c9 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import type { PluginDetail } from '../types' import { cn } from '@langgenius/dify-ui/cn' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { RiArrowRightUpLine, RiBugLine, @@ -13,7 +14,6 @@ import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import { API_PREFIX } from '@/config' import { useAppContext } from '@/context/app-context' @@ -124,12 +124,18 @@ const PluginItem: FC = ({ {verified && <Verified className="ml-0.5 h-4 w-4" text={t('marketplace.verifiedTip', { ns: 'plugin' })} />} {!isDifyVersionCompatible && ( - <Tooltip popupContent={ - t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: declarationMeta.minimum_dify_version }) - } - > - <RiErrorWarningLine color="red" className="ml-0.5 h-4 w-4 shrink-0 text-text-accent" /> - </Tooltip> + <Popover> + <PopoverTrigger + openOnHover + aria-label={t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: declarationMeta.minimum_dify_version })} + className="ml-0.5 inline-flex h-4 w-4 shrink-0 border-0 bg-transparent p-0" + > + <RiErrorWarningLine color="red" className="h-4 w-4 text-text-accent" /> + </PopoverTrigger> + <PopoverContent popupClassName="px-3 py-2 system-xs-regular text-text-tertiary"> + {t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: declarationMeta.minimum_dify_version })} + </PopoverContent> + </Popover> )} <Badge className="ml-1 shrink-0" diff --git a/web/app/components/rag-pipeline/components/panel/input-field/index.tsx b/web/app/components/rag-pipeline/components/panel/input-field/index.tsx index 95a76d5e86..3572d6012f 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/index.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/index.tsx @@ -13,7 +13,7 @@ import { import { useTranslation } from 'react-i18next' import { useNodes } from 'reactflow' import Divider from '@/app/components/base/divider' -import Tooltip from '@/app/components/base/tooltip' +import { Infotip } from '@/app/components/base/infotip' import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks' import { useNodesSyncDraft } from '@/app/components/workflow/hooks' import { useStore } from '@/app/components/workflow/store' @@ -137,10 +137,12 @@ const InputFieldPanel = () => { <span className="system-sm-semibold-uppercase text-text-secondary"> {t('inputFieldPanel.uniqueInputs.title', { ns: 'datasetPipeline' })} </span> - <Tooltip - popupContent={t('inputFieldPanel.uniqueInputs.tooltip', { ns: 'datasetPipeline' })} + <Infotip + aria-label={t('inputFieldPanel.uniqueInputs.tooltip', { ns: 'datasetPipeline' })} popupClassName="max-w-[240px]" - /> + > + {t('inputFieldPanel.uniqueInputs.tooltip', { ns: 'datasetPipeline' })} + </Infotip> </div> <div className="flex flex-col gap-y-1 py-1"> { diff --git a/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx b/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx index bee775e80b..9ed7c45165 100644 --- a/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx @@ -6,9 +6,9 @@ import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { useTranslation } from 'react-i18next' import Drawer from '@/app/components/base/drawer-plus' +import { Infotip } from '@/app/components/base/infotip' import Input from '@/app/components/base/input' import Radio from '@/app/components/base/radio/ui' -import Tooltip from '@/app/components/base/tooltip' import { AuthHeaderPrefix, AuthType } from '@/app/components/tools/types' type Props = { @@ -123,14 +123,13 @@ const ConfigCredential: FC<Props> = ({ <div> <div className="flex items-center py-2 system-sm-medium text-text-primary"> {t('createTool.authMethod.key', { ns: 'tools' })} - <Tooltip - popupContent={( - <div className="w-[261px] text-text-tertiary"> - {t('createTool.authMethod.keyTooltip', { ns: 'tools' })} - </div> - )} - triggerClassName="ml-0.5 w-4 h-4" - /> + <Infotip + aria-label={t('createTool.authMethod.keyTooltip', { ns: 'tools' })} + className="ml-0.5 h-4 w-4" + popupClassName="w-[261px] text-text-tertiary" + > + {t('createTool.authMethod.keyTooltip', { ns: 'tools' })} + </Infotip> </div> <Input value={tempCredential.api_key_header} @@ -153,14 +152,13 @@ const ConfigCredential: FC<Props> = ({ <div> <div className="flex items-center py-2 system-sm-medium text-text-primary"> {t('createTool.authMethod.queryParam', { ns: 'tools' })} - <Tooltip - popupContent={( - <div className="w-[261px] text-text-tertiary"> - {t('createTool.authMethod.queryParamTooltip', { ns: 'tools' })} - </div> - )} - triggerClassName="ml-0.5 w-4 h-4" - /> + <Infotip + aria-label={t('createTool.authMethod.queryParamTooltip', { ns: 'tools' })} + className="ml-0.5 h-4 w-4" + popupClassName="w-[261px] text-text-tertiary" + > + {t('createTool.authMethod.queryParamTooltip', { ns: 'tools' })} + </Infotip> </div> <Input value={tempCredential.api_key_query_param} diff --git a/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx b/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx index 8c35232d35..3a8e3a539b 100644 --- a/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx +++ b/web/app/components/tools/workflow-tool/__tests__/index.spec.tsx @@ -1,4 +1,3 @@ -import type { ReactNode } from 'react' import type { WorkflowToolDrawerPayload } from '../index' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -28,21 +27,6 @@ vi.mock('@/app/components/tools/labels/selector', () => ({ ), })) -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ - children, - popupContent, - }: { - children?: ReactNode - popupContent?: ReactNode - }) => ( - <div> - {children} - {popupContent} - </div> - ), -})) - vi.mock('../confirm-modal', () => ({ default: ({ show, onClose, onConfirm }: { show: boolean, onClose: () => void, onConfirm: () => void }) => ( show diff --git a/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx index 3002cafa0a..208d87c23a 100644 --- a/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx @@ -23,21 +23,6 @@ const { }, })) -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ - children, - popupContent, - }: { - children: React.ReactNode - popupContent: React.ReactNode - }) => ( - <div> - <span>{popupContent}</span> - {children} - </div> - ), -})) - vi.mock('@/service/use-plugins', () => ({ useFeaturedToolsRecommendations: () => ({ plugins: [], @@ -121,11 +106,13 @@ describe('Tabs', () => { filterElem: <div>filter</div>, } - it('should render start content and disabled tab tooltip text', () => { + it('should render start content and disabled tab tooltip text', async () => { + const user = userEvent.setup() render(<Tabs {...baseProps} />) expect(screen.getByText('start-content'))!.toBeInTheDocument() - expect(screen.getByText('workflow.tabs.startDisabledTip'))!.toBeInTheDocument() + await user.hover(screen.getByText('Blocks')) + expect(await screen.findByText('workflow.tabs.startDisabledTip'))!.toBeInTheDocument() }) it('should switch tabs through click handlers and render tools content with normalized icons', () => { diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 48af942df7..0b38a2df2c 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -6,10 +6,10 @@ import type { ToolWithProvider, } from '../types' import { cn } from '@langgenius/dify-ui/cn' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useSuspenseQuery } from '@tanstack/react-query' import { memo, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { systemFeaturesQueryOptions } from '@/service/system-features' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools' @@ -129,19 +129,22 @@ const TabHeaderItem = ({ if (tab.disabled) { return ( - <Tooltip - key={tab.key} - position="top" - popupClassName="max-w-[200px]" - popupContent={disabledTip} - > - <div - className={className} - aria-disabled={tab.disabled} - onClick={handleClick} - > - {tab.name} - </div> + <Tooltip key={tab.key}> + <TooltipTrigger + render={( + <button + type="button" + className={className} + aria-disabled={tab.disabled} + onClick={handleClick} + > + {tab.name} + </button> + )} + /> + <TooltipContent placement="top" className="max-w-[200px]"> + {disabledTip} + </TooltipContent> </Tooltip> ) } diff --git a/web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx b/web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx index 671459bbbd..8c2ee7f976 100644 --- a/web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx +++ b/web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx @@ -1,22 +1,23 @@ 'use client' import type { FC } from 'react' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { RiAlertFill } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' const McpToolNotSupportTooltip: FC = () => { const { t } = useTranslation() + const tip = t('detailPanel.toolSelector.unsupportedMCPTool', { ns: 'plugin' }) + return ( - <Tooltip - popupContent={( - <div className="w-[256px]"> - {t('detailPanel.toolSelector.unsupportedMCPTool', { ns: 'plugin' })} - </div> - )} - > - <RiAlertFill className="size-4 text-text-warning-secondary" /> - </Tooltip> + <Popover> + <PopoverTrigger openOnHover aria-label={tip} className="inline-flex border-0 bg-transparent p-0"> + <RiAlertFill className="size-4 text-text-warning-secondary" /> + </PopoverTrigger> + <PopoverContent popupClassName="w-[256px] px-3 py-2 system-xs-regular text-text-tertiary"> + {tip} + </PopoverContent> + </Popover> ) } export default React.memo(McpToolNotSupportTooltip) diff --git a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx index 141323e5b3..fc2b328950 100644 --- a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx +++ b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx @@ -2,13 +2,13 @@ import type { FC, ReactNode } from 'react' import { cn } from '@langgenius/dify-ui/cn' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { RiArrowLeftRightLine, RiExternalLinkLine } from '@remixicon/react' import { useBoolean } from 'ahooks' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import { Badge as Badge2, BadgeState } from '@/app/components/base/badge/index' -import Tooltip from '@/app/components/base/tooltip' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' import { pluginManifestToCardPluginProps } from '@/app/components/plugins/install-plugin/utils' import PluginMutationModel from '@/app/components/plugins/plugin-mutation-model' @@ -67,76 +67,91 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => { if (!uniqueIdentifier || !pluginId) return null + const content = ( + <div className={cn('flex w-fit items-center justify-center', className)} onClick={e => e.stopPropagation()}> + {isShowUpdateModal && pluginDetail && ( + <PluginMutationModel + onCancel={hideUpdateModal} + plugin={pluginManifestToCardPluginProps({ + ...pluginDetail.declaration, + icon: icon!, + })} + mutation={mutation} + mutate={install} + confirmButtonText={t('nodes.agent.installPlugin.install', { ns: 'workflow' })} + cancelButtonText={t('nodes.agent.installPlugin.cancel', { ns: 'workflow' })} + modelTitle={t('nodes.agent.installPlugin.title', { ns: 'workflow' })} + description={t('nodes.agent.installPlugin.desc', { ns: 'workflow' })} + cardTitleLeft={( + <> + <Badge2 className="mx-1" size="s" state={BadgeState.Warning}> + {`${pluginDetail.version} -> ${target!.version}`} + </Badge2> + </> + )} + modalBottomLeft={( + <Link + className="flex items-center justify-center gap-1" + href={getMarketplaceUrl(`/plugins/${pluginDetail.declaration.author}/${pluginDetail.declaration.name}`)} + target="_blank" + rel="noopener noreferrer" + > + <span className="system-xs-regular text-xs text-text-accent"> + {t('nodes.agent.installPlugin.changelog', { ns: 'workflow' })} + </span> + <RiExternalLinkLine className="size-3 text-text-accent" /> + </Link> + )} + /> + )} + {pluginDetail && ( + <PluginVersionPicker + isShow={isShow} + onShowChange={setIsShow} + pluginID={pluginId} + currentVersion={pluginDetail.version} + onSelect={(state) => { + setTarget({ + pluginUniqueIden: state.unique_identifier, + version: state.version, + }) + showUpdateModal() + }} + trigger={( + <Badge + className={cn( + 'mx-1 flex hover:bg-state-base-hover', + isShow && 'bg-state-base-hover', + )} + uppercase={true} + text={( + <> + <div>{pluginDetail.version}</div> + <RiArrowLeftRightLine className="ml-1 h-3 w-3 text-text-tertiary" /> + </> + )} + hasRedCornerMark={true} + /> + )} + /> + )} + </div> + ) + + if (!tooltip || isShow || isShowUpdateModal) + return content + return ( - <Tooltip popupContent={!isShow && !isShowUpdateModal && tooltip} triggerMethod="hover"> - <div className={cn('flex w-fit items-center justify-center', className)} onClick={e => e.stopPropagation()}> - {isShowUpdateModal && pluginDetail && ( - <PluginMutationModel - onCancel={hideUpdateModal} - plugin={pluginManifestToCardPluginProps({ - ...pluginDetail.declaration, - icon: icon!, - })} - mutation={mutation} - mutate={install} - confirmButtonText={t('nodes.agent.installPlugin.install', { ns: 'workflow' })} - cancelButtonText={t('nodes.agent.installPlugin.cancel', { ns: 'workflow' })} - modelTitle={t('nodes.agent.installPlugin.title', { ns: 'workflow' })} - description={t('nodes.agent.installPlugin.desc', { ns: 'workflow' })} - cardTitleLeft={( - <> - <Badge2 className="mx-1" size="s" state={BadgeState.Warning}> - {`${pluginDetail.version} -> ${target!.version}`} - </Badge2> - </> - )} - modalBottomLeft={( - <Link - className="flex items-center justify-center gap-1" - href={getMarketplaceUrl(`/plugins/${pluginDetail.declaration.author}/${pluginDetail.declaration.name}`)} - target="_blank" - rel="noopener noreferrer" - > - <span className="system-xs-regular text-xs text-text-accent"> - {t('nodes.agent.installPlugin.changelog', { ns: 'workflow' })} - </span> - <RiExternalLinkLine className="size-3 text-text-accent" /> - </Link> - )} - /> - )} - {pluginDetail && ( - <PluginVersionPicker - isShow={isShow} - onShowChange={setIsShow} - pluginID={pluginId} - currentVersion={pluginDetail.version} - onSelect={(state) => { - setTarget({ - pluginUniqueIden: state.unique_identifier, - version: state.version, - }) - showUpdateModal() - }} - trigger={( - <Badge - className={cn( - 'mx-1 flex hover:bg-state-base-hover', - isShow && 'bg-state-base-hover', - )} - uppercase={true} - text={( - <> - <div>{pluginDetail.version}</div> - <RiArrowLeftRightLine className="ml-1 h-3 w-3 text-text-tertiary" /> - </> - )} - hasRedCornerMark={true} - /> - )} - /> - )} - </div> - </Tooltip> + <Popover> + <PopoverTrigger + openOnHover + nativeButton={false} + aria-label={typeof tooltip === 'string' ? tooltip : t('nodes.agent.installPlugin.title', { ns: 'workflow' })} + render={content} + /> + <PopoverContent popupClassName="px-3 py-2 system-xs-regular text-text-tertiary"> + {tooltip} + </PopoverContent> + </Popover> ) } diff --git a/web/app/components/workflow/nodes/iteration-start/index.tsx b/web/app/components/workflow/nodes/iteration-start/index.tsx index 90a57bef26..ec4f1bfa7f 100644 --- a/web/app/components/workflow/nodes/iteration-start/index.tsx +++ b/web/app/components/workflow/nodes/iteration-start/index.tsx @@ -1,8 +1,8 @@ import type { NodeProps } from 'reactflow' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiHome5Fill } from '@remixicon/react' import { memo } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { NodeSourceHandle } from '@/app/components/workflow/nodes/_base/components/node-handle' const IterationStartNode = ({ id, data }: NodeProps) => { @@ -10,10 +10,14 @@ const IterationStartNode = ({ id, data }: NodeProps) => { return ( <div className="nodrag group mt-1 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg shadow-xs"> - <Tooltip popupContent={t('blocks.iteration-start', { ns: 'workflow' })} asChild={false}> - <div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500"> + <Tooltip> + <TooltipTrigger + aria-label={t('blocks.iteration-start', { ns: 'workflow' })} + className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0" + > <RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" /> - </div> + </TooltipTrigger> + <TooltipContent>{t('blocks.iteration-start', { ns: 'workflow' })}</TooltipContent> </Tooltip> <NodeSourceHandle id={id} @@ -30,10 +34,14 @@ export const IterationStartNodeDumb = () => { return ( <div className="nodrag relative top-[21px] left-[17px] z-11 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg"> - <Tooltip popupContent={t('blocks.iteration-start', { ns: 'workflow' })} asChild={false}> - <div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500"> + <Tooltip> + <TooltipTrigger + aria-label={t('blocks.iteration-start', { ns: 'workflow' })} + className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0" + > <RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" /> - </div> + </TooltipTrigger> + <TooltipContent>{t('blocks.iteration-start', { ns: 'workflow' })}</TooltipContent> </Tooltip> </div> ) diff --git a/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/search-method-option.tsx b/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/search-method-option.tsx index a1f601cce9..54b37f9b52 100644 --- a/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/search-method-option.tsx +++ b/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/search-method-option.tsx @@ -17,7 +17,7 @@ import { import { useTranslation } from 'react-i18next' import WeightedScoreComponent from '@/app/components/app/configuration/dataset-config/params-config/weighted-score' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' -import Tooltip from '@/app/components/base/tooltip' +import { Infotip } from '@/app/components/base/infotip' import { DEFAULT_WEIGHTED_SCORE } from '@/models/datasets' import { HybridSearchModeEnum, @@ -174,10 +174,13 @@ const SearchMethodOption = ({ disabled={readonly} /> {t('modelProvider.rerankModel.key', { ns: 'common' })} - <Tooltip - triggerClassName="ml-0.5 shrink-0 w-3.5 h-3.5" - popupContent={t('modelProvider.rerankModel.tip', { ns: 'common' })} - /> + <Infotip + aria-label={t('modelProvider.rerankModel.tip', { ns: 'common' })} + className="ml-0.5 h-3.5 w-3.5 shrink-0" + iconClassName="h-3.5 w-3.5" + > + {t('modelProvider.rerankModel.tip', { ns: 'common' })} + </Infotip> </div> ) } diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx index 2f5baeb089..61d693de29 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx @@ -5,7 +5,7 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' +import { Infotip } from '@/app/components/base/infotip' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import Collapse from '@/app/components/workflow/nodes/_base/components/collapse' import { MetadataFilteringModeEnum } from '@/app/components/workflow/nodes/knowledge-retrieval/types' @@ -46,13 +46,9 @@ const MetadataFilter = ({ <div className="mr-0.5 system-sm-semibold-uppercase text-text-secondary"> {t('nodes.knowledgeRetrieval.metadata.title', { ns: 'workflow' })} </div> - <Tooltip - popupContent={( - <div className="w-[200px]"> - {t('nodes.knowledgeRetrieval.metadata.tip', { ns: 'workflow' })} - </div> - )} - /> + <Infotip aria-label={t('nodes.knowledgeRetrieval.metadata.tip', { ns: 'workflow' })} popupClassName="w-[200px]"> + {t('nodes.knowledgeRetrieval.metadata.tip', { ns: 'workflow' })} + </Infotip> {collapseIcon} </div> <div className="flex items-center"> diff --git a/web/app/components/workflow/nodes/loop-start/index.tsx b/web/app/components/workflow/nodes/loop-start/index.tsx index a7bd18c7a5..9900b84856 100644 --- a/web/app/components/workflow/nodes/loop-start/index.tsx +++ b/web/app/components/workflow/nodes/loop-start/index.tsx @@ -1,8 +1,8 @@ import type { NodeProps } from 'reactflow' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiHome5Fill } from '@remixicon/react' import { memo } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { NodeSourceHandle } from '@/app/components/workflow/nodes/_base/components/node-handle' const LoopStartNode = ({ id, data }: NodeProps) => { @@ -10,10 +10,14 @@ const LoopStartNode = ({ id, data }: NodeProps) => { return ( <div className="nodrag group mt-1 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg"> - <Tooltip popupContent={t('blocks.loop-start', { ns: 'workflow' })} asChild={false}> - <div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500"> + <Tooltip> + <TooltipTrigger + aria-label={t('blocks.loop-start', { ns: 'workflow' })} + className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0" + > <RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" /> - </div> + </TooltipTrigger> + <TooltipContent>{t('blocks.loop-start', { ns: 'workflow' })}</TooltipContent> </Tooltip> <NodeSourceHandle id={id} @@ -30,10 +34,14 @@ export const LoopStartNodeDumb = () => { return ( <div className="nodrag relative top-[21px] left-[17px] z-11 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg"> - <Tooltip popupContent={t('blocks.loop-start', { ns: 'workflow' })} asChild={false}> - <div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500"> + <Tooltip> + <TooltipTrigger + aria-label={t('blocks.loop-start', { ns: 'workflow' })} + className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0" + > <RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" /> - </div> + </TooltipTrigger> + <TooltipContent>{t('blocks.loop-start', { ns: 'workflow' })}</TooltipContent> </Tooltip> </div> ) diff --git a/web/app/components/workflow/nodes/parameter-extractor/panel.tsx b/web/app/components/workflow/nodes/parameter-extractor/panel.tsx index a116a6303d..9165d53394 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/panel.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/panel.tsx @@ -3,7 +3,7 @@ import type { ParameterExtractorNodeType } from './types' import type { NodePanelProps } from '@/app/components/workflow/types' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' +import { Infotip } from '@/app/components/base/infotip' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse' import Field from '@/app/components/workflow/nodes/_base/components/field' @@ -131,14 +131,14 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({ title={( <div className="flex items-center space-x-1"> <span className="uppercase">{t(`${i18nPrefix}.instruction`, { ns: 'workflow' })}</span> - <Tooltip - popupContent={( - <div className="w-[120px]"> - {t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })} - </div> - )} - triggerClassName="w-3.5 h-3.5 ml-0.5" - /> + <Infotip + aria-label={t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })} + className="ml-0.5 h-3.5 w-3.5" + iconClassName="h-3.5 w-3.5" + popupClassName="w-[120px]" + > + {t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })} + </Infotip> </div> )} value={inputs.instruction} diff --git a/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx index c11f78bc08..ada3fc43cc 100644 --- a/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/question-classifier/__tests__/integration.spec.tsx @@ -344,8 +344,8 @@ describe('question-classifier path', () => { ) expect(screen.getByText(`${longName.slice(0, 50)}...`)).toBeInTheDocument() - await user.hover(screen.getByText(`${longName.slice(0, 50)}...`)) - expect(screen.getByText(longName)).toBeInTheDocument() + await user.hover(screen.getByRole('button', { name: longName })) + expect(await screen.findByText(longName)).toBeInTheDocument() rerender( <Node diff --git a/web/app/components/workflow/nodes/question-classifier/__tests__/node.spec.tsx b/web/app/components/workflow/nodes/question-classifier/__tests__/node.spec.tsx index a7e72c343c..ad411639e9 100644 --- a/web/app/components/workflow/nodes/question-classifier/__tests__/node.spec.tsx +++ b/web/app/components/workflow/nodes/question-classifier/__tests__/node.spec.tsx @@ -1,25 +1,10 @@ import type { QuestionClassifierNodeType, Topic } from '../types' import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' import { BlockEnum } from '@/app/components/workflow/types' import Node from '../node' -vi.mock('@/app/components/base/tooltip', () => ({ - __esModule: true, - default: ({ - children, - popupContent, - }: { - children: React.ReactNode - popupContent: React.ReactNode - }) => ( - <div> - {children} - {popupContent} - </div> - ), -})) - vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useTextGenerationCurrentProviderAndModelAndModelList: vi.fn(), })) @@ -101,7 +86,8 @@ describe('question-classifier/node', () => { expect(screen.getByText('handle-topic-2')).toBeInTheDocument() }) - it('returns nothing when neither model nor classes are configured and truncates long class names', () => { + it('returns nothing when neither model nor classes are configured and truncates long class names', async () => { + const user = userEvent.setup() const longName = 'L'.repeat(60) const { container, rerender } = render( <Node @@ -119,7 +105,8 @@ describe('question-classifier/node', () => { ) expect(screen.getByText(`${longName.slice(0, 50)}...`)).toBeInTheDocument() - expect(screen.getByText(longName)).toBeInTheDocument() + await user.hover(screen.getByRole('button', { name: longName })) + expect(await screen.findByText(longName)).toBeInTheDocument() rerender( <Node diff --git a/web/app/components/workflow/nodes/question-classifier/components/advanced-setting.tsx b/web/app/components/workflow/nodes/question-classifier/components/advanced-setting.tsx index 90d53f7271..d788d2518f 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/advanced-setting.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/advanced-setting.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import type { Memory, Node, NodeOutPutVar } from '@/app/components/workflow/types' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' +import { Infotip } from '@/app/components/base/infotip' import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor' import MemoryConfig from '../../_base/components/memory-config' @@ -48,14 +48,14 @@ const AdvancedSetting: FC<Props> = ({ title={( <div className="flex items-center space-x-1"> <span className="uppercase">{t(`${i18nPrefix}.instruction`, { ns: 'workflow' })}</span> - <Tooltip - popupContent={( - <div className="w-[120px]"> - {t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })} - </div> - )} - triggerClassName="w-3.5 h-3.5 ml-0.5" - /> + <Infotip + aria-label={t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })} + className="ml-0.5 h-3.5 w-3.5" + iconClassName="h-3.5 w-3.5" + popupClassName="w-[120px]" + > + {t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })} + </Infotip> </div> )} value={instruction} diff --git a/web/app/components/workflow/nodes/question-classifier/node.tsx b/web/app/components/workflow/nodes/question-classifier/node.tsx index 2aae8debcf..305eacc204 100644 --- a/web/app/components/workflow/nodes/question-classifier/node.tsx +++ b/web/app/components/workflow/nodes/question-classifier/node.tsx @@ -2,9 +2,9 @@ import type { TFunction } from 'i18next' import type { FC } from 'react' import type { NodeProps } from 'reactflow' import type { QuestionClassifierNodeType } from './types' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { useTextGenerationCurrentProviderAndModelAndModelList, } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -47,15 +47,18 @@ const TruncatedClassItem: FC<TruncatedClassItemProps> = ({ topic, index, nodeId, </div> {shouldShowTooltip ? ( - <Tooltip - popupContent={( - <div className="max-w-[300px] wrap-break-word"> - <ReadonlyInputWithSelectVar value={topic.name} nodeId={nodeId} /> - </div> - )} - > - {content} - </Tooltip> + <Popover> + <PopoverTrigger + openOnHover + aria-label={topic.name} + className="w-full border-0 bg-transparent p-0 text-left" + > + {content} + </PopoverTrigger> + <PopoverContent popupClassName="max-w-[300px] px-3 py-2 system-xs-regular wrap-break-word text-text-tertiary"> + <ReadonlyInputWithSelectVar value={topic.name} nodeId={nodeId} /> + </PopoverContent> + </Popover> ) : content} </div> diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx index 44eb9d44c6..3cae373d48 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx +++ b/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx @@ -9,7 +9,7 @@ import { RiBracesLine, } from '@remixicon/react' import { useBoolean } from 'ahooks' -import Tooltip from '@/app/components/base/tooltip' +import { Infotip } from '@/app/components/base/infotip' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { SchemaModal } from '@/app/components/plugins/plugin-detail-panel/tool-selector/components' @@ -57,15 +57,13 @@ const TriggerFormItem: FC<Props> = ({ <div className="ml-1 system-xs-regular text-text-destructive-secondary">*</div> )} {!showDescription && tooltip && ( - <Tooltip - popupContent={( - <div className="w-[200px]"> - {tooltip[language] || tooltip.en_US} - </div> - )} - triggerClassName="ml-1 w-4 h-4" - asChild={false} - /> + <Infotip + aria-label={tooltip[language] || tooltip.en_US} + className="ml-1 h-4 w-4" + popupClassName="w-[200px]" + > + {tooltip[language] || tooltip.en_US} + </Infotip> )} {showSchemaButton && ( <> diff --git a/web/app/components/workflow/nodes/trigger-webhook/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/trigger-webhook/__tests__/panel.spec.tsx index a1f5f1e2c8..4d60e77ba2 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/__tests__/panel.spec.tsx @@ -94,10 +94,6 @@ vi.mock('@/app/components/base/input-with-copy', () => ({ ), })) -vi.mock('@/app/components/base/tooltip', () => ({ - default: ({ children }: { children: React.ReactNode }) => <>{children}</>, -})) - vi.mock('@/app/components/workflow/nodes/_base/components/field', () => ({ default: ({ title, children }: { title: string, children: React.ReactNode }) => ( <div> diff --git a/web/app/components/workflow/nodes/trigger-webhook/panel.tsx b/web/app/components/workflow/nodes/trigger-webhook/panel.tsx index fb6bfacf38..53498c52f2 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/panel.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/panel.tsx @@ -11,12 +11,12 @@ import { } from '@langgenius/dify-ui/number-field' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select' import { toast } from '@langgenius/dify-ui/toast' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import copy from 'copy-to-clipboard' import * as React from 'react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import InputWithCopy from '@/app/components/base/input-with-copy' -import Tooltip from '@/app/components/base/tooltip' import Field from '@/app/components/workflow/nodes/_base/components/field' import OutputVars from '@/app/components/workflow/nodes/_base/components/output-vars' import Split from '@/app/components/workflow/nodes/_base/components/split' @@ -118,32 +118,38 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({ </div> {inputs.webhook_debug_url && ( <div className="space-y-2"> - <Tooltip - popupContent={debugUrlCopied ? t(`${i18nPrefix}.debugUrlCopied`, { ns: 'workflow' }) : t(`${i18nPrefix}.debugUrlCopy`, { ns: 'workflow' })} - popupClassName="system-xs-regular text-text-primary bg-components-tooltip-bg border border-components-panel-border shadow-lg backdrop-blur-xs rounded-md px-1.5 py-1" - position="top" - offset={{ mainAxis: -20 }} - needsDelay={true} - > - <div - className="flex cursor-pointer gap-1.5 rounded-lg px-1 py-1.5 transition-colors" - style={{ width: '368px', height: '38px' }} - onClick={() => { - copy(inputs.webhook_debug_url || '') - setDebugUrlCopied(true) - setTimeout(() => setDebugUrlCopied(false), 2000) - }} + <Tooltip> + <TooltipTrigger + render={( + <button + type="button" + aria-label={t(`${i18nPrefix}.debugUrlCopy`, { ns: 'workflow' })} + className="flex cursor-pointer gap-1.5 rounded-lg px-1 py-1.5 text-left transition-colors" + style={{ width: '368px', height: '38px' }} + onClick={() => { + copy(inputs.webhook_debug_url || '') + setDebugUrlCopied(true) + setTimeout(() => setDebugUrlCopied(false), 2000) + }} + > + <span className="mt-0.5 w-0.5 bg-divider-regular" style={{ height: '28px' }} /> + <span className="flex-1" style={{ width: '352px', height: '32px' }}> + <span className="block text-xs leading-4 text-text-tertiary"> + {t(`${i18nPrefix}.debugUrlTitle`, { ns: 'workflow' })} + </span> + <span className="block truncate text-xs leading-4 text-text-primary"> + {inputs.webhook_debug_url} + </span> + </span> + </button> + )} + /> + <TooltipContent + placement="top" + className="rounded-md border border-components-panel-border bg-components-tooltip-bg px-1.5 py-1 system-xs-regular text-text-primary shadow-lg backdrop-blur-xs" > - <div className="mt-0.5 w-0.5 bg-divider-regular" style={{ height: '28px' }}></div> - <div className="flex-1" style={{ width: '352px', height: '32px' }}> - <div className="text-xs leading-4 text-text-tertiary"> - {t(`${i18nPrefix}.debugUrlTitle`, { ns: 'workflow' })} - </div> - <div className="truncate text-xs leading-4 text-text-primary"> - {inputs.webhook_debug_url} - </div> - </div> - </div> + {debugUrlCopied ? t(`${i18nPrefix}.debugUrlCopied`, { ns: 'workflow' }) : t(`${i18nPrefix}.debugUrlCopy`, { ns: 'workflow' })} + </TooltipContent> </Tooltip> {isPrivateOrLocalAddress(inputs.webhook_debug_url) && ( <div className="mt-1 px-0 py-[2px] system-xs-regular text-text-warning"> diff --git a/web/app/components/workflow/panel/env-panel/variable-modal.tsx b/web/app/components/workflow/panel/env-panel/variable-modal.tsx index 267c014e1d..2560ab968e 100644 --- a/web/app/components/workflow/panel/env-panel/variable-modal.tsx +++ b/web/app/components/workflow/panel/env-panel/variable-modal.tsx @@ -7,8 +7,8 @@ import * as React from 'react' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { v4 as uuid4 } from 'uuid' +import { Infotip } from '@/app/components/base/infotip' import Input from '@/app/components/base/input' -import Tooltip from '@/app/components/base/tooltip' import { useWorkflowStore } from '@/app/components/workflow/store' import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' @@ -129,14 +129,14 @@ const VariableModal = ({ onClick={() => setType('secret')} > <span>Secret</span> - <Tooltip - popupContent={( - <div className="w-[240px]"> - {t('env.modal.secretTip', { ns: 'workflow' })} - </div> - )} - triggerClassName="ml-0.5 w-3.5 h-3.5" - /> + <Infotip + aria-label={t('env.modal.secretTip', { ns: 'workflow' })} + className="ml-0.5 h-3.5 w-3.5" + iconClassName="h-3.5 w-3.5" + popupClassName="w-[240px]" + > + {t('env.modal.secretTip', { ns: 'workflow' })} + </Infotip> </div> </div> </div> diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx index a87922eb54..85607a1342 100644 --- a/web/app/components/workflow/run/node.tsx +++ b/web/app/components/workflow/run/node.tsx @@ -17,7 +17,7 @@ import { RiLoader2Line, RiPauseCircleFill, } from '@remixicon/react' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip' @@ -68,16 +68,6 @@ const NodePanel: FC<Props> = ({ return doSetCollapseState(state) }, [hideProcessDetail]) - const titleRef = useRef<HTMLDivElement>(null) - const [isTooltipOpen, setIsTooltipOpen] = useState(false) - const handleTooltipOpenChange = useCallback((open: boolean) => { - if (open) { - const el = titleRef.current - if (!el || el.scrollWidth <= el.clientWidth) - return - } - setIsTooltipOpen(open) - }, []) const { t } = useTranslation() const docLink = useDocLink() @@ -142,11 +132,10 @@ const NodePanel: FC<Props> = ({ /> )} <BlockIcon size={inMessage ? 'xs' : 'sm'} className={cn('mr-2 shrink-0', inMessage && 'mr-1!')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} /> - <Tooltip open={isTooltipOpen} onOpenChange={handleTooltipOpenChange}> + <Tooltip> <TooltipTrigger render={( <div - ref={titleRef} className={cn( 'min-w-0 grow truncate system-xs-semibold-uppercase text-text-secondary', hideInfo && 'text-xs!', diff --git a/web/app/components/workflow/variable-inspect/listening.tsx b/web/app/components/workflow/variable-inspect/listening.tsx index 3994355d58..cf702a623a 100644 --- a/web/app/components/workflow/variable-inspect/listening.tsx +++ b/web/app/components/workflow/variable-inspect/listening.tsx @@ -4,12 +4,12 @@ import type { Node } from 'reactflow' import type { ScheduleTriggerNodeType } from '@/app/components/workflow/nodes/trigger-schedule/types' import type { WebhookTriggerNodeType } from '@/app/components/workflow/nodes/trigger-webhook/types' import { Button } from '@langgenius/dify-ui/button' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import copy from 'copy-to-clipboard' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStoreApi } from 'reactflow' import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' -import Tooltip from '@/app/components/base/tooltip' import BlockIcon from '@/app/components/workflow/block-icon' import { useGetToolIcon } from '@/app/components/workflow/hooks/use-tool-icon' import { getNextExecutionTime } from '@/app/components/workflow/nodes/trigger-schedule/utils/execution-time-calculator' @@ -179,28 +179,32 @@ const Listening: FC<ListeningProps> = ({ <div className="shrink-0 system-xs-regular whitespace-pre-line text-text-tertiary"> {t('nodes.triggerWebhook.debugUrlTitle', { ns: 'workflow' })} </div> - <Tooltip - popupContent={debugUrlCopied - ? t('nodes.triggerWebhook.debugUrlCopied', { ns: 'workflow' }) - : t('nodes.triggerWebhook.debugUrlCopy', { ns: 'workflow' })} - popupClassName="system-xs-regular text-text-primary bg-components-tooltip-bg border border-components-panel-border shadow-lg backdrop-blur-xs rounded-md px-1.5 py-1" - position="top" - offset={{ mainAxis: -4 }} - needsDelay={true} - > - <button - type="button" - aria-label={t('nodes.triggerWebhook.debugUrlCopy', { ns: 'workflow' }) || ''} - className={`inline-flex items-center rounded-md border border-divider-regular bg-components-badge-white-to-dark px-1.5 py-[2px] font-mono text-[13px] leading-[18px] text-text-secondary transition-colors hover:bg-components-panel-on-panel-item-bg-hover focus:outline-hidden focus-visible:outline-2 focus-visible:outline-components-panel-border focus-visible:outline-solid ${debugUrlCopied ? 'bg-components-panel-on-panel-item-bg-hover text-text-primary' : ''}`} - onClick={() => { - copy(webhookDebugUrl) - setDebugUrlCopied(true) - }} + <Tooltip> + <TooltipTrigger + render={( + <button + type="button" + aria-label={t('nodes.triggerWebhook.debugUrlCopy', { ns: 'workflow' }) || ''} + className={`inline-flex items-center rounded-md border border-divider-regular bg-components-badge-white-to-dark px-1.5 py-[2px] font-mono text-[13px] leading-[18px] text-text-secondary transition-colors hover:bg-components-panel-on-panel-item-bg-hover focus:outline-hidden focus-visible:outline-2 focus-visible:outline-components-panel-border focus-visible:outline-solid ${debugUrlCopied ? 'bg-components-panel-on-panel-item-bg-hover text-text-primary' : ''}`} + onClick={() => { + copy(webhookDebugUrl) + setDebugUrlCopied(true) + }} + > + <span className="whitespace-nowrap text-text-primary"> + {webhookDebugUrl} + </span> + </button> + )} + /> + <TooltipContent + placement="top" + className="rounded-md border border-components-panel-border bg-components-tooltip-bg px-1.5 py-1 system-xs-regular text-text-primary shadow-lg backdrop-blur-xs" > - <span className="whitespace-nowrap text-text-primary"> - {webhookDebugUrl} - </span> - </button> + {debugUrlCopied + ? t('nodes.triggerWebhook.debugUrlCopied', { ns: 'workflow' }) + : t('nodes.triggerWebhook.debugUrlCopy', { ns: 'workflow' })} + </TooltipContent> </Tooltip> </div> )} diff --git a/web/app/components/workflow/workflow-preview/components/nodes/base.tsx b/web/app/components/workflow/workflow-preview/components/nodes/base.tsx index 34c8d753ce..5bfbd52561 100644 --- a/web/app/components/workflow/workflow-preview/components/nodes/base.tsx +++ b/web/app/components/workflow/workflow-preview/components/nodes/base.tsx @@ -6,12 +6,12 @@ import type { NodeProps, } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { cloneElement, memo, } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import BlockIcon from '@/app/components/workflow/block-icon' import { BlockEnum, @@ -91,19 +91,21 @@ const BaseCard = ({ </div> { data.type === BlockEnum.Iteration && (data as IterationNodeType).is_parallel && ( - <Tooltip popupContent={( - <div className="w-[180px]"> + <Popover> + <PopoverTrigger + openOnHover + aria-label={t('nodes.iteration.parallelModeEnableTitle', { ns: 'workflow' })} + className="ml-1 flex items-center justify-center rounded-[5px] border border-text-warning bg-transparent px-[5px] py-[3px] system-2xs-medium-uppercase text-text-warning" + > + {t('nodes.iteration.parallelModeUpper', { ns: 'workflow' })} + </PopoverTrigger> + <PopoverContent popupClassName="w-[180px] px-3 py-2 system-xs-regular text-text-tertiary"> <div className="font-extrabold"> {t('nodes.iteration.parallelModeEnableTitle', { ns: 'workflow' })} </div> {t('nodes.iteration.parallelModeEnableDesc', { ns: 'workflow' })} - </div> - )} - > - <div className="ml-1 flex items-center justify-center rounded-[5px] border border-text-warning px-[5px] py-[3px] system-2xs-medium-uppercase text-text-warning"> - {t('nodes.iteration.parallelModeUpper', { ns: 'workflow' })} - </div> - </Tooltip> + </PopoverContent> + </Popover> ) } </div> diff --git a/web/app/components/workflow/workflow-preview/components/nodes/iteration-start/index.tsx b/web/app/components/workflow/workflow-preview/components/nodes/iteration-start/index.tsx index 6a69e5f2aa..391150649f 100644 --- a/web/app/components/workflow/workflow-preview/components/nodes/iteration-start/index.tsx +++ b/web/app/components/workflow/workflow-preview/components/nodes/iteration-start/index.tsx @@ -1,8 +1,8 @@ import type { NodeProps } from 'reactflow' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiHome5Fill } from '@remixicon/react' import { memo } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { NodeSourceHandle } from '../../node-handle' const IterationStartNode = ({ id, data }: NodeProps) => { @@ -10,10 +10,14 @@ const IterationStartNode = ({ id, data }: NodeProps) => { return ( <div className="nodrag group mt-1 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg shadow-xs"> - <Tooltip popupContent={t('blocks.iteration-start', { ns: 'workflow' })} asChild={false}> - <div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500"> + <Tooltip> + <TooltipTrigger + aria-label={t('blocks.iteration-start', { ns: 'workflow' })} + className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0" + > <RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" /> - </div> + </TooltipTrigger> + <TooltipContent>{t('blocks.iteration-start', { ns: 'workflow' })}</TooltipContent> </Tooltip> <NodeSourceHandle id={id} diff --git a/web/app/components/workflow/workflow-preview/components/nodes/loop-start/index.tsx b/web/app/components/workflow/workflow-preview/components/nodes/loop-start/index.tsx index e67c0d9f10..67865be470 100644 --- a/web/app/components/workflow/workflow-preview/components/nodes/loop-start/index.tsx +++ b/web/app/components/workflow/workflow-preview/components/nodes/loop-start/index.tsx @@ -1,8 +1,8 @@ import type { NodeProps } from 'reactflow' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiHome5Fill } from '@remixicon/react' import { memo } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { NodeSourceHandle } from '../../node-handle' const LoopStartNode = ({ id, data }: NodeProps) => { @@ -10,10 +10,14 @@ const LoopStartNode = ({ id, data }: NodeProps) => { return ( <div className="nodrag group mt-1 flex h-11 w-11 items-center justify-center rounded-2xl border border-workflow-block-border bg-workflow-block-bg"> - <Tooltip popupContent={t('blocks.loop-start', { ns: 'workflow' })} asChild={false}> - <div className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500"> + <Tooltip> + <TooltipTrigger + aria-label={t('blocks.loop-start', { ns: 'workflow' })} + className="flex h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-components-panel-border-subtle bg-util-colors-blue-brand-blue-brand-500 p-0" + > <RiHome5Fill className="h-3 w-3 text-text-primary-on-surface" /> - </div> + </TooltipTrigger> + <TooltipContent>{t('blocks.loop-start', { ns: 'workflow' })}</TooltipContent> </Tooltip> <NodeSourceHandle id={id} diff --git a/web/docs/overlay-migration.md b/web/docs/overlay-migration.md index b849159867..3d94d82e64 100644 --- a/web/docs/overlay-migration.md +++ b/web/docs/overlay-migration.md @@ -7,7 +7,6 @@ This document tracks the Dify-web migration away from legacy overlay APIs. ## Scope - Deprecated imports: - - `@/app/components/base/tooltip` - `@/app/components/base/modal` - `@/app/components/base/dialog` - `@/app/components/base/drawer` @@ -36,6 +35,8 @@ This document tracks the Dify-web migration away from legacy overlay APIs. 1. Business/UI features outside `app/components/base/**` - Migrate old calls to semantic primitives from `@langgenius/dify-ui/*`. - Keep deprecated imports out of newly touched files. + - Use `@langgenius/dify-ui/tooltip` only for short, non-interactive labels where the trigger already has its own accessible name. + - Use `@langgenius/dify-ui/popover` or the web `Infotip` wrapper for explanatory, long-form, structured, or interactive content. 1. Legacy base components - Migrate legacy base callers gradually. - Keep deprecated imports out of newly touched files. @@ -75,6 +76,9 @@ back to `z-9999`. parent legacy overlay should be migrated instead. - When migrating a legacy overlay that has a high z-index, remove the z-index entirely — the new primitive's default `z-1002` handles it. +- When using Base UI trigger `render`, render a real `button` for button-like + triggers. If the trigger must render a non-button element, the primitive must + explicitly opt out of the native button behavior where that API is available. ### Post-migration cleanup diff --git a/web/eslint.constants.mjs b/web/eslint.constants.mjs index f74c5c9115..eb85d5d902 100644 --- a/web/eslint.constants.mjs +++ b/web/eslint.constants.mjs @@ -45,13 +45,6 @@ export const WEB_RESTRICTED_IMPORT_PATTERNS = [ ] export const OVERLAY_RESTRICTED_IMPORT_PATTERNS = [ - { - group: [ - '**/base/tooltip', - '**/base/tooltip/index', - ], - message: 'Deprecated: use @langgenius/dify-ui/tooltip instead. See issue #32767.', - }, { group: [ '**/base/modal',