前端需掌握: React定制右键菜单组件

5,622 阅读2分钟

效果图

实现需求如下:

 右键点击联系人头像出现定制的右键菜单组件。有以下功能: 设置未读,静音、移除、清除历史记录等。
 右键点击聊天信息出现不同功能的组件。如果有: 撤回、删除等。

思考

浏览器都有默认右键菜单。我们要对其屏蔽。增加我们业务需要的右键菜单组件。联系人跟聊天内容右键菜单内容各不相同,不能每个div都加入右键菜单,因为有太多内容。所以需要定制一个右键菜单并且右键菜单功能不影响全局

思路:

  • 屏蔽浏览器默认右键事件
  • 获得右键点击的位置
  • 获得点击内容
  • 赋值右键菜单位置和内容
  • 渲染右键菜单
  • 监听点击和滚动事件进行关闭菜单

完整代码: 详细注释包看包会

// 右键菜单

import React, { useEffect, useState, useRef } from 'react';

// style接口
interface iStyle {
  position: any,
  left: number,
  top: number
}

const PublicRightClick = () => {
  // 显示/隐藏
  const [show, setShow] = useState<boolean>(false);
  // 改变位置
  const [style, setStyle] = useState<iStyle>({
    position: 'fixed', left: 300, top: 200
  });
  // 获得show最新值
  const showRef = useRef();
  // 右键菜单ref
  const rightClickRef = useRef<any>();
  
  // 右键点击
  const handleContextMenu = (event: any) => {
  	// 屏蔽默认右键事件
    event.preventDefault();
    // 先显示才能捕捉到右键菜单Ref
    // 否则rightClickRef将为undefined
    setShow(true);
    // 获得点击的位置
    let { clientX, clientY } = event;
    // 文档显示区的宽度
    const screenW: number = window.innerWidth;
    const screenH: number = window.innerHeight;
    // 右键菜单的宽度
    const rightClickRefW: number = rightClickRef.current.offsetWidth;
    const rightClickRefH: number = rightClickRef.current.offsetHeight;

    // right为true,说明鼠标点击的位置到浏览器的右边界的宽度可以放下contextmenu。
    // 否则,菜单放到左边。
    const right = (screenW - clientX) > rightClickRefW;
    const top = (screenH - clientY) > rightClickRefH;
    // 赋值右键菜单离鼠标一些距离
    clientX = right ?  clientX + 6 : clientX - rightClickRefW - 6;
    clientY = top ? clientY + 6 : clientY - rightClickRefH - 6;
    setStyle({
      ...style,
      left: clientX,
      top: clientY
    });
  };
  
  // 点击事件
  const handleClick = (event: any) => {
    // 聊天页面中会一直监听左键点击事件直到销毁  
    // 如果右键菜单不出现则不做逻辑处理、避免冲突
    if(!showRef.current) return;
    // 点击目标不在右键菜单里则关闭菜单
    if (event.target.parentNode !== rightClickRef.current){
      setShow(false)
    }
  };

  // 滑动关闭右键功能
  const setShowFalse = () => {
    // 如果右键菜单不出现则不做逻辑处理
    // eslint-disable-next-line no-useless-return
    if(!showRef.current) return;
    // 滚动直接关闭
    setShow(false)
  };
  
  // 生命周期监听
  useEffect(() => {
    document.addEventListener('contextmenu', handleContextMenu);
    document.addEventListener('click', handleClick,true);
    document.addEventListener('scroll', setShowFalse, true);
    return () => {
      document.removeEventListener('contextmenu', handleContextMenu);
      document.removeEventListener('click', handleClick,true);
      document.removeEventListener('scroll', setShowFalse, true);
    }
  }, []);
  
  // 副作用: show一改变就赋值showRef新的state。
  // 因为监听事件获取不到最新的state
  // 通过ref来获取。美滋滋
  useEffect(() => {
    showRef.current = show;
  }, [show]);
 
  // 渲染右键
  const renderContentMenu = () => (
    <div ref={rightClickRef as any} className="WeChatContactsAvatarTools" style={style} >
      <div className="rightClickItems">
        Mark as unread
      </div>
      <div className="rightClickItems">
        Mute Notifications
      </div>
      <div className="rightClickItems">
        Remove
      </div>
      <div className="rightClickItems">
        Clear Chat History
      </div>
    </div>
  );
  // 总渲染
  return show ? renderContentMenu() : null;
};

export default React.memo(PublicRightClick);

Thanks for reading

  • 文中若有错误,欢迎在评论区指正
  • 若有更好解决方案,相当欢迎指导
  • 若有帮到了您,点个赞再走吧~😊