mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 01:20:03 +00:00
feat: show ComfyUI context menu on textarea widget right-click (#9840)
## Summary Right-clicking a textarea widget (e.g. text node) shows the browser's native context menu instead of ComfyUI's context menu, preventing access to promote/un-promote options in subgraphs. ## Changes - **What**: Replace `@contextmenu.capture.stop` on `WidgetTextarea.vue`'s `<Textarea>` with a handler implementing double-right-click toggling: first right-click shows ComfyUI's context menu, second right-click (while menu is open) allows browser native menu. Exposes `isNodeOptionsOpen()` from `useMoreOptionsMenu.ts` to check menu state. ## Review Focus The capture-phase handler in `WidgetTextarea.vue` only changes `contextmenu` handling — pointer event modifiers (`pointerdown/move/up.capture.stop`) that prevent canvas panning are untouched. The double-right-click pattern matches Notion/YouTube behavior for editable text fields. <!-- Pipeline-Ticket: d7a53160-e1e1-42bb-a5ac-c0c2702c629c --> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9840-fix-show-ComfyUI-context-menu-on-textarea-widget-right-click-3216d73d36508102b4c9c13a5915bc48) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
@@ -80,8 +80,12 @@ export function showNodeOptions(
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the node options popover
|
||||
* Check if the node options menu is currently open
|
||||
*/
|
||||
export function isNodeOptionsOpen(): boolean {
|
||||
return nodeOptionsInstance?.isOpen.value ?? false
|
||||
}
|
||||
|
||||
interface NodeOptionsInstance {
|
||||
toggle: (event: Event) => void
|
||||
show: (event: MouseEvent) => void
|
||||
|
||||
@@ -7,6 +7,7 @@ import WidgetTextarea from './WidgetTextarea.vue'
|
||||
import { createMockWidget } from './widgetTestUtils'
|
||||
|
||||
const mockCopyToClipboard = vi.hoisted(() => vi.fn())
|
||||
const mockIsNodeOptionsOpen = vi.hoisted(() => vi.fn(() => false))
|
||||
|
||||
vi.mock('@/composables/useCopyToClipboard', () => ({
|
||||
useCopyToClipboard: vi.fn().mockReturnValue({
|
||||
@@ -14,6 +15,10 @@ vi.mock('@/composables/useCopyToClipboard', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/graph/useMoreOptionsMenu', () => ({
|
||||
isNodeOptionsOpen: mockIsNodeOptionsOpen
|
||||
}))
|
||||
|
||||
function createTextareaWidget(
|
||||
value: string = 'default text',
|
||||
options: SimplifiedWidget<string>['options'] = {},
|
||||
@@ -277,3 +282,43 @@ describe('WidgetTextarea Value Binding', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('WidgetTextarea contextmenu', () => {
|
||||
it('prevents browser menu on first right-click (menu closed)', () => {
|
||||
mockIsNodeOptionsOpen.mockReturnValue(false)
|
||||
const widget = createTextareaWidget('test')
|
||||
const wrapper = mountComponent(widget, 'test')
|
||||
const textarea = wrapper.find('textarea')
|
||||
|
||||
const event = new MouseEvent('contextmenu', {
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
const preventDefaultSpy = vi.spyOn(event, 'preventDefault')
|
||||
const stopPropagationSpy = vi.spyOn(event, 'stopPropagation')
|
||||
|
||||
textarea.element.dispatchEvent(event)
|
||||
|
||||
expect(preventDefaultSpy).toHaveBeenCalled()
|
||||
expect(stopPropagationSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('allows browser menu on second right-click (menu open)', () => {
|
||||
mockIsNodeOptionsOpen.mockReturnValue(true)
|
||||
const widget = createTextareaWidget('test')
|
||||
const wrapper = mountComponent(widget, 'test')
|
||||
const textarea = wrapper.find('textarea')
|
||||
|
||||
const event = new MouseEvent('contextmenu', {
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
const preventDefaultSpy = vi.spyOn(event, 'preventDefault')
|
||||
const stopPropagationSpy = vi.spyOn(event, 'stopPropagation')
|
||||
|
||||
textarea.element.dispatchEvent(event)
|
||||
|
||||
expect(preventDefaultSpy).not.toHaveBeenCalled()
|
||||
expect(stopPropagationSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
@pointerdown.capture.stop
|
||||
@pointermove.capture.stop
|
||||
@pointerup.capture.stop
|
||||
@contextmenu.capture.stop
|
||||
@contextmenu.capture="handleContextMenu"
|
||||
/>
|
||||
<Button
|
||||
v-if="isReadOnly"
|
||||
@@ -54,6 +54,7 @@ import { computed, useId } from 'vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Textarea from '@/components/ui/textarea/Textarea.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { isNodeOptionsOpen } from '@/composables/graph/useMoreOptionsMenu'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { useHideLayoutField } from '@/types/widgetTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -85,6 +86,14 @@ const isReadOnly = computed(() =>
|
||||
Boolean(widget.options?.read_only || widget.options?.disabled)
|
||||
)
|
||||
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
if (isNodeOptionsOpen()) {
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
function handleCopy() {
|
||||
copyToClipboard(modelValue.value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user