import { useCallback, useEffect, useRef, useState, forwardRef, type ReactNode, type CSSProperties, } from 'react' import { clsx } from 'clsx' import { Tooltip } from './tooltip' export interface EllipsisTextProps { /** Text or inline content to display. */ children: ReactNode /** * Maximum visible lines before truncation. * @default 1 */ lines?: number /** Extra className for the text node. */ className?: string /** * Tooltip placement when overflow is detected. * @default 'top' */ placement?: 'top' | 'bottom' | 'left' | 'right' /** Extra className for Tooltip content. */ tooltipClassName?: string /** Custom tooltip content. Defaults to children. */ tooltipContent?: ReactNode /** * Disable tooltip even when content overflows. * @default false */ disabled?: boolean /** Inline style for the text node. */ style?: CSSProperties /** * Rendered text element. * @default 'span' */ as?: 'span' | 'div' | 'p' } function setRef(ref: React.Ref | undefined, value: T | null) { if (!ref) return if (typeof ref === 'function') ref(value) else (ref as React.MutableRefObject).current = value } export const EllipsisText = forwardRef( ( { children, lines = 1, className = '', placement = 'top', tooltipClassName = '', tooltipContent, disabled = false, style, as: Component = 'span', }, ref ) => { const textRef = useRef(null) const [isOverflowing, setIsOverflowing] = useState(false) const checkOverflow = useCallback(() => { const el = textRef.current if (!el) return const overflowing = lines === 1 ? el.scrollWidth > el.clientWidth : el.scrollHeight > el.clientHeight setIsOverflowing(overflowing) }, [lines]) useEffect(() => { const raf = requestAnimationFrame(() => checkOverflow()) const el = textRef.current if (!el) return () => cancelAnimationFrame(raf) const ro = new ResizeObserver(() => checkOverflow()) ro.observe(el) window.addEventListener('resize', checkOverflow) return () => { cancelAnimationFrame(raf) ro.disconnect() window.removeEventListener('resize', checkOverflow) } }, [checkOverflow, children]) const ellipsisStyle: CSSProperties = lines === 1 ? { display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', ...style, } : { overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: lines, WebkitBoxOrient: 'vertical', ...style, } const refCallback = useCallback( (el: HTMLElement | null) => { setRef(textRef, el) setRef(ref, el) }, [ref] ) const triggerNode = ( {children} ) const useTooltip = isOverflowing && !disabled if (useTooltip) { return ( {children} ) } return triggerNode } ) EllipsisText.displayName = 'EllipsisText'