Tom Butcher d52a8eb570 Refactor ElipsisText component to integrate Tooltip for truncated text
- Updated the ElipsisText component to conditionally wrap truncated text in a Tooltip, enhancing user experience by providing additional context on hover.
- Simplified the rendering logic by consolidating the content structure, improving code readability and maintainability.
2026-08-20 21:03:32 +01:00

113 lines
3.1 KiB
JavaScript

import { useLayoutEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import { Typography } from 'antd'
import Tooltip from './Tooltip'
const { Text } = Typography
const childrenToString = (children) => {
if (children == null || typeof children === 'boolean') return ''
if (typeof children === 'string' || typeof children === 'number') {
return String(children)
}
if (Array.isArray(children)) {
return children.map(childrenToString).join('')
}
if (typeof children === 'object' && children.props) {
return childrenToString(children.props.children)
}
return ''
}
const splitMiddle = (text) => {
const mid = Math.ceil(text.length / 2)
return {
start: text.slice(0, mid),
end: text.slice(mid)
}
}
const ElipsisText = ({ children, style, className, title, code, ...rest }) => {
const text = childrenToString(children)
const { start, end } = splitMiddle(text)
const containerRef = useRef(null)
const measureRef = useRef(null)
const truncatedRef = useRef(false)
const [truncated, setTruncated] = useState(false)
useLayoutEffect(() => {
const container = containerRef.current
const measure = measureRef.current
if (!container || !measure) return
const update = () => {
const available = container.getBoundingClientRect().width
if (available < 1) return
const textWidth = measure.getBoundingClientRect().width
const next = Boolean(end) && textWidth > available
if (truncatedRef.current === next) return
truncatedRef.current = next
setTruncated(next)
}
update()
const frame = requestAnimationFrame(update)
const resizeObserver = new ResizeObserver(update)
resizeObserver.observe(container)
const intersectionObserver = new IntersectionObserver(update)
intersectionObserver.observe(container)
return () => {
cancelAnimationFrame(frame)
resizeObserver.disconnect()
intersectionObserver.disconnect()
}
}, [text, end])
const showTruncated = truncated && Boolean(end)
const RootTag = code ? 'code' : 'span'
const content = (
<span
className={showTruncated ? 'elipsis-text is-truncated' : 'elipsis-text'}
ref={containerRef}
>
<RootTag className='elipsis-text-measure' ref={measureRef}>
{text}
</RootTag>
<RootTag className='elipsis-text-full'>{text}</RootTag>
<RootTag className='elipsis-text-truncated'>
<span className='elipsis-text-start'>{start}</span>
<span className='elipsis-text-dots'>...</span>
<span className='elipsis-text-end'>
<span className='elipsis-text-end-inner'>{end}</span>
</span>
</RootTag>
</span>
)
return (
<Text
{...rest}
className={['elipsis-text-wrapper', className].filter(Boolean).join(' ')}
style={style}
>
{truncated ? <Tooltip title={title ?? text}>{content}</Tooltip> : content}
</Text>
)
}
ElipsisText.propTypes = {
children: PropTypes.node,
style: PropTypes.object,
className: PropTypes.string,
title: PropTypes.string,
code: PropTypes.bool
}
export default ElipsisText