diff --git a/packages/grafana-ui/src/components/Text/Text.tsx b/packages/grafana-ui/src/components/Text/Text.tsx index eb036f186e7..29f3a46c113 100644 --- a/packages/grafana-ui/src/components/Text/Text.tsx +++ b/packages/grafana-ui/src/components/Text/Text.tsx @@ -1,12 +1,11 @@ import { css } from '@emotion/css'; -import React, { createElement, CSSProperties, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; -import ReactDomServer from 'react-dom/server'; +import React, { createElement, CSSProperties } from 'react'; import { GrafanaTheme2, ThemeTypographyVariantTypes } from '@grafana/data'; import { useStyles2 } from '../../themes'; -import { Tooltip } from '../Tooltip/Tooltip'; +import { TruncatedText } from './TruncatedText'; import { customWeight, customColor, customVariant } from './utils'; export interface TextProps extends Omit, 'className' | 'style'> { @@ -30,70 +29,35 @@ export interface TextProps extends Omit, 'clas export const Text = React.forwardRef( ({ element = 'span', variant, weight, color, truncate, italic, textAlignment, children, ...restProps }, ref) => { const styles = useStyles2(getTextStyles, element, variant, color, weight, truncate, italic, textAlignment); - const [isOverflowing, setIsOverflowing] = useState(false); - const internalRef = useRef(null); - // wire up the forwarded ref to the internal ref - useImperativeHandle(ref, () => internalRef.current); - - const childElement = createElement( - element, - { - ...restProps, - style: undefined, // remove style prop to avoid overriding the styles - className: styles, - // when overflowing, the internalRef is passed to the tooltip which forwards it on to the child element - ref: isOverflowing ? undefined : internalRef, - }, - children - ); - - const resizeObserver = useMemo( - () => - new ResizeObserver((entries) => { - for (const entry of entries) { - if (entry.target.clientWidth && entry.target.scrollWidth) { - if (entry.target.scrollWidth > entry.target.clientWidth) { - setIsOverflowing(true); - } - if (entry.target.scrollWidth <= entry.target.clientWidth) { - setIsOverflowing(false); - } - } - } - }), - [] - ); - - useEffect(() => { - const { current } = internalRef; - if (current && truncate) { - resizeObserver.observe(current); - } - return () => { - resizeObserver.disconnect(); - }; - }, [isOverflowing, resizeObserver, truncate]); - - const getTooltipText = (children: NonNullable) => { - if (typeof children === 'string') { - return children; - } - const html = ReactDomServer.renderToStaticMarkup(<>{children}); - const getRidOfTags = html.replace(/(<([^>]+)>)/gi, ''); - return getRidOfTags; - }; - // A 'span' is an inline element therefore it can't be truncated - // and it should be wrapped in a parent element that is the one that will show the tooltip - if (truncate && isOverflowing && element !== 'span') { - return ( - - {childElement} - + const childElement = (ref: React.ForwardedRef | undefined) => { + return createElement( + element, + { + ...restProps, + style: undefined, // Remove the style prop to avoid overriding the styles + className: styles, + // When overflowing, the internalRef is passed to the tooltip, which forwards it to the child element + ref, + }, + children ); - } else { - return childElement; + }; + + // A 'span' is an inline element, so it can't be truncated + // and it should be wrapped in a parent element that will show the tooltip + if (!truncate || element === 'span') { + return childElement(undefined); } + + return ( + + ); } ); diff --git a/packages/grafana-ui/src/components/Text/TruncatedText.tsx b/packages/grafana-ui/src/components/Text/TruncatedText.tsx new file mode 100644 index 00000000000..e74cbd36bb7 --- /dev/null +++ b/packages/grafana-ui/src/components/Text/TruncatedText.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import ReactDOMServer from 'react-dom/server'; + +import { Tooltip } from '../Tooltip/Tooltip'; + +interface TruncatedTextProps { + childElement: (ref: React.ForwardedRef | undefined) => React.ReactElement; + children: NonNullable; +} + +export const TruncatedText = React.forwardRef(({ childElement, children }, ref) => { + const [isOverflowing, setIsOverflowing] = useState(false); + const internalRef = useRef(null); + + // Wire up the forwarded ref to the internal ref + useImperativeHandle(ref, () => internalRef.current); + + const resizeObserver = useMemo( + () => + new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.target.clientWidth && entry.target.scrollWidth) { + if (entry.target.scrollWidth > entry.target.clientWidth) { + setIsOverflowing(true); + } + if (entry.target.scrollWidth <= entry.target.clientWidth) { + setIsOverflowing(false); + } + } + } + }), + [] + ); + + useEffect(() => { + const { current } = internalRef; + if (current) { + resizeObserver.observe(current); + } + return () => { + resizeObserver.disconnect(); + }; + }, [setIsOverflowing, resizeObserver]); + + const getTooltipText = (children: NonNullable) => { + if (typeof children === 'string') { + return children; + } + const html = ReactDOMServer.renderToStaticMarkup(<>{children}); + return html.replace(/(<([^>]+)>)/gi, ''); + }; + + if (isOverflowing) { + return ( + + {childElement(undefined)} + + ); + } else { + return childElement(internalRef); + } +}); + +TruncatedText.displayName = 'TruncatedText';