Popover 下拉容器定位原理浅析

不换
2024-04-02 00:40
阅读2.58分钟

使用过 Antd 的小伙伴应该深有体会,但是我们用的多了,也就想了解它怎么做的(:跟我一样吧

Preview

乍一看实现方式并不难,但是其实本篇文章更想表述一些细节的内容。

目标能力

  1. 精准定位
  2. 跟随屏幕缩放实时定位
  3. 跟随滚动

构造容器

"use client";
import {FC, PropsWithChildren, RefObject, useCallback, useEffect, useRef, useState} from "react";
import { createPortal } from "react-dom";
import { cloneElement } from "react";
 
const getStyle = (ref: RefObject<HTMLElement>, position: 'top' | 'bottom' | 'right' | 'left') => {
    const styles = ref.current?.getBoundingClientRect();
 
    const top = styles!.top + styles.height + 30 + 'px';
    const left = styles!.left + 'px';
 
    return {
        top,
        left,
    }
}
 
const Popover: FC<PropsWithChildren> = ({ children }) => {
 
    const ref = useRef(null);
    const [styles, setStyles] = useState({});
 
    const [opacity, setOpacity] = useState(0);
 
 
    useEffect(() => {
        const _ = getStyle(ref)
        setStyles(_);
    }, []);
 
    const onMouseEnter = useCallback(() => {
        setOpacity(1)
    }, [])
    const onMouseLeave = useCallback(() => {
        setOpacity(0)
    }, [])
 
    const newChild = cloneElement(children as any, {
        ref,
        onMouseEnter,
        onMouseLeave
    })
 
    return <>
        {newChild}
        {
            createPortal(<div className="popover-wrapper bg-amber-300 w-[100px] h-[80px] absolute" style={{...styles, opacity }}>
                    Hello )_________
            </div>, document.body)
        }
    </>
}
 
 
export  default Popover;

其实大家大可关注到精髓点:

  • createPortal
  • getBoundingClientRect

createPortal 的目的是脱离文档流,getBoundingClientRect 是为了拿到目标元素的具体位置 坐标宽高

我们可以设置 Popover 的模式,大体分为两种 hoverclick 模式:

  • click
    • 监听 onClick 事件即可
  • hover
    • 监听 onMouseEnter
    • 监听 onMouseLeave

到这里,你觉得结束了吗?

答案是:没有,因为我们要考虑,点击模式常驻屏幕,滚动跟随移动的场景

在这里我们借助 ResizeObserver 来实现:

const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
       const _ = getStyle(ref)
       setStyles(_);
  }
});
 
resizeObserver.observe(document.target || document.body);

Preview

兼容性还是可以的,但是我们依旧要考虑我们程序的鲁棒性:

 
async function handleWatch() {
    if(!window.ResizeObserver) {
       await  import ('resize-observer-polyfill')
    }
 
    const resizeObserver = new ResizeObserver((entries) => {
      for (const entry of entries) {
           const _ = getStyle(ref)
           setStyles(_);
      }
    });
 
    resizeObserver.observe(document.target || document.body);
}

到此,一个简单的跟随移动 Popover 就实现了,如果你有更好的想法 ,欢迎交流~

不换
不换
中国.上海