feat(ui): add copy button to read-only textarea widget on hover (#9331)

## Summary

Added a `copy-to-clipboard` button that appears when hovering over
read-only textarea widgets to improve user experience.

## Changes

- **What**: Added a copy button utilizing `useCopyToClipboard` to
[WidgetTextarea.vue](cci:7://file:///Users/kelly/Documents/comfyui/ComfyUI_frontend/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:0:0-0:0)
that only displays when the widget is read-only and hovered.

## Screenshots 
<img width="670" height="498" alt="e30362fdc6792f3a955f3415f0f42afb"
src="https://github.com/user-attachments/assets/1b7ec5dc-3733-48b6-9708-6ae56926054a"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9331-feat-ui-add-copy-button-to-read-only-textarea-widget-on-hover-3176d73d36508159a339d567b5c33591)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: Alexander Brown <DrJKL0424@gmail.com>
Co-authored-by: Dante <bunggl@naver.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
Kelly Yang
2026-03-04 16:35:38 -08:00
committed by GitHub
parent 7b316eb9a2
commit fd9e774a29
2 changed files with 69 additions and 2 deletions

View File

@@ -1,10 +1,18 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import WidgetTextarea from './WidgetTextarea.vue'
const mockCopyToClipboard = vi.hoisted(() => vi.fn())
vi.mock('@/composables/useCopyToClipboard', () => ({
useCopyToClipboard: vi.fn().mockReturnValue({
copyToClipboard: mockCopyToClipboard
})
}))
function createMockWidget(
value: string = 'default text',
options: SimplifiedWidget['options'] = {},
@@ -31,6 +39,11 @@ function mountComponent(
modelValue,
readonly,
placeholder
},
global: {
mocks: {
$t: (msg: string) => msg
}
}
})
}
@@ -190,6 +203,41 @@ describe('WidgetTextarea Value Binding', () => {
expect(textarea.attributes('placeholder')).toBe('Custom placeholder')
})
})
describe('Copy Button Behavior', () => {
beforeEach(() => {
mockCopyToClipboard.mockClear()
})
it('hides copy button when not read-only', async () => {
const widget = createMockWidget('test')
const wrapper = mountComponent(widget, 'test', false)
const button = wrapper.find('button')
expect(button.exists()).toBe(false)
})
it('copy button has invisible class by default when read-only', () => {
const widget = createMockWidget('test', { read_only: true })
const wrapper = mountComponent(widget, 'test', true)
const button = wrapper.find('button')
expect(button.exists()).toBe(true)
expect(button.classes()).toContain('invisible')
})
it('copy button has group-hover:visible class when read-only, and copies on click', async () => {
const widget = createMockWidget('test value', { read_only: true })
const wrapper = mountComponent(widget, 'test value', true)
const button = wrapper.find('button')
expect(button.exists()).toBe(true)
expect(button.classes()).toContain('group-hover:visible')
await button.trigger('click')
expect(mockCopyToClipboard).toHaveBeenCalledWith('test value')
})
})
describe('Edge Cases', () => {
it('handles very long text', async () => {