skip to content
月与羽

文字省略组件

/ 9 min read

1. 组件功能介绍

DynamicTruncate 是一个高级 React (TSX) 组件,旨在解决 Web 界面中常见的长文本展示问题。它通过智能检测和上下文感知交互,提供了远超原生 CSS 的用户体验。

核心功能清单:

  • 动态省略检测: 组件不依赖于固定的字符长度。它通过实时测量 DOM 元素的实际渲染尺寸,精确判断文本是否因空间不足而被 CSS 的 truncate 属性所截断。
  • 条件化 UI 渲染: 仅在文本确认被省略的情况下,组件才会在文本末尾渲染一个“详情”按钮。这保持了界面的简洁性,避免了在文本完整显示时出现不必要的操作。
  • 上下文感知 Popover: 点击“详情”按钮后,会弹出一个浮动卡片(Popover),而非一个打断工作流的居中模态框。这个 Popover 会精准地定位在触发它的“详情”按钮旁,为用户提供了无缝的上下文阅读体验。
  • 智能定位与自适应: Popover 的定位是完全动态和智能的。它能感知浏览器视口的边缘,如果预设的弹出位置空间不足,它会自动“翻转”到空间更充足的一侧,确保内容始终完整可见。
  • 丰富的用户交互: 组件内置了完善的交互逻辑。用户可以通过点击 Popover 之外的任何区域或按下 Escape 键来关闭它,这符合现代 Web 应用的交互标准。

2. 文字省略的实现逻辑

组件的“智能”核心在于其精确的省略检测机制。这一机制通过 React Hooks 与浏览器 DOM API 深度集成。

核心原理:scrollWidth vs clientWidth

我们通过比较一个 DOM 元素的两个关键宽度属性来判断其内容是否溢出:

  • clientWidth: 元素在屏幕上可见的宽度
  • scrollWidth: 元素内容在不换行的情况下所需要的完整宽度

truncate 生效时,scrollWidth 会大于 clientWidth。我们的逻辑就是利用这个不等式。

React Hooks 的协同实现

  1. useRef - 建立 DOM 连接 我们使用 useRef 创建一个对目标 <p> 元素的持久引用。这允许我们在渲染周期之外安全地访问其 DOM 节点。

    // 为 useRef 提供明确的元素类型,增强类型安全
    const textRef = useRef<HTMLParagraphElement>(null);
    // ... 在 JSX 中 ...
    <p ref={textRef} className="truncate" title={text}>
    {text}
    </p>
  2. useEffect - 执行测量 DOM 测量必须在浏览器完成布局和绘制后进行。useEffect 钩子是执行此类“副作用”的理想场所。

    useEffect(() => {
    const checkTruncation = () => {
    const element = textRef.current;
    // 确保 element 存在
    if (element) {
    // 核心判断逻辑
    const hasOverflow = element.scrollWidth > element.clientWidth;
    // 使用 useState 更新状态,触发UI重渲染
    if (hasOverflow !== isTruncated) {
    setIsTruncated(hasOverflow);
    }
    }
    };
    checkTruncation(); // 初始加载时检查
    window.addEventListener('resize', checkTruncation); // 窗口变化时重新检查
    // 清理函数:在组件卸载时移除监听,防止内存泄漏
    return () => window.removeEventListener('resize', checkTruncation);
    }, [text, isTruncated]); // 依赖项确保在文本变化时重新检测
  3. useState - 驱动 UI 变化 测量结果被存储在 isTruncated 状态中。这个状态直接控制“详情”按钮的渲染。

    const [isTruncated, setIsTruncated] = useState(false);
    // ... 在 JSX 中 ...
    {isTruncated && (
    <button /* ... */>
    详情
    </button>
    )}

3. Popover 定位库: @floating-ui/react

为了实现复杂而又健壮的 Popover 定位,我们引入了 @floating-ui/react。手动实现此功能不仅代码量巨大,而且极易出错。

库介绍与解决的问题

