div+css模拟实现Tooltip

前言

最近公司有个需求,大致概括为如下:

  • 在一个编辑框内,新增某个特殊展示的内容(这个内容由JS手动创建标签)
  • 然后鼠标移到这个特殊内容上时,显示 Tooltip 效果
  • 一个编辑框内可能有多个特殊内容

实现后的效果其实就是这样:

由于以前的代码很老了,四五年前的代码,然后通过创建 span 标签包裹内容,替换原来的文本节点(因此有这个黄色的样式效果),所以不能直接包 antd 的 ToolTip,需要手动实现

实现思路

  • 通过 div + css 实现 ReplaceTextTooltip 组件(ToolTip 的样式效果)
  • 在最外层 div 容器上绑定 onMouseOver,判断是否移入特殊内容的DOM节点
  • 如果是,获取节点的坐标;如果不是,则重置 ReplaceTextTooltip 的位置
  • 计算偏移,把最终的位置传给 ReplaceTextTooltip

代码

最外层绑定 onMouseOver

tsx 复制代码
// React 类组件
<div className='container'
    onMouseOver={(e) => {
       this.showReplaceAllTextPopover(e);
    }}
>
   //...
</div>

// 模拟 ToolTip 组件
<ReplaceTextTooltip
    elementInfo={popoverPositions}  // 位置
    content={popoverReplaceText} // hover时显示的内容
/>

onMouseover回调

tsx 复制代码
  // 展示文本替代的 popover
  showReplaceAllTextPopover = (e) => {
    const { hoverReplaceTooltip } = this.state;
    if (
      e.target.className === 'eReplaceTextPronunciation_tag' &&
      !hoverReplaceTooltip
    ) {
      // 获取 DOM 节点的坐标和宽度
      const { left, top, width } = e.target.getBoundingClientRect();
      this.setState({
        // popoverPositions 用来透传给组件
        popoverPositions: {
          left,
          top,
          width,
        },
        hoverReplaceTooltip: true,
        popoverReplaceText: e.target.getAttribute('data-ereplacealltext') || '',
        popoverRenderDomText: e.target.innerText || '',
      });
    } else {
      // hoverReplaceTooltip 是判断当前是不是已经移入某个特殊DOM了
      if (hoverReplaceTooltip) {
        this.setState({
          popoverPositions: {
            left: 0,
            top: 0,
            width: 0,
          },
          hoverReplaceTooltip: false,
          popoverRenderDomText: '',
          popoverReplaceText: '',
        });
      }
    }
  };

ReplaceTextToolTip

tsx 复制代码
import React, { FC, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import './style.less';

interface IProps {
  elementInfo: {
    left: number;
    top: number;
    width: number;
  };
  content: string;
}

const ReplaceTextTooltip: FC<IProps> = ({
  elementInfo = {
    left: 0,
    top: 0,
    width: 0,
  },
  content = '',
}) => {
  const contentRef = useRef<any>();
  const popContainerRef = useRef<any>();
  const [dealPosition, setDealPosition] = useState<{
    left: string;
    top: string;
  }>({
    left: '0px',
    top: '0px',
  });

  useEffect(() => {
    const { left, top, width } = elementInfo;
    if (left !== 0 && top !== 0) {
      const Left =
        left -
        (contentRef.current.offsetWidth
          ? contentRef.current.offsetWidth / 2
          : 0) +
        (contentRef.current.offsetWidth ? width / 2 : 0);
      const Top = top - (contentRef.current.offsetHeight > 33 ? 15 : 0) - 60;
      setDealPosition({
        left: `${Left}px`,
        top: `${Top}px`,
      });
    } else {
      setDealPosition({
        left: `0px`,
        top: `0px`,
      });
    }
  }, [elementInfo]);

  return ReactDOM.createPortal(
    <div className="absolute top-0 left-0 w-[100%]">
      <div>
        <div
          ref={popContainerRef}
          style={dealPosition}
          className="absolute border-box m-0 p-0 text-[#000000a5] text-[14px] leading-[1.5] list-none max-w-[250px] visible pb-md"
        >
          <div>
            <div className="replacePopover-arrow absolute left-[50%] translate-x-[-50%] bottom-[-5.071068px] w-[13.07106781px] h-[13.07106781px] block overflow-hidden pointer-events-none"></div>
            <div
              ref={contentRef}
              className="min-w-[30px] min-h-[32px] px-[8px] py-[6px] text-[#fff] text-center text-wrap bg-[#000000bf] rounded-[4px] shadow-[0 2px 8px rgba(0,0,0,.15)]"
            >
              <span>{content}</span>
            </div>
          </div>
        </div>
      </div>
    </div>,
    document.body,
  );
};

export default ReplaceTextTooltip;

其中,这段代码是使得 ToolTip 在 DOM 正上方显示的逻辑:

tsx 复制代码
useEffect(() => {
    const { left, top, width } = elementInfo;
    if (left !== 0 && top !== 0) {
      const Left =
        left -
        (contentRef.current.offsetWidth
          ? contentRef.current.offsetWidth / 2
          : 0) +
        (contentRef.current.offsetWidth ? width / 2 : 0);
      const Top = top - (contentRef.current.offsetHeight > 33 ? 15 : 0) - 60;
      setDealPosition({
        left: `${Left}px`,
        top: `${Top}px`,
      });
    } else {
      setDealPosition({
        left: `0px`,
        top: `0px`,
      });
    }
  }, [elementInfo]);

这里面最重要的就是 Left 的计算,最开始我是直接使用 left

tsx 复制代码
const Left = left;

但是效果确实这样的:

也就是 ToolTip 内容的开始位置和 DOM 的起始位置是一样的,所以需要计算在 X 轴的偏移量

计算 X 轴的偏移量

计算的思路是:

  • 首先让 ToolTip 向左偏移自己内容宽度的 1/2
  • 再让 ToolTip 向右偏移DOM的宽度 1/2

也就是

tsx 复制代码
const Left =
        left -
        // contentRef 就是 ToolTip 自己
        (contentRef.current.offsetWidth 
          ? contentRef.current.offsetWidth / 2
          : 0) +
          // width 就是 DOM 的宽度
        (contentRef.current.offsetWidth ? width / 2 : 0);

最后

人为什么要上班

相关推荐
wakangda22 分钟前
React Native 集成原生Android功能
javascript·react native·react.js
秃头女孩y6 小时前
【React中最优雅的异步请求】
javascript·vue.js·react.js
前端小小王12 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发13 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
不是鱼17 小时前
构建React基础及理解与Vue的区别
前端·vue.js·react.js
飞翔的渴望20 小时前
antd3升级antd5总结
前端·react.js·ant design
╰つ゛木槿1 天前
深入了解 React:从入门到高级应用
前端·react.js·前端框架
用户30587584891251 天前
Connected-react-router核心思路实现
react.js
哑巴语天雨2 天前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情2 天前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js