import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import clsx from 'clsx'; import debounce from 'lodash/debounce'; import MuiTooltip from '@mui/material/Tooltip'; import { styled } from '@mui/material/styles'; import type { Theme } from '../@styles/theme-provider'; import { isElementOverflowing, useResizeObserverEffect } from '../../utils'; import type { TextWithTooltipProps } from './types'; /** * The minimum length of a text list for the overflowLabel to be displayed. */ const minTextListLength = 1; const isElementVisible = (element: HTMLElement, container: HTMLElement) => { const containerRect = container.getBoundingClientRect(); const elementRect = element.getBoundingClientRect(); return ( elementRect.bottom <= containerRect.bottom && (elementRect.top <= containerRect.top ? containerRect.top - elementRect.top <= elementRect.height : elementRect.bottom - containerRect.bottom <= elementRect.height) ); }; type StyledTextProps = { numOfLines?: string; }; const StyledText = styled('span', { shouldForwardProp: prop => prop !== 'numOfLines' })(({ numOfLines, theme }: StyledTextProps & { theme: Theme }) => ({ ...theme.typography.subtitle1, position: 'relative', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'normal', wordWrap: 'break-word', display: '-webkit-box', WebkitBoxOrient: 'vertical', WebkitLineClamp: numOfLines })); const StyledOverflowLabel = styled('span')(({ theme }: { theme: Theme }) => ({ position: 'absolute', right: 0, bottom: 0, float: 'right', marginLeft: theme.spacing(0.5), color: theme.palette.secondary[500] })); const StyledTextContainer = styled('span')({ '&.breakAll': { wordBreak: 'break-all' }, '&.breakWord': { wordBreak: 'break-word' } }); const StyledTextListItem = styled('span')({ display: 'inline', '&:not(:last-of-type):after': { content: 'attr(data-separator)', whitespace: 'break-spaces' } }); const TextWithTooltip = (props: TextWithTooltipProps) => { const { text, textSeparator = '', title, numOfLines = 2, renderDelay = 0, resizeDebounceTime = 40, showLimitedItemsOnly, classes, textClassName, disableHoverListener, ...otherProps } = props; const textListLength = Array.isArray(text) ? text.length : -1; const ref = useRef(null); // Delayed rendering const [renderReady, setRenderReady] = useState(false); useEffect(() => { let timeoutId: ReturnType; if (typeof renderDelay !== 'undefined' && renderDelay >= 0) timeoutId = setTimeout(() => setRenderReady(true), renderDelay); else setRenderReady(true); return () => timeoutId && clearTimeout(timeoutId); }, [renderDelay]); const compareSize = useCallback(() => { const textElement = ref?.current; if (renderReady && textElement) { setHover(isElementOverflowing(textElement)); // enable limit items count if (textListLength > minTextListLength) { const textListItems = Array.from( textElement.querySelectorAll('.textListItem') ) as HTMLElement[]; const visibleItems = getVisibleElements(textListItems, textElement); setLimitItemsCount(textListLength - visibleItems.length); } const overflowLabelElement = textElement.parentElement?.querySelector('.overflowLabel'); const width = overflowLabelElement?.clientWidth; setOverflowLabelSize(width ? width + 4 : 0); } function getVisibleElements(elements: HTMLElement[], container: HTMLElement) { return elements.filter(el => isElementVisible(el, container)); } }, [renderReady, textListLength]); const debouncedCompareSize = useMemo( () => typeof resizeDebounceTime !== 'undefined' && resizeDebounceTime >= 0 ? debounce(compareSize, resizeDebounceTime) : () => compareSize(), [compareSize, resizeDebounceTime] ); useResizeObserverEffect(debouncedCompareSize, ref?.current); // Define state and function to update the value const [hoverStatus, setHover] = useState(false); const [limitItemsCount, setLimitItemsCount] = useState(0); const [overflowLabelSize, setOverflowLabelSize] = useState(0); // Tooltip title content let tooltipTitle = title || text; if (!title && textListLength > -1) { tooltipTitle = (text as string[]) .slice(showLimitedItemsOnly ? -limitItemsCount : 0) .join(textSeparator); } const styledText = ( 0 && classes?.withOverflowLabel )} style={{ paddingRight: limitItemsCount > 0 && overflowLabelSize ? overflowLabelSize : '' }} > {textListLength > minTextListLength && ( {limitItemsCount > 0 ? `+${limitItemsCount}` : ''} )} {Array.isArray(text) ? text?.map(item => ( {item} )) : text} ); return renderReady ? ( minTextListLength } disableInteractive title={tooltipTitle} {...otherProps} > {styledText} ) : ( styledText ); }; const m = memo(TextWithTooltip); export { m as TextWithTooltip };