65 lines
2.0 KiB
TypeScript
65 lines
2.0 KiB
TypeScript
import type { ComponentProps, CSSProperties } from 'react'
|
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
|
import { useResizeObserver } from '@/hooks/use-resize-observer'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface FadeTextProps extends Omit<ComponentProps<'span'>, 'children'> {
|
|
children: React.ReactNode
|
|
/**
|
|
* Width of the fade region on the trailing edge. Accepts any CSS length.
|
|
* Defaults to 3rem so long strings clearly trail off — short enough to
|
|
* preserve readable content, long enough to feel like a deliberate fade
|
|
* rather than a clipped ellipsis.
|
|
*/
|
|
fadeWidth?: string
|
|
}
|
|
|
|
/**
|
|
* Single-line text that fades out instead of truncating with an ellipsis.
|
|
*
|
|
* Uses an inline mask-image so the fade resolves against whatever the parent
|
|
* background is — no need to know the surface color, no after-pseudo overlap.
|
|
* The mask is only applied when the text is actually overflowing, so short
|
|
* strings render as plain text without an unnecessary gradient on their tail.
|
|
*/
|
|
export function FadeText({ children, className, fadeWidth = '3rem', style, ...rest }: FadeTextProps) {
|
|
const ref = useRef<HTMLSpanElement>(null)
|
|
const [overflowing, setOverflowing] = useState(false)
|
|
|
|
const measureOverflow = useCallback(() => {
|
|
const el = ref.current
|
|
|
|
if (!el) {
|
|
return
|
|
}
|
|
|
|
setOverflowing(el.scrollWidth - el.clientWidth > 1)
|
|
}, [])
|
|
|
|
useResizeObserver(measureOverflow, ref)
|
|
|
|
useEffect(() => {
|
|
measureOverflow()
|
|
}, [children, measureOverflow])
|
|
|
|
const maskStyle: CSSProperties = overflowing
|
|
? {
|
|
maskImage: `linear-gradient(to right, black calc(100% - ${fadeWidth}), transparent)`,
|
|
WebkitMaskImage: `linear-gradient(to right, black calc(100% - ${fadeWidth}), transparent)`,
|
|
...style
|
|
}
|
|
: (style ?? {})
|
|
|
|
return (
|
|
<span
|
|
{...rest}
|
|
className={cn('block min-w-0 max-w-full overflow-hidden whitespace-nowrap', className)}
|
|
ref={ref}
|
|
style={maskStyle}
|
|
>
|
|
{children}
|
|
</span>
|
|
)
|
|
}
|