[backport core/1.43] fix: prevent node context menu from overflowing viewport on desktop (#11122)

Backport of #10854 to `core/1.43`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11122-backport-core-1-43-fix-prevent-node-context-menu-from-overflowing-viewport-on-desktop-33e6d73d365081898904f66316bab150)
by [Unito](https://www.unito.io)

Co-authored-by: Dante <bunggl@naver.com>
This commit is contained in:
Comfy Org PR Bot
2026-04-14 03:50:59 +09:00
committed by GitHub
parent 924da682ab
commit e6a59dcdc2
2 changed files with 123 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
test.describe(
'Node context menu viewport overflow (#10824)',
{ tag: '@ui' },
() => {
test.beforeEach(async ({ comfyPage }) => {
// Keep the viewport well below the menu content height so overflow is guaranteed.
await comfyPage.page.setViewportSize({ width: 1280, height: 520 })
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.nextFrame()
})
async function openMoreOptions(comfyPage: ComfyPage) {
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found')
}
// Drag the KSampler toward the lower-left so the menu has limited space below it.
const nodePos = await ksamplerNodes[0].getPosition()
const viewportSize = comfyPage.page.viewportSize()!
const centerX = viewportSize.width / 3
const centerY = viewportSize.height * 0.75
await comfyPage.canvasOps.dragAndDrop(
{ x: nodePos.x, y: nodePos.y },
{ x: centerX, y: centerY }
)
await comfyPage.nextFrame()
await ksamplerNodes[0].click('title')
await comfyPage.nextFrame()
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({
timeout: 5000
})
const moreOptionsBtn = comfyPage.page.locator(
'[data-testid="more-options-button"]'
)
await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 })
await moreOptionsBtn.click()
await comfyPage.nextFrame()
const menu = comfyPage.page.locator('.p-contextmenu')
await expect(menu).toBeVisible({ timeout: 3000 })
// Wait for constrainMenuHeight (runs via requestAnimationFrame in onMenuShow)
await comfyPage.nextFrame()
return menu
}
test('last menu item "Remove" is reachable via scroll', async ({
comfyPage
}) => {
const menu = await openMoreOptions(comfyPage)
const rootList = menu.locator(':scope > ul')
await expect
.poll(
() => rootList.evaluate((el) => el.scrollHeight > el.clientHeight),
{
message:
'Menu should overflow vertically so this test exercises the viewport clamp',
timeout: 3000
}
)
.toBe(true)
// "Remove" is the last item in the More Options menu.
// It must become reachable by scrolling the bounded menu list.
const removeItem = menu.getByText('Remove', { exact: true })
const didScroll = await rootList.evaluate((el) => {
const previousScrollTop = el.scrollTop
el.scrollTo({ top: el.scrollHeight })
return el.scrollTop > previousScrollTop
})
expect(didScroll).toBe(true)
await expect(removeItem).toBeVisible()
})
test('last menu item "Remove" is clickable and removes the node', async ({
comfyPage
}) => {
const menu = await openMoreOptions(comfyPage)
const removeItem = menu.getByText('Remove', { exact: true })
await removeItem.scrollIntoViewIfNeeded()
await removeItem.click()
await comfyPage.nextFrame()
// The node should be removed from the graph
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 3000 })
.toBe(0)
})
}
)

View File

@@ -260,8 +260,26 @@ function handleColorSelect(subOption: SubMenuOption) {
hide()
}
function constrainMenuHeight() {
const menuInstance = contextMenu.value as unknown as {
container?: HTMLElement
}
const rootList = menuInstance?.container?.querySelector(
':scope > ul'
) as HTMLElement | null
if (!rootList) return
const rect = rootList.getBoundingClientRect()
const maxHeight = window.innerHeight - rect.top - 8
if (maxHeight > 0) {
rootList.style.maxHeight = `${maxHeight}px`
rootList.style.overflowY = 'auto'
}
}
function onMenuShow() {
isOpen.value = true
requestAnimationFrame(constrainMenuHeight)
}
function onMenuHide() {