---
title: "ContextMenu"
language: "en"
canonical: "https://x6.antv.antgroup.com/en/examples/edge/tool/context-menu/"
version: "3.x"
---

# ContextMenu

[Live example](https://x6.antv.antgroup.com/en/examples/edge/tool/context-menu/)

[Resource index](/llms.txt)

Provide a DOM element with `id="container"`. The original example source follows.

## Source

```tsx
import { Edge, Graph, Shape } from '@antv/x6'
import { Dropdown } from 'antd'
import type { MenuProps } from 'antd'
import React, { useState, useEffect, useRef, useCallback } from 'react'
import { createRoot } from 'react-dom/client'
import type { Root } from 'react-dom/client'

// 如果项目中使用的是 antd@4，使用方式参考：https://codesandbox.io/s/contextmenu-z8gpq3

// 2.x的使用可以参考2.x文档： https://x6-v2.antv.vision/examples/edge/tool/#context-menu


Shape.Rect.config({
  attrs: {
    body: {
      fill: '#f5f5f5',
      stroke: '#d9d9d9',
      strokeWidth: 1,
    },
  },
  ports: {
    groups: {
      in: { position: { name: 'top' } },
      out: { position: { name: 'bottom' } },
    },
  },
  portMarkup: [
    {
      tagName: 'circle',
      selector: 'portBody',
      attrs: {
        r: 5,
        magnet: true,
        stroke: '#31d0c6',
        fill: '#fff',
        strokeWidth: 2,
      },
    },
  ],
})

const magnetAvailabilityHighlighter = {
  name: 'stroke',
  args: {
    padding: 3,
    attrs: {
      strokeWidth: 3,
      stroke: '#52c41a',
    },
  },
}

// 工具函数：检查点击是否在 dropdown 内
const isClickInDropdown = (target: HTMLElement): boolean => {
  const checkElement = (el: HTMLElement | null): boolean => {
    if (!el || el === document.body) return false

    const className = el.className?.toString() || ''

    if (
      className.includes('ant-dropdown') ||
      className.includes('ant-dropdown-menu')
    ) {
      return true
    }

    return checkElement(el.parentElement)
  }

  return checkElement(target)
}

// 右键菜单组件
interface ContextMenuProps {
  x: number
  y: number
  cell: Edge
  onClose: () => void
}

const ContextMenu: React.FC<ContextMenuProps> = ({ x, y, cell, onClose }) => {
  const [visible, setVisible] = useState(true)
  const containerRef = useRef<HTMLDivElement>(null)

  // 关闭菜单
  const handleClose = useCallback(() => {
    setVisible(false)
    // 异步调用 onClose，避免同步 unmount
    Promise.resolve().then(onClose)
  }, [onClose])

  // 处理菜单项点击
  const handleMenuClick: MenuProps['onClick'] = useCallback(({ key }) => {
    const actions: Record<string, () => void> = {
      'color-blue': () => cell.setAttrs({ line: { stroke: '#1890ff' } }),
      'color-green': () => cell.setAttrs({ line: { stroke: '#52c41a' } }),
      'color-red': () => cell.setAttrs({ line: { stroke: '#ff4d4f' } }),
      'style-solid': () => cell.setAttrs({ line: { strokeDasharray: '' } }),
      'style-dashed': () => cell.setAttrs({ line: { strokeDasharray: '5 5' } }),
      'delete': () => cell.remove(),
    }

    actions[key]?.()
    handleClose()
  }, [cell, handleClose])

  // 监听外部点击
  useEffect(() => {
    if (!visible) return

    const handleClickOutside = (e: MouseEvent) => {
      const target = e.target as HTMLElement
      const isInContainer = containerRef.current?.contains(target)
      const isInDropdown = isClickInDropdown(target)

      if (!isInContainer && !isInDropdown) {
        handleClose()
      }
    }

    // 延迟添加监听器，避免立即触发
    const timer = setTimeout(() => {
      document.addEventListener('mousedown', handleClickOutside, true)
    }, 200)

    return () => {
      clearTimeout(timer)
      document.removeEventListener('mousedown', handleClickOutside, true)
    }
  }, [visible, handleClose])

  // 菜单配置
  const menuItems: MenuProps['items'] = [
    {
      key: 'changeColor',
      label: '修改颜色',
      children: [
        { key: 'color-blue', label: '蓝色' },
        { key: 'color-green', label: '绿色' },
        { key: 'color-red', label: '红色' },
      ],
    },
    {
      key: 'changeStyle',
      label: '修改样式',
      children: [
        { key: 'style-solid', label: '实线' },
        { key: 'style-dashed', label: '虚线' },
      ],
    },
    { type: 'divider' },
    { key: 'delete', label: '删除', danger: true },
  ]

  return (
    <div
      ref={containerRef}
      style={{
        position: 'fixed',
        left: x,
        top: y,
        zIndex: 9999,
      }}
    >
      <Dropdown
        open={visible}
        onOpenChange={(open) => !open && handleClose()}
        menu={{ items: menuItems, onClick: handleMenuClick }}
        trigger={[]}
        getPopupContainer={() => document.body}
      >
        <span style={{ display: 'inline-block', width: 0, height: 0 }} />
      </Dropdown>
    </div>
  )
}

// Graph 配置
const graph = new Graph({
  container: document.getElementById('container')!,
  grid: true,
  highlighting: {
    magnetAvailable: magnetAvailabilityHighlighter,
  },
  connecting: {
    snap: true,
    allowBlank: false,
    allowLoop: false,
    allowNode: false,
    highlight: true,
    validateMagnet({ magnet }) {
      return magnet.getAttribute('port-group') !== 'in'
    },
    validateConnection({ sourceMagnet, targetMagnet }) {
      if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') {
        return false
      }
      if (!targetMagnet || targetMagnet.getAttribute('port-group') !== 'in') {
        return false
      }
      return true
    },
  },
})

// 添加节点和边
const source = graph.addNode({
  x: 40,
  y: 40,
  width: 100,
  height: 40,
  ports: [{ id: 'out-2', group: 'out' }],
})

const target = graph.addNode({
  x: 140,
  y: 240,
  width: 100,
  height: 40,
  ports: [{ id: 'in-1', group: 'in' }],
})

graph.addEdge({
  source: { cell: source.id, port: 'out-2' },
  target: { cell: target.id, port: 'in-1' },
})

// 菜单管理器
class MenuManager {
  private root: Root | null = null
  private container: HTMLDivElement | null = null
  private isCleaningUp = false

  cleanup = () => {
    if (this.isCleaningUp) return
    this.isCleaningUp = true

    requestAnimationFrame(() => {
      try {
        this.root?.unmount()
        this.container?.parentNode?.removeChild(this.container)
      } catch (error) {
        console.error('Error cleaning up menu:', error)
      } finally {
        this.root = null
        this.container = null
        this.isCleaningUp = false
      }
    })
  }

  show = (x: number, y: number, cell: Edge) => {
    this.cleanup()

    requestAnimationFrame(() => {
      this.container = document.createElement('div')
      document.body.appendChild(this.container)

      this.root = createRoot(this.container)
      this.root.render(
        <ContextMenu x={x} y={y} cell={cell} onClose={this.cleanup} />
      )
    })
  }
}

const menuManager = new MenuManager()

// 监听边的右键菜单事件
graph.on('edge:contextmenu', ({ cell, e }: { cell: Edge; e: MouseEvent }) => {
  e.preventDefault()
  e.stopPropagation()
  menuManager.show(e.clientX, e.clientY, cell)
})
```
