Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ interface UseCanvasContextMenuProps {

/**
* Hook for managing workflow canvas context menus.
* Handles right-click events, menu state, click-outside detection, and block info extraction.
*
* Handles right-click events on nodes, pane, and selections with proper multi-select behavior.
*
* @param props - Hook configuration
* @returns Context menu state and handlers
*/
export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasContextMenuProps) {
const [activeMenu, setActiveMenu] = useState<MenuType>(null)
Expand Down Expand Up @@ -46,19 +50,29 @@ export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasCo
event.stopPropagation()

const isMultiSelect = event.shiftKey || event.metaKey || event.ctrlKey
setNodes((nodes) =>
nodes.map((n) => ({
...n,
selected: isMultiSelect ? (n.id === node.id ? true : n.selected) : n.id === node.id,
}))
)

const selectedNodes = getNodes().filter((n) => n.selected)
const nodesToUse = isMultiSelect
? selectedNodes.some((n) => n.id === node.id)
? selectedNodes
: [...selectedNodes, node]
: [node]
const currentSelectedNodes = getNodes().filter((n) => n.selected)
const isClickedNodeSelected = currentSelectedNodes.some((n) => n.id === node.id)

let nodesToUse: Node[]
if (isClickedNodeSelected) {
nodesToUse = currentSelectedNodes
} else if (isMultiSelect) {
nodesToUse = [...currentSelectedNodes, node]
setNodes((nodes) =>
nodes.map((n) => ({
...n,
selected: n.id === node.id ? true : n.selected,
}))
)
} else {
nodesToUse = [node]
setNodes((nodes) =>
nodes.map((n) => ({
...n,
selected: n.id === node.id,
}))
)
}

setPosition({ x: event.clientX, y: event.clientY })
setSelectedBlocks(nodesToBlockInfos(nodesToUse))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,13 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
const [isOpen, setIsOpen] = useState(false)
const [position, setPosition] = useState<ContextMenuPosition>({ x: 0, y: 0 })
const menuRef = useRef<HTMLDivElement>(null)
// Used to prevent click-outside dismissal when trigger is clicked
const dismissPreventedRef = useRef(false)

/**
* Handle right-click event
*/
const handleContextMenu = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()

// Calculate position relative to viewport
const x = e.clientX
const y = e.clientY

Expand All @@ -50,17 +45,10 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
[onContextMenu]
)

/**
* Close the context menu
*/
const closeMenu = useCallback(() => {
setIsOpen(false)
}, [])

/**
* Prevent the next click-outside from dismissing the menu.
* Call this on pointerdown of a toggle trigger to allow proper toggle behavior.
*/
const preventDismiss = useCallback(() => {
dismissPreventedRef.current = true
}, [])
Expand All @@ -72,7 +60,6 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
if (!isOpen) return

const handleClickOutside = (e: MouseEvent) => {
// Check if dismissal was prevented (e.g., by toggle trigger's pointerdown)
if (dismissPreventedRef.current) {
dismissPreventedRef.current = false
return
Expand All @@ -82,7 +69,6 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
}
}

// Small delay to prevent immediate close from the same click that opened the menu
const timeoutId = setTimeout(() => {
document.addEventListener('click', handleClickOutside)
}, 0)
Expand Down