From fd9e774a29d82e6a7043c12dbe1c6cdd1a992361 Mon Sep 17 00:00:00 2001
From: Kelly Yang <124ykl@gmail.com>
Date: Wed, 4 Mar 2026 16:35:38 -0800
Subject: [PATCH] feat(ui): add copy button to read-only textarea widget on
hover (#9331)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 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
┆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
Co-authored-by: Alexander Brown
Co-authored-by: Dante
Co-authored-by: Alexander Brown
---
.../widgets/components/WidgetTextarea.test.ts | 50 ++++++++++++++++++-
.../widgets/components/WidgetTextarea.vue | 21 +++++++-
2 files changed, 69 insertions(+), 2 deletions(-)
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts
index 15115917c0..5e6ef628b0 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts
@@ -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 () => {
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue
index d01ae7c437..dcb42db6cc 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue
@@ -2,7 +2,7 @@
+