portainer/app/react/components/ExpandableMessageByLines.tsx

77 lines
2.0 KiB
TypeScript

import { useRef, useState, useEffect, useCallback } from 'react';
import { Button } from '@@/buttons';
// use enum so that the tailwind classes aren't interpolated
type MaxLines = 2 | 5 | 10 | 20 | 50;
const lineClampClasses: Record<MaxLines, string> = {
2: 'line-clamp-[2]',
5: 'line-clamp-[5]',
10: 'line-clamp-[10]',
20: 'line-clamp-[20]',
50: 'line-clamp-[50]',
};
interface LineBasedProps {
children: string;
maxLines?: MaxLines;
}
export function ExpandableMessageByLines({
children,
maxLines = 10,
}: LineBasedProps) {
const [isExpanded, setIsExpanded] = useState(false);
const [isTruncated, setIsTruncated] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const checkTruncation = useCallback(() => {
const el = contentRef.current;
if (el) {
setIsTruncated(el.scrollHeight > el.clientHeight);
}
}, []);
useEffect(() => {
checkTruncation();
// Use requestAnimationFrame for better performance
let rafId: number;
function handleResize() {
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(checkTruncation);
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
if (rafId) cancelAnimationFrame(rafId);
};
}, [children, maxLines, checkTruncation, isExpanded]);
return (
<div className="flex flex-col items-start">
<div
ref={contentRef}
className={`whitespace-pre-wrap break-words overflow-hidden ${
isExpanded ? '' : lineClampClasses[maxLines]
}`}
>
{children}
</div>
{(isTruncated || isExpanded) && (
<Button
color="link"
size="xsmall"
onClick={() => setIsExpanded(!isExpanded)}
className="!ml-0 !p-0 mt-1"
data-cy="expandable-message-lines-button"
>
{isExpanded ? 'Show less' : 'Show more'}
</Button>
)}
</div>
);
}