前端自定义右键菜单

  • 废话少说,先上效果

自定义右键菜单

菜单容器

容器主要代码

class App extends Component<appProps, appState> {
  constructor(props: appProps) {
    super(props)
    this.state = {
      position: { top: 0, left: 0 },
      menuVisiable: false,
      menuWidth: 0,
      menuHeight: 0,
      menuDatas: [{
        id: "A000001",
        value: "apple",
        children: []
      },
      {
        id: "A000003",
        value: "AAAAAAAAAAAAAAAAAAAA",
        children: [
          {
            id: "A000031",
            value: "BBBBBBBBBBBBBBB",
            children: [{
              id: "A000311",
              value: "ddddddddddddd",
              children: []
            }]
          },
          {
            id: "A000032",
            value: "CCCCCCCCCCCCCCCCCCC",
            children: []
          }
        ]
      }, {
        id: "A000002",
        value: "banana",
        children: []
      }
      ]
    }
  }

  contextMenuHandle = (e: React.MouseEvent<HTMLDivElement>) => {
    this.setState({ menuVisiable: true, position: { top: top, left: left } })
  }

  handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
    e.preventDefault()
    this.setState({ menuVisiable: false })
  }

  onMenuClick = (data: menu) => {
    console.log(data)
    this.setState({ menuVisiable: false })
  }

  syncParams = (width: number, height: number) => {
    this.setState({ menuHeight: height, menuWidth: width })
  }

  showMenus = () => {
    if (!this.state.menuVisiable) {
      return null;
    }
    return <Content containerId='content-container' syncParams={this.syncParams} menuItams={this.state.menuDatas} position={this.state.position} onMenuClick={this.onMenuClick} />
  }

  render(): ReactNode {
    return (
      <section id="content-container" onClick={this.handleClick} onContextMenu={this.contextMenuHandle}>
        {this.showMenus()}
      </section>
    )
  }
}

export default App

容器样式

#content-container {
  height: 80vh;
  width: 90vw;
  background-color: rgb(255, 255, 255);
  border-radius: 8px;
}

菜单内容

菜单主要代码

export interface menu {
  id: string
  value: string
  children: menu[]
}

export interface position {
  top: number
  left: number
}

interface contentProps {
  menuItams: menu[]
  onMenuClick: (data: menu) => void
  position: position
  syncParams: Function
  containerId: string
}

interface contentState {
  selfHeight: number
}


class Content extends Component<contentProps, contentState> {
  innerRef = createRef<HTMLDivElement>();
  observer: ResizeObserver | null = null;
  state: contentState = {
    selfHeight: 0
  }

  componentDidMount = () => {
    if (this.innerRef.current) {
      this.setState({ selfHeight: this.innerRef.current.clientHeight })
      // 实例化 ResizeObserver,回调函数会在尺寸变化时触发
      this.observer = new ResizeObserver(([entries]) => {
        // 获取最新的 contentRect 尺寸(不包含 padding 和 border)
        const { width, height } = entries.contentRect;
        this.props.syncParams(width, height)
      });
      // 开始监听绑定的 DOM 元素
      this.observer.observe(this.innerRef.current);
    }
  }

  componentWillUnmount(): void {
    if (this.observer) {
      this.observer.disconnect();
    }
  }

  handleClick = (data: menu): void => {
    this.props.onMenuClick(data)
  }

  getTop = () => {
    return this.props.position.top
  }

  getLeft = () => {
    return this.props.position.left
  }

  generateItems = (menuItams: menu[]): React.ReactNode => {
    let menuList = menuItams.map((el, i) => {
      let children: React.ReactNode
      if (el.children.length > 0) {
        children = this.generateItems(el.children)
      }
      return <MenuItem containerId={this.props.containerId} level={i} originPosition={this.props.position} onChildClick={this.handleClick} key={el.id} item={el} children={children} />
    })
    return menuList;
  }

  render(): ReactNode {
    let menuList = this.generateItems(this.props.menuItams);
    return (
      <>
        <div ref={this.innerRef} id="content-menu" style={{ left: this.getLeft() + "px", top: this.getTop() + "px" }} onContextMenu={(e) => { e.stopPropagation(); e.preventDefault() }}>
          {menuList}
        </div>
      </>
    )
  }

}

interface menuItemProps {
  item: menu
  children: React.ReactNode
  level: number
  brother: number
  originPosition: position
  containerId: string
  onChildClick: (data: menu) => void
}

interface menuItemState {
  top: number
  left: number
  showChild: boolean
  hoverMenu: boolean
}

class MenuItem extends Component<menuItemProps, menuItemState> {
  innerRef = createRef<HTMLDivElement>();

  constructor(props: menuItemProps) {
    super(props)
    this.state = {
      top: 0,
      left: 0,
      showChild: false,
      hoverMenu: false
    }
  }

  handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
    e.stopPropagation()
    e.preventDefault()
    this.props.onChildClick(this.props.item)
  }

  getTop = () => {
    return this.state.top
  }

  getLeft = () => {
    return this.state.left
  }

  handleMouseOver = (e: React.MouseEvent<HTMLDivElement>) => {
    e.preventDefault()
    e.stopPropagation()
    this.setState({ hoverMenu: true })
    if (this.props.children) {
      this.setState({ showChild: true })
    }
  }

  handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
    e.preventDefault()
    e.stopPropagation()
    this.setState({ hoverMenu: false, showChild: false })
  }

  render(): ReactNode {
    return (
      <>
        <div key={this.props.item.id} className={this.state.hoverMenu ? "hightlight" : ""} ref={this.innerRef} id="menu-item" onClick={this.handleClick} onMouseEnter={this.handleMouseOver} onMouseLeave={() => this.setState({ hoverMenu: false, showChild: false })}>
          <QuestionCircleOutlined />
          <div id="item-context">{this.props.item.value}</div>
          {this.props.children ? <RightOutlined /> : null}
          {this.state.showChild ?
            <div id="children-menu" style={{ left: this.getLeft() + "px", top: this.getTop() + "px" }}>
              {this.props.children}
            </div>
            : null}
        </div>
      </>
    )
  }
}

export default Content

菜单样式

#content-menu {
  box-shadow: 0 6px 16px 0 rgba(0,0,0,0.08), 0 3px 6px -4px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);
  border-radius: 4px;
  position: relative;
  width: 200px;
  background-color: rgb(255, 255, 255);
}

#menu-item{
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 3px 10px;
  height: 26px;
  #item-context{
    padding: 0 10px;
    max-width: 100px;
    margin-right: auto;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

.hightlight{
  color: #1677ff; 
  cursor: pointer;
}

#children-menu{
  color: initial; 
  box-shadow: 0 6px 16px 0 rgba(0,0,0,0.08), 0 3px 6px -4px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);
  position: absolute;
  width: 200px;
  background-color: rgb(255, 255, 255);
}

感谢交流、留言!!!

posted @ 2026-05-24 19:55  每天七点半  阅读(7)  评论(0)    收藏  举报