* Truncated text that reveals its hidden lines on hover: the head shows the * first `clampLines` lines, and when hovered (and actually overflowing) the * remaining lines mount below and blur in as one continuous block. Text that * fits within the clamp is never truncated and the card never grows
({
text,
expanded,
clampLines,
lineHeightPx,
leadingIcon,
className,
reduceMotion,
}: RevealTextProps)
| 197 | * fits within the clamp is never truncated and the card never grows for it. |
| 198 | */ |
| 199 | function RevealText({ |
| 200 | text, |
| 201 | expanded, |
| 202 | clampLines, |
| 203 | lineHeightPx, |
| 204 | leadingIcon, |
| 205 | className, |
| 206 | reduceMotion, |
| 207 | }: RevealTextProps) { |
| 208 | const headRef = useRef<HTMLDivElement>(null) |
| 209 | const [truncated, setTruncated] = useState(false) |
| 210 | const clampHeight = clampLines * lineHeightPx |
| 211 | |
| 212 | useLayoutEffect(() => { |
| 213 | const el = headRef.current |
| 214 | if (!el) return |
| 215 | const check = () => setTruncated(el.scrollHeight - el.clientHeight > 1) |
| 216 | check() |
| 217 | const observer = new ResizeObserver(check) |
| 218 | observer.observe(el) |
| 219 | return () => observer.disconnect() |
| 220 | }, [text]) |
| 221 | |
| 222 | const open = expanded && truncated |
| 223 | const fadeStart = Math.max(0, clampHeight - lineHeightPx * 1.5) |
| 224 | const collapsedMask = `linear-gradient(to bottom, #000 ${fadeStart}px, transparent ${clampHeight}px)` |
| 225 | |
| 226 | return ( |
| 227 | <div> |
| 228 | <div |
| 229 | ref={headRef} |
| 230 | className={cn('overflow-hidden', className)} |
| 231 | style={{ |
| 232 | maxHeight: clampHeight, |
| 233 | maskImage: truncated && !open ? collapsedMask : undefined, |
| 234 | WebkitMaskImage: truncated && !open ? collapsedMask : undefined, |
| 235 | }} |
| 236 | > |
| 237 | {leadingIcon} |
| 238 | {text} |
| 239 | </div> |
| 240 | <AnimatePresence initial={false}> |
| 241 | {open ? ( |
| 242 | <motion.div |
| 243 | className='overflow-hidden' |
| 244 | aria-hidden |
| 245 | initial={reduceMotion ? false : { height: 0 }} |
| 246 | animate={{ height: 'auto' }} |
| 247 | exit={{ height: 0 }} |
| 248 | transition={ |
| 249 | reduceMotion ? { duration: 0 } : { duration: RESIZE_DURATION, ease: TOAST_EASE } |
| 250 | } |
| 251 | > |
| 252 | <motion.div |
| 253 | className={className} |
| 254 | style={{ marginTop: -clampHeight }} |
| 255 | initial={reduceMotion ? false : { opacity: 0, filter: 'blur(5px)' }} |
| 256 | animate={{ opacity: 1, filter: 'blur(0px)' }} |
nothing calls this directly
no test coverage detected