文字省略组件
/ 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 的协同实现
-
useRef- 建立 DOM 连接 我们使用useRef创建一个对目标<p>元素的持久引用。这允许我们在渲染周期之外安全地访问其 DOM 节点。// 为 useRef 提供明确的元素类型,增强类型安全const textRef = useRef<HTMLParagraphElement>(null);// ... 在 JSX 中 ...<p ref={textRef} className="truncate" title={text}>{text}</p> -
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]); // 依赖项确保在文本变化时重新检测 -
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?
在组件中的具体应用解析
-
useFloating- 定位引擎 这是库的核心 Hook,负责所有的定位计算。const { refs, floatingStyles, context } = useFloating({open: isOpen, // 将库的可见状态与我们的 React state 绑定onOpenChange: setIsOpen, // 允许库反向更新我们的 stateplacement: 'top-end', // 首选位置:在锚点的“上方-末尾”对齐whileElementsMounted: autoUpdate, // 页面滚动或尺寸变化时自动更新位置middleware: [ /* ... */ ], // 定位增强插件});placement: 定义了 Popover 相对于锚点(按钮)的初始位置。'top-end'意味着 Popover 的右下角将对齐到按钮的右上角。whileElementsMounted: autoUpdate: 这是一个非常强大的功能,它能确保即使用户滚动页面,Popover 也会像“粘”在按钮上一样实时更新位置。
-
middleware- 智能定位的插件middleware是一个函数数组,用于在核心定位算法之外添加额外的智能行为。middleware: [// 在按钮和卡片之间留出 10px 的间隙offset(10),// 边缘翻转:如果上方空间不足,自动翻转到下方flip(),// 边缘移动:防止卡片溢出屏幕,与屏幕边缘保留 8px 边距shift({ padding: 8 }),],offset(): 提供视觉上的间距,避免 Popover 与按钮紧贴。flip(): 解决了边缘碰撞问题。如果'top-end'会导致 Popover 溢出视窗顶部,flip()会自动将其切换到'bottom-end'。shift(): 解决了侧向溢出问题。如果 Popover 太宽导致伸出屏幕,shift()会将其向内推,直到完全可见。
-
useInteractions- 交互逻辑的瑞士军刀 这组 Hooks 负责处理用户的输入事件。const click = useClick(context); // 处理点击锚点时开关 Popoverconst dismiss = useDismiss(context); // 处理点击外部或按 Esc 关闭// 合并所有交互逻辑,生成可直接注入到 JSX 的 propsconst { getReferenceProps, getFloatingProps } = useInteractions([click,dismiss,]);这些 Hooks 返回的
getReferenceProps和getFloatingProps包含了所有必要的事件处理器(如onClick,onKeyDown)和 ARIA 属性,我们只需用扩展运算符将它们应用到对应的元素上即可。 -
连接 JSX 最后,我们将
refs、styles和props应用到我们的 JSX 元素上,完成整个系统的闭环。// 锚点元素(按钮)<buttonref={refs.setReference} // 告诉库这是锚点{...getReferenceProps()} // 附加交互 props>详情</button>// 浮动元素(Popover)<divref={refs.setFloating} // 告诉库这是要定位的元素style={floatingStyles} // 应用计算出的定位样式!{...getFloatingProps()} // 附加交互 props>{/* ... Popover 内容 ... */}</div>通过这种方式,
@floating-ui/react让我们能够以一种声明式、高内聚的方式,构建出功能强大且行为可预测的浮动 UI,而无需关心底层的复杂实现。