import { createContext, HTMLAttributes, useCallback, useContext, useLayoutEffect, useMemo, useRef, useState, } from 'react' const MAX_LINES = 2 const getLineHeightPx = (element: HTMLElement): number => { const style = window.getComputedStyle(element) const lineHeight = parseFloat(style.lineHeight) if (!Number.isNaN(lineHeight) && lineHeight > 0) return lineHeight // `line-height: normal` fallback (approx. 1.2 * font-size) const fontSize = parseFloat(style.fontSize) return (!Number.isNaN(fontSize) && fontSize > 0 ? fontSize : 16) * 1.2 } const elementOverflowsLines = (measureEl: HTMLElement, lineHeightPx: number, maxLines: number): boolean => { const maxHeight = lineHeightPx * maxLines const height = measureEl.getBoundingClientRect().height return height > maxHeight + 0.5 } export interface IMiddleTruncate extends HTMLAttributes { children: string tail?: number } type TTruncateContext = { tail: number | undefined } export const TruncateContext = createContext({ tail: 5 }) export const Truncate = ({ children, tail: tailProp, ...props }: IMiddleTruncate): JSX.Element => { const firstPart = children const context = useContext(TruncateContext) const tail = tailProp !== undefined ? tailProp : context.tail || 0 const secondPart = children.length <= tail + 3 ? null : children.slice(-tail) const showingSecondPart = secondPart !== null && tail > 0 const wrapperRef = useRef(null) const measureRef = useRef(null) const [isOverflowing, setIsOverflowing] = useState(false) const computeOverflow = useCallback(() => { const wrapper = wrapperRef.current const measureEl = measureRef.current if (!wrapper || !measureEl) return const lineHeightPx = getLineHeightPx(wrapper) setIsOverflowing(elementOverflowsLines(measureEl, lineHeightPx, MAX_LINES)) }, []) const resizeObserver = useMemo(() => { return new ResizeObserver((entries) => { if (entries.some((entry) => entry.target === wrapperRef.current)) { computeOverflow() } }) }, [computeOverflow]) useLayoutEffect(() => { if (wrapperRef.current) { resizeObserver.observe(wrapperRef.current) } computeOverflow() return () => resizeObserver.disconnect() }, [children, resizeObserver, computeOverflow]) return ( {firstPart} {tail !== 0 && ( {secondPart} )} {/* Hidden measuring element (full text, wraps normally) */} {children} ) }