背景
在业务开发中有时候会遇到这样的场景,父容器是一个可以纵向滚动的 List,要在水平方向上增加一个 hover 显示的气泡(Tooltip)。你会发现,不管怎么写,气泡都会因为超出父容器而被截断。
即使父容器只是设置了纵向滚动,即 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 的子节点了。
总结
这个场景不难,但确实困扰过我一段时间,而且也趁机了解了 Portals,故作记录。