React 文字超出显示省略号并且触发Tooltip 的组件封装

377 阅读3分钟

React 文字超出显示省略号并且触发Tooltip 的组件封装

背景

项目中的遇到了很多的文案显示,比如form表单的label、checkbox等等的文案显示。要求的是没有超出固定的宽度只显示本身的文字,如果超出的固定宽度是显示省略号并且鼠标移上去显示tooltip。查看的一下组件库也没有该组件,于是思考了一下,不得不自己封装一个组件。

方法1

通过创建一个标签来跟原标签的宽度对比来判断是否显示省略号,对比再删除创建的标签

封装步骤

第一步 checkIsOverflowed 方法

先封装一个能够自动获取到传入文案的宽度的方法。

创建一个标签,根据传入的字号来获取到宽度与传入的宽度比较一下。

export const checkIsOverflowed = (
    ref: any, 
    width: number = 0,
    fontSize: number = 12
) => {
    const element = ref
    const tempDiv = document.createElement('div');
    tempDiv.style.position = 'absolute';
    tempDiv.style.top = '9999px';
    tempDiv.style.fontSize = `${fontSize}px`

    tempDiv.textContent = element.textContent;
    document.body.appendChild(tempDiv);

    const isOverflowed = tempDiv.clientWidth > width;

    document.body.removeChild(tempDiv);

    return isOverflowed
}
第二步 OverflowTooltip 组件

根据 checkIsOverflowed 方法来获取到是否显示省略号,来判断是显示tooltip组件还是本身的文本。 为了方便外部控制组件的样式,所以暴露了 className, tooltipOverlayClassName,style三个参数。

export const OverflowTooltip: FC<OverflowProps> = ({
    text,
    width,
    ellipsisWidth,
    fontSize = 14,
    className = '',
    tooltipOverlayClassName = '',
    style = {},
}) => {
    const overflowRef = useRef<HTMLDivElement>(null)
    // 是否出现省略号
    const [isOverflow, setIsOverflow] = useState<boolean>(false)

    useEffect(() => {
        const isOverflowed = checkIsOverflowed(overflowRef?.current, width, fontSize)
        setIsOverflow(isOverflowed)
    }, [overflowRef.current, text])

    const renderItem = () => {
        if (!isOverflow) {
            return <div>{text}</div>
        }

        return (
            <Tooltip
                title={text}
                overlayClassName={tooltipOverlayClassName}
            >
                <div
                    style={{
                        whiteSpace: 'nowrap',
                        overflow: 'hidden',
                        textOverflow: 'ellipsis',
                        width: ellipsisWidth
                    }}
                >{text}</div>
            </Tooltip>
        )
    }

    return (
        <div
            ref={overflowRef}
            className={className}
            style={style}
        >
            {renderItem()}
        </div>
    )
}

完整代码

import { FC, useRef, useEffect, useState } from 'react'
import { Tooltip } from 'antd'


export const checkIsOverflowed = (
    ref: any, 
    width: number = 0,
    fontSize: number = 12
) => {
    const element = ref
    const tempDiv = document.createElement('div');
    tempDiv.style.position = 'absolute';
    tempDiv.style.top = '9999px';
    tempDiv.style.fontSize = `${fontSize}px`

    tempDiv.textContent = element.textContent;
    document.body.appendChild(tempDiv);

    const isOverflowed = tempDiv.clientWidth > width;

    document.body.removeChild(tempDiv);

    return isOverflowed
}

interface OverflowProps {
    text: React.ReactNode
    width: number
    ellipsisWidth: number
    fontSize?: number
    className?: any
    tooltipOverlayClassName?: any
    style?: any
}

export const OverflowTooltip: FC<OverflowProps> = ({
    text,
    width,
    ellipsisWidth,
    fontSize = 14,
    className = '',
    tooltipOverlayClassName = '',
    style = {},
}) => {
    const overflowRef = useRef<HTMLDivElement>(null)
    // 是否出现省略号
    const [isOverflow, setIsOverflow] = useState<boolean>(false)

    useEffect(() => {
        const isOverflowed = checkIsOverflowed(overflowRef?.current, width, fontSize)
        setIsOverflow(isOverflowed)
    }, [overflowRef.current, text])

    const renderItem = () => {
        if (!isOverflow) {
            return <div>{text}</div>
        }

        return (
            <Tooltip
                title={text}
                overlayClassName={tooltipOverlayClassName}
            >
                <div
                    style={{
                        whiteSpace: 'nowrap',
                        overflow: 'hidden',
                        textOverflow: 'ellipsis',
                        width: ellipsisWidth
                    }}
                >{text}</div>
            </Tooltip>
        )
    }

    return (
        <div
            ref={overflowRef}
            className={className}
            style={style}
        >
            {renderItem()}
        </div>
    )
}

