下拉框子项hover时提示详情:ReactDOM.createPortal实战

278 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

背景

之前在工作中遇到一个需求:存在多个select下拉选择框,当下拉弹框出现时,鼠标经过每一个子项时,需要展示该子项的详细内容。参照同组大佬的代码,利用ReactDOM.createPortal和定位来实现了该需求。

ReactDOM.createPortal

function createPortal(children: React.ReactNode, container: Element, key?: string | null | undefined): React.ReactPortal

第一个children参数表示需要被渲染的子组件或元素,第二个参数container表示该渲染子组件需要挂载的元素节点,代表容器节点。
ReactDOM.createPortal()方法能够让子组件脱离父组件,在父组件以外的地方进行挂载,所以利用它能够实现脱离父组件的子组件定位,它经常用于创建Modal弹框,实现更加自由的弹框定位。

问题与解决方式

在开发过程中遇到了几个问题,提供了解决方案,这里也提供给大家:
所遇问题记录:

  • 由于下拉弹框不定宽,导致获取其宽度是个难题:最先开始直接使用.ant-select-selector类名获取select输入框元素,用getBoundingClientRect()方法计算其left、top、width等属性值去定位描述面板,但是会出现下拉框比输入框宽,属性面板遮盖住部分下拉框。
  • 添加几项后,获取下拉框类名.ant-select-dropdown元素,下拉框的getBoundingClientRect()或该HTMLElement元素的width、height、left、top等属性值都变为了0

解决方式:
为select添加dropdownClassName属性,每一个Select选择框都有唯一类名作为每个下拉框的类名,从而替换掉.ant-select-dropdown类名,之后就能获取其clientWidth宽度了。
实现思路: 主要实现方式就是在组件中传入dropdownClassName用于后续获取当前下拉框宽高和位置,便于为容器节点定位,当鼠标经过或离开下拉弹框每一项时,使用onMouseEnter和onMouseLeave设置提示详情子组件的显示与否和详情信息。
主要代码如下:

      <Select
        dropdownClassName={dropdownClassName[0]}
        onClick={(e: any) =>
          registerFunc(nodeClassname[0], e, dropdownClassName[0])
        }
        onBlur={() => resetRoots(nodeClassname[0])}
        defaultValue='test1'
        dropdownMatchSelectWidth={false}
        style={{ minWidth: 100 }}
      >
        {options.map((item) => (
          <Select.Option key={item.value} value={item.value}>
            <div
              onMouseEnter={(e: any) => {
                setOpen(true);
                setCurDesc(item.value);
              }}
              onMouseLeave={(e: any) => {
                setOpen(false);
                setCurDesc("");
              }}
            >
              {item.label}
            </div>
          </Select.Option>
        ))}
      </Select>

当点击select选择框显示弹出框时,调用注册容器节点registerFunc方法创建容器节点,因为存在多个Select选择框,所以需要定义一个节点对象,用于存储不同的容器节点,然后就利用getBoundingClient()方法获取定位值,最后使用组件即可 注册容器节点registerFunc代码如下:

    const selectDropdownEl =
      document.getElementsByClassName(dropdownClassName)[0] as HTMLElement;
    const containerWidth =
      (selectDropdownEl as HTMLElement)?.clientWidth || 0;

    const containerRoot = document.createElement("div");
    containerRoot.style.position = "absolute";
    containerRoot.style.left = `${(selectDropdownEl?.getBoundingClientRect()?.left || 0) + containerWidth}px`;
    containerRoot.style.top = `${
      (selectDropdownEl?.getBoundingClientRect()?.top || 0)
    }px`;
    newRoot.style.zIndex = "999";
    if (!roots[nodeClassname]) {
      document.body.append(containerRoot);
    } else {
      document.body.replaceChild(containerRoot, roots[nodeClassname]);
    }
    roots[nodeClassname] = containerRoot;

提示详情子组件代码为:

const ChildPortal: FC<{
  info: any;
  isOpen: boolean;
  nodeClassName: string;
  visible?: boolean;
}> = ({ info, isOpen, nodeClassName, visible = false }) => {
  if (!isOpen || !info || !roots[nodeClassName]) {
    return null;
  }
  return ReactDOM.createPortal(
    <div visible={visible}>
      <div
        className="content"
        style={{
          background: "#fff",
          minWidth: 200,
          maxWidth: 500,
          minHeight: 150,
          borderRadius: "4px",
          boxShadow: "rgba(0,0,0,0.1) 0px 0px 4px",
          padding: "10px",
        }}
      >
        <h4>描述</h4>
        <p>{info}</p>
      </div>
    </div>,
    roots[nodeClassName]
  );
};

demo实现效果

这里实现了一个类似的demo,实现效果如下所示:

image.png