@floating-ui/react 是一个用于创建浮动元素(如工具提示、弹出框、下拉菜单)的现代化、轻量级 React 库。它将所有复杂的定位计算、边缘检测和交互管理都封装在简洁的 Hooks API 背后。

它为我们解决了以下棘手问题:

  • 定位计算: 如何让 Popover 精准出现在按钮的右上角?
  • 边缘碰撞: 如果按钮靠近屏幕顶部或侧边,如何防止 Popover 溢出视窗?
  • 动态更新: 当页面滚动或窗口大小改变时,如何让 Popover 的位置保持同步?
  • 交互管理: 如何轻松实现点击外部或按 Esc 键关闭 Popover?

在组件中的具体应用解析

  1. useFloating - 定位引擎 这是库的核心 Hook,负责所有的定位计算。

    const { refs, floatingStyles, context } = useFloating({
    open: isOpen, // 将库的可见状态与我们的 React state 绑定
    onOpenChange: setIsOpen, // 允许库反向更新我们的 state
    placement: 'top-end', // 首选位置:在锚点的“上方-末尾”对齐
    whileElementsMounted: autoUpdate, // 页面滚动或尺寸变化时自动更新位置
    middleware: [ /* ... */ ], // 定位增强插件
    });
    • placement: 定义了 Popover 相对于锚点(按钮)的初始位置。'top-end' 意味着 Popover 的右下角将对齐到按钮的右上角。
    • whileElementsMounted: autoUpdate: 这是一个非常强大的功能,它能确保即使用户滚动页面,Popover 也会像“粘”在按钮上一样实时更新位置。
  2. middleware - 智能定位的插件 middleware 是一个函数数组,用于在核心定位算法之外添加额外的智能行为。

    middleware: [
    // 在按钮和卡片之间留出 10px 的间隙
    offset(10),
    // 边缘翻转:如果上方空间不足,自动翻转到下方
    flip(),
    // 边缘移动:防止卡片溢出屏幕,与屏幕边缘保留 8px 边距
    shift({ padding: 8 }),
    ],
    • offset(): 提供视觉上的间距,避免 Popover 与按钮紧贴。
    • flip(): 解决了边缘碰撞问题。如果 'top-end' 会导致 Popover 溢出视窗顶部,flip() 会自动将其切换到 'bottom-end'
    • shift(): 解决了侧向溢出问题。如果 Popover 太宽导致伸出屏幕,shift() 会将其向内推,直到完全可见。
  3. useInteractions - 交互逻辑的瑞士军刀 这组 Hooks 负责处理用户的输入事件。

    const click = useClick(context); // 处理点击锚点时开关 Popover
    const dismiss = useDismiss(context); // 处理点击外部或按 Esc 关闭
    // 合并所有交互逻辑,生成可直接注入到 JSX 的 props
    const { getReferenceProps, getFloatingProps } = useInteractions([
    click,
    dismiss,
    ]);

    这些 Hooks 返回的 getReferencePropsgetFloatingProps 包含了所有必要的事件处理器(如 onClick, onKeyDown)和 ARIA 属性,我们只需用扩展运算符将它们应用到对应的元素上即可。

  4. 连接 JSX 最后,我们将 refsstylesprops 应用到我们的 JSX 元素上,完成整个系统的闭环。

    // 锚点元素(按钮)
    <button
    ref={refs.setReference} // 告诉库这是锚点
    {...getReferenceProps()} // 附加交互 props
    >
    详情
    </button>
    // 浮动元素(Popover)
    <div
    ref={refs.setFloating} // 告诉库这是要定位的元素
    style={floatingStyles} // 应用计算出的定位样式!
    {...getFloatingProps()} // 附加交互 props
    >
    {/* ... Popover 内容 ... */}
    </div>

    通过这种方式,@floating-ui/react 让我们能够以一种声明式、高内聚的方式,构建出功能强大且行为可预测的浮动 UI,而无需关心底层的复杂实现。