『问题探究』如何使气泡不被父容器截断?

55 阅读2分钟

背景

在业务开发中有时候会遇到这样的场景,父容器是一个可以纵向滚动的 List,要在水平方向上增加一个 hover 显示的气泡(Tooltip)。你会发现,不管怎么写,气泡都会因为超出父容器而被截断。

image.png

即使父容器只是设置了纵向滚动,即 overflow-y: auto,浏览器也会把 overflow-x 隐式设置为 auto,而非 visible。最后气泡还是无法按照预期去展示。如果父容器的 overflow 设置为 hidden,就更不必说了。那么问题来了,如何才能突破父容器的限制呢?

解法

Fixed 定位

要避免被截断,最直接的方式就是使用 fixed 定位。此时元素相对于浏览器视口(viewport)进行定位,不随页面滚动而移动,也就不会被父元素的滚动区域截断。

那么只需要获得 trigger(即 Tooltip 指向的那个 dom)的位置信息,赋值给 top、left 属性就可以了。

给 trigger 绑定 ref,Tooltip 接收此 ref,再通过 ref.current.getBoundingClientRect拿到位置属性。代码如下:

const GuideTooltip = ({ visible, targetRef }) => {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  
  useEffect(() => {
    if (visible && targetRef.current) {
      const updatePosition = () => {
        // 通过 ref 获取位置信息
        const rect = targetRef.current?.getBoundingClientRect();
        if (rect) {
          setPosition({
            top: rect.top,
            left: rect.right + 8,
          });
        }
      };

      updatePosition();
      window.addEventListener('scroll', updatePosition);

      return () => {
        window.removeEventListener('scroll', updatePosition);
      };
    }
    return;
  }, [show, targetRef]);

  return (
    <div 
      style={{
        position: 'fixed',
        top: position.top,
        left: position.left,
      }}>
      一些内容...
    </div>
  )
}

React.Portals

在研究这个问题的过程中,我了解到 React 提供了 Portals API,可以将组件渲染到 DOM 树中的任何位置,适合处理这种需要突破父容器限制的场景。

React Portals 是 React 提供的一种将子组件渲染到父组件 DOM 层级之外的技术,允许组件内容“传送”到指定的 DOM 节点(如 document.body)中。它解决了子组件需要脱离父容器样式限制(如 overflow: hidden 或 z-index 冲突)的场景,例如模态框、工具提示、悬浮层等 。

好家伙,Tooltip 不就属于“悬浮层”吗,正是 Portals 应用的典型场景。

API 使用起来也很方便,第一个参数是组件的 jsx,第二个渲染的位置。这样就能让组件脱离父容器限制。

还是刚刚的代码,只是最后返回组件的时候用 Portals 创建。这时候因为 Tooltip 是在 body 下面的,使用绝对定位也都没问题。

import { createPortal } from 'react-dom';

const GuideTooltip = ({ visible, targetRef }) => {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  
  useEffect(() => {
    // 和上面一样,此处略
  }, [show, targetRef]);

  return createPortal(
    <div
      className={styles.guideWrapper}
      style={{
        position: 'absolute', // 因为已经脱离了父容器限制所以可以不用 fixed
        top: position.top,
        left: position.left,
      }}
    >
    </div>,
    document.body,
  );
}

可以看到,Tooltip 确实成了 body 的子节点了。

image.png

总结

image.png

这个场景不难,但确实困扰过我一段时间,而且也趁机了解了 Portals,故作记录。