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);

最后

人为什么要上班

相关推荐
早點睡3908 小时前
高级进阶 ReactNative for Harmony 项目鸿蒙化三方库集成实战:react-native-drag-sort
react native·react.js·harmonyos
C澒9 小时前
Vue 项目渐进式迁移 React:组件库接入与跨框架协同技术方案
前端·vue.js·react.js·架构·系统架构
发现一只大呆瓜10 小时前
虚拟列表:从定高到动态高度的 Vue 3 & React 满分实现
前端·vue.js·react.js
全栈探索者11 小时前
列表渲染不用 map,用 ForEach!—— React 开发者的鸿蒙入门指南(第 4 期)
react.js·harmonyos·arkts·foreach·列表渲染
程序员Agions12 小时前
useMemo、useCallback、React.memo,可能真的要删了
前端·react.js
NEXT0612 小时前
React Hooks 进阶:useState与useEffect的深度理解
前端·javascript·react.js
早點睡39013 小时前
基础入门 React Native 鸿蒙跨平台开发:react-native-flash-message 消息提示三方库适配
react native·react.js·harmonyos
早點睡39014 小时前
高级进阶 ReactNative for Harmony项目鸿蒙化三方库集成实战:react-native-image-picker(打开手机相册)
react native·react.js·harmonyos
早點睡39014 小时前
基础入门 React Native 鸿蒙跨平台开发:react-native-easy-toast三方库适配
react native·react.js·harmonyos
●VON1 天前
React Native for OpenHarmony:2048 小游戏的开发与跨平台适配实践
javascript·学习·react native·react.js·von