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}
)
}