mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Grafana-UI: Create fast path in Text component (#76167)
Text component fast path Truncated text an isolated component
This commit is contained in:
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
64
packages/grafana-ui/src/components/Text/TruncatedText.tsx
Normal file
64
packages/grafana-ui/src/components/Text/TruncatedText.tsx
Normal 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';
|
||||||
Reference in New Issue
Block a user