带Show more/Show less按钮的可展开文本组件窗口适配问题
解决TailwindCSS可展开文本组件的窗口resize适配问题
问题根源
你的组件当前仅在元素引用初始化时判断内容是否截断,未监听窗口resize事件。当窗口尺寸变化后,内容可能不再需要截断,但组件不会重新计算状态,导致按钮无法自动消失。
最优解决方案
通过监听窗口resize事件并结合防抖优化,实时重新判断内容是否需要截断。同时优化状态判断逻辑,区分展开/收起状态下的按钮显示规则:
修改后的useTruncatedElement Hook
import { RefObject, useLayoutEffect, useState, useCallback } from 'react'; // 防抖函数,避免resize事件频繁触发计算 const debounce = (func: () => void, delay: number) => { let timeoutId: NodeJS.Timeout; return () => { clearTimeout(timeoutId); timeoutId = setTimeout(func, delay); }; }; export const useTruncatedElement = (ref: RefObject<HTMLDivElement | null>) => { const [isTruncated, setIsTruncated] = useState(false); const [isShowingLess, setIsShowingLess] = useState(true); // 核心判断逻辑:仅在收起状态下检查内容是否截断 const checkTruncation = useCallback(() => { const element = ref.current; if (!element) return; if (isShowingLess) { const { offsetHeight, scrollHeight } = element; setIsTruncated(offsetHeight < scrollHeight); } else { // 展开状态下始终显示"Show less"按钮 setIsTruncated(true); } }, [ref, isShowingLess]); useLayoutEffect(() => { // 初始化时检查一次 checkTruncation(); // 添加窗口resize监听,防抖处理 const handleResize = debounce(checkTruncation, 150); window.addEventListener('resize', handleResize); // 组件卸载时清理监听 return () => { window.removeEventListener('resize', handleResize); }; }, [checkTruncation]); const toggleIsShowingMore = () => { setIsShowingLess(prev => !prev); // 切换状态后延迟检查,确保DOM更新完成 setTimeout(checkTruncation, 0); }; return { isTruncated, isShowingLess, toggleIsShowingMore, }; };
修改后的ExpandableText组件
'use client'; import { useRef } from 'react'; import { useTruncatedElement } from '@/hooks/useTruncatedElement'; import { cn } from '@/lib/utils'; import { Button } from '../ui/button'; type ExpandableTextProps = { children: React.ReactNode; visibleLines?: number; className?: string; }; const ExpandableText = ({ children, visibleLines = 6, className }: ExpandableTextProps) => { const ref = useRef<HTMLDivElement>(null); const { isTruncated, isShowingLess, toggleIsShowingMore } = useTruncatedElement(ref); const classList = cn('break-all', isShowingLess && `line-clamp-${visibleLines}`, className); return ( <div className="relative inline-block"> <div ref={ref} className={classList}> {children} </div> {isTruncated && ( <Button variant="link" onClick={toggleIsShowingMore}> {isShowingLess ? 'Show more' : 'Show less'} </Button> )} </div> ); }; export default ExpandableText;
关于统计可视行数的疑问
统计可视行数可以通过计算元素高度与行高的比值实现:
const getVisibleLines = (element: HTMLDivElement) => { const style = getComputedStyle(element); const lineHeight = parseInt(style.lineHeight, 10); const elementHeight = element.offsetHeight - parseInt(style.paddingTop, 10) - parseInt(style.paddingBottom, 10); return Math.floor(elementHeight / lineHeight); };
但这种方法存在明显局限性:
- 行高可能为相对值(如
1.5),需额外计算实际像素值 - 元素的内边距、边框会干扰高度计算
- 不如直接对比
offsetHeight和scrollHeight的方法可靠
对比offsetHeight与scrollHeight是当前场景的最优方案:当line-clamp生效时,offsetHeight就是限制行数的高度,scrollHeight是完整内容的高度,两者的大小关系直接反映内容是否被截断,无需额外计算行数,逻辑更简洁可靠。
内容的提问来源于stack exchange,提问作者Kit
相关产品推荐
相关产品推荐

