You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

带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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 06:31:07