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 e30362fdc6792f3a955f3415f0f42afb ┆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 @@
+