fix(web): position of selection context menu in workflow graph

This commit is contained in:
JzoNg 2026-03-25 22:02:50 +08:00
parent 26bc108bf1
commit df3b960505
2 changed files with 40 additions and 17 deletions

View File

@ -81,19 +81,31 @@ describe('SelectionContextmenu', () => {
expect(screen.queryByText('operator.vertical')).not.toBeInTheDocument()
})
it('should keep the menu inside the workflow container bounds', () => {
it('should still render the menu when the requested position exceeds workflow container bounds', () => {
const nodes = [
createNode({ id: 'n1', selected: true, width: 80, height: 40 }),
createNode({ id: 'n2', selected: true, position: { x: 140, y: 0 }, width: 80, height: 40 }),
]
const { store } = renderSelectionMenu({ nodes })
const container = document.querySelector('#workflow-container') as HTMLDivElement
vi.spyOn(container, 'getBoundingClientRect').mockReturnValue({
x: 16,
y: 24,
left: 16,
top: 24,
right: 816,
bottom: 624,
width: 800,
height: 600,
toJSON: () => ({}),
})
act(() => {
store.setState({ selectionMenu: { left: 780, top: 590 } })
})
const menu = screen.getByTestId('selection-contextmenu')
expect(menu).toHaveStyle({ left: '540px', top: '210px' })
expect(screen.getByTestId('selection-contextmenu-item-left')).toBeInTheDocument()
})
it('should close itself when only one node is selected', async () => {

View File

@ -25,7 +25,6 @@ import {
ContextMenuGroupLabel,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/app/components/base/ui/context-menu'
import CreateSnippetDialog from './create-snippet-dialog'
import { useNodesReadOnly, useNodesSyncDraft } from './hooks'
@ -295,6 +294,25 @@ const SelectionContextmenu = () => {
return getMenuPosition(selectionMenu, container?.getBoundingClientRect())
}, [selectionMenu])
const anchor = useMemo(() => {
if (!selectionMenu)
return null
const container = document.querySelector('#workflow-container')
const containerRect = container?.getBoundingClientRect()
if (!containerRect)
return null
return {
getBoundingClientRect: () => DOMRect.fromRect({
width: 0,
height: 0,
x: containerRect.left + menuPosition.left,
y: containerRect.top + menuPosition.top,
}),
}
}, [menuPosition.left, menuPosition.top, selectionMenu])
useEffect(() => {
if (selectionMenu && selectedNodes.length <= 1)
handleSelectionContextmenuCancel()
@ -382,18 +400,11 @@ const SelectionContextmenu = () => {
}
}, [store, workflowStore, selectedNodes, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory, handleSelectionContextmenuCancel])
if (!selectionMenu && !isCreateSnippetDialogOpen)
if ((!selectionMenu || !anchor) && !isCreateSnippetDialogOpen)
return null
return (
<div
className="absolute z-[9]"
data-testid="selection-contextmenu"
style={{
left: menuPosition.left,
top: menuPosition.top,
}}
>
<div data-testid="selection-contextmenu">
<ContextMenu
open
onOpenChange={(open) => {
@ -401,10 +412,10 @@ const SelectionContextmenu = () => {
handleSelectionContextmenuCancel()
}}
>
<ContextMenuTrigger>
<span aria-hidden className="block size-px opacity-0" />
</ContextMenuTrigger>
<ContextMenuContent popupClassName="w-[240px]">
<ContextMenuContent
positionerProps={{ anchor }}
popupClassName="w-[240px]"
>
{menuSections.map((section, sectionIndex) => (
<ContextMenuGroup key={section.titleKey}>
{sectionIndex > 0 && <ContextMenuSeparator />}