方法2

原理

  • 创建两个标签,一个标签可能是多行显示,另个一个标签是永远是单行显示。

  • 对比两个标签的高度来判断是否显示省略号。

注意: 创建的两个标签是不会在文档流中显示

参考 ant-design-mobile ellipsis组件

完整代码

/**
 * * aria-hidden: https://developer.mozilla.org/zh-CN/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden
 * * \u00A0:  HTML 中的作用主要是表示不换行空格, 保持文本连续性:在一些需要保持特定文本内容在同一行显示,避免因浏览器窗口大小调整或其他因素导致换行的场景中,\u00A0非常有用。
 */
import { FC, useRef, useEffect, useState, useLayoutEffect, Fragment } from 'react'
import { Tooltip } from 'antd'

enum MEASURE_STATUS {
    PREPARE = 1,
    ELLIPSIS = 99,
    NO_ELLIPSIS = 100
}

const measureStyle: React.CSSProperties = {
    visibility: 'hidden',
    whiteSpace: 'inherit',
    lineHeight: 'inherit',
    fontSize: 'inherit',
}

interface OverflowProps {
    text: React.ReactNode
    width: number
    fontSize?: number
    className?: any
    tooltipOverlayClassName?: any
    style?: any
}

export const OverflowTooltip: FC<OverflowProps> = ({
    text,
    width,
    className = '',
    tooltipOverlayClassName = '',
    style = {},
}) => {
    const [status, setStatus] = useState<MEASURE_STATUS>(
        MEASURE_STATUS.PREPARE
    )
    const singleRowMeasureRef = useRef<HTMLDivElement>(null)
    const fullMeasureRef = useRef<HTMLDivElement>(null)

    useLayoutEffect(() => {
        if (status === MEASURE_STATUS.PREPARE) {
            const fullMeasureHeight = fullMeasureRef.current?.offsetHeight || 0
            const singleRowMeasureHeight = singleRowMeasureRef.current?.offsetHeight || 0

            if (fullMeasureHeight <= singleRowMeasureHeight) {
                setStatus(MEASURE_STATUS.NO_ELLIPSIS)
            }
            else {
                setStatus(MEASURE_STATUS.ELLIPSIS)
            }
        }
    }, [status])

    const renderContent = () => {
        if (status === MEASURE_STATUS.NO_ELLIPSIS) {
            return (
                <div>{text}</div>
            )
        }

        if (status === MEASURE_STATUS.ELLIPSIS) {
            return (
                <Tooltip
                    title={text}
                    overlayClassName={tooltipOverlayClassName}
                >
                    <div
                        style={{
                            whiteSpace: 'nowrap',
                            overflow: 'hidden',
                            textOverflow: 'ellipsis',
                            width: width
                        }}
                    >{text}</div>
                </Tooltip>
            )
        }

        return null
    }

    const renderItem = () => {
        return (
            <>
                {/* 文本完全显示,折行显示 */}
                {
                    status === MEASURE_STATUS.PREPARE && (
                        <div
                            key='full'
                            aria-hidden
                            ref={fullMeasureRef}
                            style={{
                                ...measureStyle,
                                width: width
                            }}
                        >
                            {text}
                        </div>
                    )
                }

                {/* 控制单行显示 */}
                {
                    status === MEASURE_STATUS.PREPARE && (
                        <div
                            key='stable'
                            aria-hidden
                            ref={singleRowMeasureRef}
                            style={{
                                ...measureStyle,
                                width: width
                            }}
                        >
                            {'\u00A0'}
                        </div>
                    )
                }

                {/* 真正的显示文本 */}
                {renderContent()}
            </>
        )

    }

    return (
        <div
            className={className}
            style={{
                ...style,
                overflow: 'hidden',
                wordBreak: 'break-word'
            }}
        >
            {renderItem()}
        </div>
    )
}

使用后的效果

image.png

总结

根据在项目中的需求来封装一些组件,方便复用,以后就是省时省力。

方法1的思路:通过对传入文本的宽度和要求的宽度进行对比,来显示原本的文本还是tooltip组件。但是因为会进行dom元素删除,大量的话会造成性能问题

方法2的思路:通过创建两个标签并且不在文档流中显示出来, 对比高度来判断是否显示省略号。 判断完就把这两个标签就不显示了,只不过就是在jsx中操作的。最后来决定显示原本的文本还是tooltip组件。