diff --git a/web/app/components/plugins/base/__tests__/key-value-item.spec.tsx b/web/app/components/plugins/base/__tests__/key-value-item.spec.tsx
index 8575eabf626..e238bcde0b4 100644
--- a/web/app/components/plugins/base/__tests__/key-value-item.spec.tsx
+++ b/web/app/components/plugins/base/__tests__/key-value-item.spec.tsx
@@ -1,4 +1,5 @@
-import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { cleanup, render, screen, within } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import KeyValueItem from '../key-value-item'
@@ -34,9 +35,20 @@ describe('KeyValueItem', () => {
expect(screen.queryByText('sk-secret')).not.toBeInTheDocument()
})
- it('copies actual value (not masked) when copy button is clicked', () => {
+ it('associates the label with the copy action and announces the result', async () => {
+ vi.useRealTimers()
+ const user = userEvent.setup()
render()
- fireEvent.click(screen.getByRole('button', { name: 'common.operation.copy' }))
+
+ const keyGroup = screen.getByRole('group', { name: 'Key' })
+ const copyButton = within(keyGroup).getByRole('button', {
+ name: 'common.operation.copy: Key',
+ })
+
+ await user.click(copyButton)
+
expect(mockCopy).toHaveBeenCalledWith('sk-secret')
+ expect(copyButton).toHaveAccessibleName('common.operation.copy: Key')
+ expect(within(keyGroup).getByRole('status')).toHaveTextContent('common.operation.copied: Key')
})
})
diff --git a/web/app/components/plugins/base/key-value-item.tsx b/web/app/components/plugins/base/key-value-item.tsx
index 9e288e5e821..eae60f88c77 100644
--- a/web/app/components/plugins/base/key-value-item.tsx
+++ b/web/app/components/plugins/base/key-value-item.tsx
@@ -3,7 +3,7 @@ import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import copy from 'copy-to-clipboard'
-import { useCallback, useEffect, useState } from 'react'
+import { useCallback, useEffect, useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { CopyCheck } from '../../base/icons/src/vender/line/files'
@@ -24,6 +24,7 @@ function KeyValueItem({
}: Props) {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState(false)
+ const labelId = useId()
const handleCopy = useCallback(() => {
copy(value)
setIsCopied(true)
@@ -40,11 +41,16 @@ function KeyValueItem({
}
}, [isCopied])
- const copyLabel = t(($) => $[`operation.${isCopied ? 'copied' : 'copy'}`], { ns: 'common' })
+ const copiedLabel = t(($) => $['operation.copied'], { ns: 'common' })
+ const copyLabel = t(($) => $['operation.copy'], { ns: 'common' })
+ const copyButtonLabel = `${copyLabel}: ${label}`
+ const copyStatus = `${copiedLabel}: ${label}`
+ const tooltipLabel = isCopied ? copiedLabel : copyLabel
return (
-
+
@@ -78,9 +84,12 @@ function KeyValueItem({
}
/>
- {copyLabel}
+ {tooltipLabel}
+
+ {isCopied ? copyStatus : ''}
+
)
}