简记|React+Antd中实现 tooltip、ellipsis、copyable功能组件

170 阅读3分钟

前言

在项目中使用了antdProTable,其tooltipellipsiscopyable在某些情况下具有局限性,比如:1、渲染html内容;2、长文本使用tooltip的情况下,宽度没有自动适应导致tooltip纵向太长,如下图:

image.png

所以,我需要重新封装一个适用的组件:ColumnOverflow

组件介绍

ColumnOverflow 是一个用于处理文本溢出的 React 组件,它提供了以下功能:

  • 文本溢出时自动显示省略号
  • 鼠标悬停时显示完整内容的 Tooltip
  • 支持 HTML 内容渲染
  • 可选的复制功能
  • 支持水平和垂直方向的溢出检测
  • ...可扩展

组件API定义

type Props = {
  title: string;  // 显示的文本内容(支持HTML)
  copyText?: string;  // 复制按钮复制的文本,默认为title
  direction?: 'horizontal' | 'vertical';  // 溢出检测方向
  isShowCopyable?: boolean;  // 是否显示复制按钮
  styles?: React.CSSProperties;  // Tooltip 的自定义样式
  placement?: TooltipPlacement; // 气泡框位置
};

布局实现

组件采用 styled-components 进行样式管理,使用 Flex 布局实现灵活的空间分配:

import styled from 'styled-components';

export const ColumnOverflowStyle = styled.div`
  .column-overflow {
    position: relative;
    width: 100%;
    display: flex;
    align-items: center;

    &__content {
      flex: 1;
      min-width: 0;
    }
    &__hidden {
      width: 100%;
      > div {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        width: 100%;
      }
    }
  }
`;

溢出检测

组件通过比较元素的 scrollWidth/scrollHeight 和 clientWidth/clientHeight 来检测内容是否溢出:

const visibilityChange = useCallback(
      (event: any) => {
        const ev = event.target;
        let evSize = ev.scrollWidth;
        let contentSize = ev.clientWidth;
        if (direction === 'vertical') {
          evSize = ev.scrollHeight;
          contentSize = ev.clientHeight;
        }
        if (evSize > contentSize) {
          setShowTooltip(true);
        } else {
          setShowTooltip(false);
        }
      },
      [direction]
    );

HTML 内容渲染

组件直接支持在 title 属性中传入 HTML 内容,并在 Tooltip 和主内容区域进行渲染:

<Tooltip
  overlayInnerStyle={{ whiteSpace: 'pre-wrap', ...styles }}
  title={<div dangerouslySetInnerHTML={{ __html: title }} />}
  open={showTooltip}
>
  <div className="column-overflow__hidden">
    <div dangerouslySetInnerHTML={{ __html: title }} />
  </div>
</Tooltip>

复制功能集成

通过 Ant Design 的 Typography.Text 组件实现复制功能,没传copytext的情况下默认为title。为什么要设置titlecopyText两个API是因为,title可能是需要渲染html的,而我们复制的内容并不需要html

{isShowCopyable && (
  <Typography.Text
    className="ml10"
    copyable={{
      text: title
    }}
  />
)}

使用示例

// 基础使用
<ColumnOverflow title="这是一段很长的文本..." />

// 带HTML内容
<ColumnOverflow title="<span style='color: red'>带HTML样式的文本</span>" />

// 启用复制功能,自定义复制内容
<ColumnOverflow 
  title="<span style='color: red'>带样式的文本</span>"
  copyText="纯文本内容"
  isShowCopyable={true}
/>

// 垂直方向溢出检测
<ColumnOverflow 
  title="多行文本..."
  direction="vertical"
/>

组件完整代码

  • ColumnOverflow.tsx
import React, { useState, useCallback } from 'react';

import { Tooltip, Typography } from 'antd';

import { ColumnOverflowStyle } from './styles';

import type { TooltipPlacement } from 'antd/lib/tooltip';

type Props = {
  title: string;
  copyText?: string;
  direction?: 'horizontal' | 'vertical';
  isShowCopyable?: boolean;
  styles?: React.CSSProperties;
  placement?: TooltipPlacement;
};

const ColumnOverflow: React.FC<Props> = React.memo(
  ({
    title,
    copyText = title,
    direction = 'horizontal',
    isShowCopyable = false,
    styles = {},
    placement = 'top'
  }) => {
    const [showTooltip, setShowTooltip] = useState(false);

    const visibilityChange = useCallback(
      (event: any) => {
        const ev = event.target;
        let evSize = ev.scrollWidth;
        let contentSize = ev.clientWidth;
        if (direction === 'vertical') {
          evSize = ev.scrollHeight;
          contentSize = ev.clientHeight;
        }
        if (evSize > contentSize) {
          setShowTooltip(true);
        } else {
          setShowTooltip(false);
        }
      },
      [direction]
    );

    let copyableElement = null;
    if (isShowCopyable) {
      copyableElement = (
        <Typography.Text
          className="ml10"
          copyable={{
            text: copyText
          }}
        />
      );
    }

    const htmlDom = <div dangerouslySetInnerHTML={{ __html: title }} />;

    return (
      <ColumnOverflowStyle>
        <div
          className="column-overflow"
          onMouseEnter={visibilityChange}
          onMouseLeave={() => setShowTooltip(false)}
        >
          <div className="column-overflow__content">
            <Tooltip
              overlayStyle={{ ...styles }}
              overlayInnerStyle={{
                whiteSpace: 'pre-wrap'
              }}
              title={htmlDom}
              open={showTooltip}
              placement={placement}
            >
              <div className="column-overflow__hidden">{htmlDom}</div>
            </Tooltip>
          </div>
          {copyableElement}
        </div>
      </ColumnOverflowStyle>
    );
  }
);

export default ColumnOverflow;

  • 以上代码是antd 4.x的用法,antd 5.x废弃了overlayStyleoverlayInnerStyle的写法,将以下组件替换即可:
<Tooltip
   styles={{
      root: { ...styles },
      body: {
        whiteSpace: 'pre-wrap'
      }
   }}
  title={htmlDom}
  open={showTooltip}
  placement={placement}
>
  <div className="column-overflow__hidden">{htmlDom}</div>
</Tooltip>
  • styles.ts见上文的布局实现

使用效果:

  • 使用前:

image.png

  • 使用后:antd 4.x image.png

  • 使用后:antd 5.x image.png

  • 渲染html内容

image.png