hooks
                                
                                
                                
                                    hooks copied to clipboard
                            
                            
                            
                        [RFC] useRightClickMenu / useContextMenu
用于添加自定义右键菜单,只需要传入需要展现的菜单以及对应的容器
API
经过一定讨论,基本形成如下的结论
type RightClickMenuInstance = [number, number, (visible: boolean) => void];
export const useRightClickMenu = (
  menu: JSX.Element | (() => JSX.Element),
  target:
    | HTMLElement
    | (() => HTMLElement)
    | React.MutableRefObject<HTMLElement> = document.body
): RightClickMenuInstance
DEMO
//default: 默认全局绑定
useRightClickMenu(<Menu/>)
// ref: 可选ref绑定
const ref=useRef();
useRightClickMenu(<Menu/>,ref );
return <div ref={ref}></div>
// HTMLElement: 可选直接通过element绑定
const container=document.getElementById("xxx")
useRightClickMenu(<Menu/>,container);
// ()=>HTMLElement
useRightClickMenu(<Menu/>,()=>document.getElementById("xxx"));
                                    
                                    
                                    
                                
这里的设计是否可以不传入菜单以及对应的容器。 参考这篇文章返回值可以设计为:
xPos, yPos, showMenu。
我认为你说的有道理,这是改造之后的useRightClickMenu
type RightClickMenuInstance = [number, number, (visible: boolean) => void];
export const useRightClickMenu = (
  menu: JSX.Element | (() => JSX.Element),
  container: HTMLElement | Element = document.body,
  overflow: 'auto' | 'visible' = 'auto'
): RightClickMenuInstance
- 返回xPos,yPos确实不错,也应该返回详细点,这里对应前两个number
 - 原版的容器意义并不是很大,所以我改造了一下,现在传入容器是可以将监听
contextmenu注册在对应容器上,不传的话默认绑定body。如图,某些iwiki只有左侧目录右键会弹出自定义菜单
 - 关于不传入菜单,其实我的思路和该文章的简单区别是,他这边只做了获取pageX,pageY,而我还做了一个额外事情是对于弹出层应该弹出位置的计算。即如果使用该文章的组件会发现,在最右侧、最下方都会存在菜单超出页面边界,而我由于将
menu直接传入hook,我在进行弹出的时候并不是直接使用e.pageX,e.pageY,而是先进行了边界情况的相关计算,再通过不同的模式(overflow="auto")来给菜单赋予位置。而想做到这点一定需要将组件传入hook 
这个 overflow 不是很理解。假如是控制菜单的展示位置,感觉我们自动帮它处理了就可以了「也就是你提到的解决不传入 menu 的文图」,估计就是 auto?还有其他的模式代表什么含义呢?
这个 overflow 不是很理解。假如是控制菜单的展示位置,感觉我们自动帮它处理了就可以了「也就是你提到的解决不传入 menu 的文图」,估计就是 auto?还有其他的模式代表什么含义呢?
- overflow==="auto" auto的情况下,会进行边界情况处理,保证菜单显示位置不超出容器。
 - overflow==="visible" visible,有些场景我们不需要做边界计算,比如上边这个左侧目录右键的案例中,我们应当允许超出这个容器,否则菜单的显示是不符合预期的。
 
同时我在思考,要不要有选择的把具体到x轴y轴的overflow交给用户选择
这个 overflow 不是很理解。假如是控制菜单的展示位置,感觉我们自动帮它处理了就可以了「也就是你提到的解决不传入 menu 的文图」,估计就是 auto?还有其他的模式代表什么含义呢?
- overflow==="auto" auto的情况下,会进行边界情况处理,保证菜单显示位置不超出容器。
 - overflow==="visible" visible,有些场景我们不需要做边界计算,比如上边这个左侧目录右键的案例中,我们应当允许超出这个容器,否则菜单的显示是不符合预期的。
 同时我在思考,要不要有选择的把具体到x轴y轴的overflow交给用户选择
我理解我们这个边界处理应该是针对页面的吧?不应该针对容器? 另外这个 DOM 的入参设计需要符合 ahooks 的处理规范 哈。
我理解我们这个边界处理应该是针对页面的吧?不应该针对容器? 另外这个 DOM 的入参设计需要符合 ahooks 的处理规范 哈。
你说的有道理,边界处理确实不应该针对容器,调整了一下,现在同时也支持了ref:
type RightClickMenuInstance = [number, number, (visible: boolean) => void];
export const useRightClickMenu = (
  menu: JSX.Element | (() => JSX.Element),
  target:
    | HTMLElement
    | (() => HTMLElement)
    | React.MutableRefObject<HTMLElement> = document.body
): RightClickMenuInstance
那么现在overflow去掉了,弹窗将依据页面高宽进行边界处理
@brickspert @crazylxr 大佬们怎么看,我个人觉得这个场景可以支持一下?

