Grafana-UI: Create fast path in Text component (#76167)

Text component fast path

Truncated text an isolated component
This commit is contained in:
Krishna Dhakal
2023-11-02 17:08:59 +05:45
committed by GitHub
parent 82a7e1229a
commit 774a8a889a
2 changed files with 92 additions and 64 deletions

View File

@@ -1,12 +1,11 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { createElement, CSSProperties, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import React, { createElement, CSSProperties } from 'react';
import ReactDomServer from 'react-dom/server';
import { GrafanaTheme2, ThemeTypographyVariantTypes } from '@grafana/data'; import { GrafanaTheme2, ThemeTypographyVariantTypes } from '@grafana/data';
import { useStyles2 } from '../../themes'; import { useStyles2 } from '../../themes';
import { Tooltip } from '../Tooltip/Tooltip';
import { TruncatedText } from './TruncatedText';
import { customWeight, customColor, customVariant } from './utils'; import { customWeight, customColor, customVariant } from './utils';
export interface TextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> { export interface TextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> {
@@ -30,70 +29,35 @@ export interface TextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'clas
export const Text = React.forwardRef<HTMLElement, TextProps>( export const Text = React.forwardRef<HTMLElement, TextProps>(
({ element = 'span', variant, weight, color, truncate, italic, textAlignment, children, ...restProps }, ref) => { ({ element = 'span', variant, weight, color, truncate, italic, textAlignment, children, ...restProps }, ref) => {
const styles = useStyles2(getTextStyles, element, variant, color, weight, truncate, italic, textAlignment); const styles = useStyles2(getTextStyles, element, variant, color, weight, truncate, italic, textAlignment);
const [isOverflowing, setIsOverflowing] = useState(false);
const internalRef = useRef<HTMLElement>(null);
// wire up the forwarded ref to the internal ref const childElement = (ref: React.ForwardedRef<HTMLElement> | undefined) => {
useImperativeHandle<HTMLElement | null, HTMLElement | null>(ref, () => internalRef.current); return createElement(
element,
const childElement = createElement( {
element, ...restProps,
{ style: undefined, // Remove the style prop to avoid overriding the styles
...restProps, className: styles,
style: undefined, // remove style prop to avoid overriding the styles // When overflowing, the internalRef is passed to the tooltip, which forwards it to the child element
className: styles, ref,
// when overflowing, the internalRef is passed to the tooltip which forwards it on to the child element },
ref: isOverflowing ? undefined : internalRef, children
},
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<React.ReactNode>) => {
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 (
<Tooltip ref={internalRef} content={getTooltipText(children)}>
{childElement}
</Tooltip>
); );
} 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 (
<TruncatedText
childElement={childElement}
// eslint-disable-next-line react/no-children-prop
children={children}
ref={ref}
/>
);
} }
); );

View File

@@ -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<HTMLElement> | undefined) => React.ReactElement;
children: NonNullable<React.ReactNode>;
}
export const TruncatedText = React.forwardRef<HTMLElement, TruncatedTextProps>(({ childElement, children }, ref) => {
const [isOverflowing, setIsOverflowing] = useState(false);
const internalRef = useRef<HTMLElement>(null);
// Wire up the forwarded ref to the internal ref
useImperativeHandle<HTMLElement | null, HTMLElement | null>(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<React.ReactNode>) => {
if (typeof children === 'string') {
return children;
}
const html = ReactDOMServer.renderToStaticMarkup(<>{children}</>);
return html.replace(/(<([^>]+)>)/gi, '');
};
if (isOverflowing) {
return (
<Tooltip ref={internalRef} content={getTooltipText(children)}>
{childElement(undefined)}
</Tooltip>
);
} else {
return childElement(internalRef);
}
});
TruncatedText.displayName = 'TruncatedText';