|
| 1 | +import React, { useEffect, useRef, useState, useLayoutEffect } from 'react' |
| 2 | +import PortalTooltip from '../PortalTooltip' |
| 3 | +import styles from './index.module.less' |
| 4 | + |
| 5 | +// SSR-safe useLayoutEffect |
| 6 | +const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect |
| 7 | + |
| 8 | +/** |
| 9 | + * OverflowText |
| 10 | + * |
| 11 | + * Renders text that shows a tooltip when it overflows. |
| 12 | + * |
| 13 | + * Supports: |
| 14 | + * - Single line truncation (default) |
| 15 | + * - Multi-line truncation (via props.lines) |
| 16 | + * - Auto-detection of overflow via ResizeObserver |
| 17 | + * - alwaysShow prop to force display |
| 18 | + * - onOverflowChange callback |
| 19 | + */ |
| 20 | +const OverflowText = ({ |
| 21 | + text = '', |
| 22 | + minWidth = 0, |
| 23 | + maxWidth, |
| 24 | + className = '', |
| 25 | + style = {}, |
| 26 | + tooltipProps = {}, |
| 27 | + alwaysShow = false, |
| 28 | + lines = 0, |
| 29 | + onOverflowChange, |
| 30 | +}) => { |
| 31 | + const elRef = useRef(null) |
| 32 | + const [isOverflow, setIsOverflow] = useState(false) |
| 33 | + |
| 34 | + const check = () => { |
| 35 | + const el = elRef.current |
| 36 | + if (!el) return |
| 37 | + |
| 38 | + const isMulti = lines && Number(lines) > 0 |
| 39 | + |
| 40 | + // For single line: use scrollWidth > clientWidth |
| 41 | + // For multi line: use scrollHeight > clientHeight |
| 42 | + let overflow = false |
| 43 | + |
| 44 | + if (isMulti) { |
| 45 | + overflow = el.scrollHeight > el.clientHeight |
| 46 | + } else { |
| 47 | + // Single line check |
| 48 | + overflow = el.scrollWidth > el.clientWidth |
| 49 | + } |
| 50 | + |
| 51 | + setIsOverflow((prev) => { |
| 52 | + if (prev !== overflow) { |
| 53 | + if (typeof onOverflowChange === 'function') onOverflowChange(overflow) |
| 54 | + return overflow |
| 55 | + } |
| 56 | + return prev |
| 57 | + }) |
| 58 | + } |
| 59 | + |
| 60 | + // Check on mount and updates |
| 61 | + useIsomorphicLayoutEffect(() => { |
| 62 | + check() |
| 63 | + }, [text, lines, maxWidth, style, className]) |
| 64 | + |
| 65 | + useEffect(() => { |
| 66 | + const el = elRef.current |
| 67 | + if (!el || typeof window === 'undefined') return |
| 68 | + |
| 69 | + // Re-check when fonts load (crucial for custom fonts delay) |
| 70 | + if (document.fonts) { |
| 71 | + document.fonts.ready.then(check) |
| 72 | + } |
| 73 | + |
| 74 | + // ResizeObserver to detect container/element size changes |
| 75 | + let ro |
| 76 | + if ('ResizeObserver' in window) { |
| 77 | + ro = new ResizeObserver(check) |
| 78 | + ro.observe(el) |
| 79 | + } |
| 80 | + |
| 81 | + // Fallback global resize |
| 82 | + window.addEventListener('resize', check) |
| 83 | + |
| 84 | + return () => { |
| 85 | + if (ro) ro.disconnect() |
| 86 | + window.removeEventListener('resize', check) |
| 87 | + } |
| 88 | + }, [text, lines]) |
| 89 | + |
| 90 | + // Construct styles |
| 91 | + const effectiveMaxWidth = maxWidth |
| 92 | + |
| 93 | + const spanStyle = { |
| 94 | + // Apply minWidth if provided |
| 95 | + ...(minWidth ? { minWidth: typeof minWidth === 'number' ? `${minWidth}px` : minWidth } : {}), |
| 96 | + // Apply maxWidth if provided (this is the CSS max-width) |
| 97 | + ...(effectiveMaxWidth |
| 98 | + ? { maxWidth: typeof effectiveMaxWidth === 'number' ? `${effectiveMaxWidth}px` : effectiveMaxWidth } |
| 99 | + : {}), |
| 100 | + // Multi-line specific styles |
| 101 | + ...(lines && Number(lines) > 0 |
| 102 | + ? { WebkitLineClamp: lines, display: '-webkit-box', WebkitBoxOrient: 'vertical', whiteSpace: 'normal' } |
| 103 | + : {}), |
| 104 | + ...style, |
| 105 | + } |
| 106 | + |
| 107 | + const classes = `${styles.ellipsis} ${lines && Number(lines) > 0 ? styles.multiLine : ''} ${className}`.trim() |
| 108 | + |
| 109 | + const child = ( |
| 110 | + <span ref={elRef} className={classes} style={spanStyle}> |
| 111 | + {text} |
| 112 | + </span> |
| 113 | + ) |
| 114 | + |
| 115 | + return alwaysShow || isOverflow ? ( |
| 116 | + <PortalTooltip title={typeof text === 'string' ? text : undefined} {...tooltipProps}> |
| 117 | + {child} |
| 118 | + </PortalTooltip> |
| 119 | + ) : ( |
| 120 | + child |
| 121 | + ) |
| 122 | +} |
| 123 | + |
| 124 | +export default OverflowText |
0 commit comments