简单实现了一下,大佬们看看我这个思路对不对
![]()
简单实现了一下,大佬们看看我这个思路对不对
待确定下来我提个PR吧...你这是按照文章做的目测
![]()
简单实现了一下,大佬们看看我这个思路对不对
待确定下来我提个PR吧...你这是按照文章做的目测
OK,你来吧,我学习一下
情况如何
情况如何
很糟糕,没有进展,但是你可以先尝试使用下边的代码
// useAppendRootNode.tsx
import { useEffect } from 'react';
import ReactDOM from 'react-dom';
export const isBrowser = () => typeof window !== 'undefined';
interface AppendRootNodeInstance {
  show: () => void;
  destory: () => void;
}
type AppendRootNodeResult = [string, AppendRootNodeInstance];
export const useAppendRootNode = (
  id: string,
  render: (() => JSX.Element) | JSX.Element,
  createElement?: () => HTMLElement,
  parent: HTMLElement = isBrowser() ? document.body : null
): AppendRootNodeResult => {
  const show = () => {
    if (document.getElementById(id)) {
      return;
    }
    const ele = createElement?.() ?? document.createElement('div');
    ele.id = id;
    parent.append(ele);
  };
  const destory = () => {
    const ele = document.getElementById(id);
    if (!ele) return;
    parent.removeChild(ele);
  };
  useEffect(() => {
    show();
    return () => {
      destory();
    };
  }, []);
  useEffect(() => {
    ReactDOM.render(
      render instanceof Function ? render() : render,
      document.getElementById(id)
    );
  }, [render, id]);
  return [id, { show, destory }];
};
export default useAppendRootNode;
// useRightClickMenu.tsx
import React, { useEffect, useState, useRef } from 'react';
import { useAppendRootNode } from './useAppendRootNode';
import { throttle } from 'lodash-es';
type RightClickMenuInstance = [number, number, (visible: boolean) => void];
export const useRightClickMenu = (
  menu: JSX.Element | (() => JSX.Element),
  target:
    | HTMLElement
    | (() => HTMLElement)
    | React.MutableRefObject<HTMLElement> = document.body
): RightClickMenuInstance => {
  const [contextMenu, setContextMenu] = useState({
    x: 0,
    y: 0,
    visible: true,
  });
  const memoAttr = useRef(null);
  const ref = useRef(null);
  const container = (() => {
    if (!target) return null;
    if (target instanceof Function) {
      return target();
    }
    // @ts-ignore
    if (target.current !== void 0) {
      // @ts-ignore
      return target.current;
    }
    return target;
  })();
  useAppendRootNode(
    'right-click-context-menu',
    <div
      className="absolute"
      ref={ref}
      style={{
        position: 'absolute',
        left: contextMenu.x,
        top: contextMenu.y,
        display: contextMenu.visible ? 'flex' : 'none',
        zIndex: 9999999,
        visibility: memoAttr.current === null ? 'hidden' : 'visible',
      }}
    >
      {menu instanceof Function ? menu() : menu}
    </div>
  );
  useEffect(() => {
    if (!ref.current) return;
    const { clientHeight, clientWidth } = ref.current;
    memoAttr.current = {
      clientHeight,
      clientWidth,
    };
    setContextMenu({
      x: 0,
      y: 0,
      visible: false,
    });
  }, [ref.current]);
  useEffect(() => {
    if (!container) return;
    const handleContextMenuClick = (e: PointerEvent) => {
      e.preventDefault();
      const { pageX, pageY } = e;
      const { clientHeight, clientWidth } = memoAttr.current;
      const {
        scrollHeight: windowHeight,
        scrollWidth: windowWidth,
      } = document.body;
      if (clientHeight > windowHeight || clientWidth > windowWidth) {
        throw new Error('the menu is longer than the browser');
      }
      const x = clientWidth + pageX > windowWidth ? pageX - clientWidth : pageX;
      const y =
        clientHeight + pageY > windowHeight ? pageY - clientHeight : pageY;
      setContextMenu({
        x,
        y,
        visible: true,
      });
    };
    const handleOutsideClick = (
      e: PointerEvent & { path: Array<HTMLElement> }
    ) => {
      if (e.path.includes(ref.current)) {
        return;
      }
      setContextMenu({
        ...contextMenu,
        visible: false,
      });
    };
    const handleThrottleOutSideClick = throttle(handleOutsideClick, 800);
    container.addEventListener('contextmenu', handleContextMenuClick);
    document.addEventListener('click', handleOutsideClick);
    document.addEventListener('scroll', handleThrottleOutSideClick);
    window.addEventListener('resize', handleThrottleOutSideClick);
    return () => {
      container.removeEventListener('contextmenu', handleContextMenuClick);
      document.removeEventListener('click', handleOutsideClick);
      document.removeEventListener('scroll', handleThrottleOutSideClick);
      window.removeEventListener('resize', handleThrottleOutSideClick);
    };
  }, [container]);
  return [
    contextMenu.x,
    contextMenu.y,
    visible => {
      setContextMenu({
        ...contextMenu,
        visible,
      });
    },
  ];
};
export default useRightClickMenu;