基于React的自定义ToolTips浮层组件编写
发布时间:
本文字数:815 字 阅读完需:约 2 分钟
基本思路
本文要实现自定义浮层组件,基于React框架实现。目标是实现类似于一个按钮或者图标,鼠标hover上去后,弹出一个提示浮层,位置在图标附近。同时鼠标移动到浮层后,浮层不会消失。鼠标移开图标或浮层,浮层消失。
实现步骤
借助ReactDOM.createPortal()
浮层一定要显示在所有元素的最上面,当我们使用各种组件库时,浮层不一定能建立在最上面。所以,要求浮层的dom基于document.body
,同时z-index
设置为足够大。
ReactDOM.createPortal 是 React 提供的一个 API,它允许你把子节点渲染到存在于父组件之外的 DOM 节点上。这个方法的签名如下:
ReactDOM.createPortal(child, container);
其中 child 是任何可以渲染的 React 子元素,例如一个元素、字符串或碎片(fragment),而 container 是一个 DOM 元素。
createPortal 的主要用处包括:
-
事件冒泡:使用 createPortal 渲染的子组件会在 DOM 树上处于不同的位置,但从 React 的角度来看,它仍然存在于 React 组件树中原来的位置,这意味着事件可以正常冒泡到 React 父组件,尽管这些元素在 DOM 层级结构中并不直接相连。
-
避免 CSS 约束:在某些情况下,你可能不希望子组件受到父组件 CSS 的影响,例如在一个设置了overflow: hidden或z-index的父元素内部渲染一个模态对话框(modal)或工具提示(tooltip)。通过使用 createPortal,模态或工具提示可以渲染到 DOM 树中的其他位置,例如直接渲染到document.body下,从而避免了这些 CSS 约束。
-
视觉上的“脱离”:当你需要组件在视觉上位于页面层次结构之外时(如模态框、通知、悬浮卡片等),createPortal 提供了一种将组件结构上与视觉结构分离的方法。这可能是因为这些组件需要在页面上占据最顶层,以避免被其他元素遮挡。
使用的代码样例如下:
const toolTip = isShow && ReactDOM.createPortal(<>
<div className={css.card} style={{ position: 'absolute', zIndex: 9999>
<Card/>
</div>
</>, document.body);
浮层定位的计算
直接利用ref api 获取 dom元素的定位即可。具体代码如下:
const [isShow, setIsShow] = useState(false);
const [imgAbsolutePos, setImgAbsolutePos] = useState({});
useEffect(() => {
const getImgAbsolutePosition = () => {
const imgDom = imgRef.current;
const imgRect = imgDom.getBoundingClientRect();
return {
left: imgRect.left + imgRect.width / 2,
top: imgRect.top + imgRect.height / 2
}
}
const imgAbsolutePos = getImgAbsolutePosition();
setImgAbsolutePos(imgAbsolutePos);
}, [isShow]);
const toolTip = isShow && ReactDOM.createPortal(<>
<div className={css.card} style={{ position: 'absolute', zIndex: 9999, left: imgAbsolutePos.left - 45, top: imgAbsolutePos.top + 20 }}>
<Card/>
</div>
</>, document.body);
其中left和top值可以根据实际情况微调位置。
鼠标hover浮层显示
要实现这个功能,只需要onMouseEnter
onMouseLeave
这两个api即可。但是要实现鼠标移动到浮层后,浮层不会消失。鼠标移开图标或浮层,浮层消失就比较困难了。我的实现思路是借助setTimeout
延时器,给500ms的缓冲。
完整的代码如下
/**
* 自定义提示浮动块
*/
export default function CusToolTip() {
const imgRef = useRef(null);
const [isShow, setIsShow] = useState(false);
const [imgAbsolutePos, setImgAbsolutePos] = useState({});
let mouseInCard = false
useEffect(() => {
const getImgAbsolutePosition = () => {
const imgDom = imgRef.current;
const imgRect = imgDom.getBoundingClientRect();
return {
left: imgRect.left + imgRect.width / 2,
top: imgRect.top + imgRect.height / 2
}
}
const imgAbsolutePos = getImgAbsolutePosition();
setImgAbsolutePos(imgAbsolutePos);
}, [isShow]);
const onMouseEnterIcon = () => {
setIsShow(true);
}
const onMouseLeaveIcon = () => {
setTimeout(() => {
if (!mouseInCard) {
setIsShow(false);
}
}, 500);
}
const onCardMouseLeave = () => {
mouseInCard = false
setTimeout(() => {
if (!mouseInCard) {
setIsShow(false);
}
}, 500);
}
const toolTip = isShow && ReactDOM.createPortal(<>
<div className={css.card} style={{ position: 'absolute', left: imgAbsolutePos.left - 45, top: imgAbsolutePos.top + 20 }} onMouseEnter={() => mouseInCard = true} onMouseLeave={onCardMouseLeave}>
<Card/>
</div>
</>, document.body);
return <>
<img ref={imgRef} className={css.quanwenhao} src={quanWenhao} alt="" onMouseEnter={onMouseEnterIcon} onMouseLeave={onMouseLeaveIcon} />
{toolTip}
</>
}