diff --git a/packages/grafana-ui/src/components/Text/Text.mdx b/packages/grafana-ui/src/components/Text/Text.mdx
index 69996bc3649..a23de7a1472 100644
--- a/packages/grafana-ui/src/components/Text/Text.mdx
+++ b/packages/grafana-ui/src/components/Text/Text.mdx
@@ -105,14 +105,14 @@ The Text component can be truncated. However, the Text component element rendere
{'And Forrest Gump said: '}
- {"Life is like a box of chocolates. You never know what you're gonna get."}
+ {'Life is like a box of chocolates. You never know what you are gonna get.'}
```jsx
And Forrest Gump said:
- Life is like a box of chocolates. You never know what you're gonna get.
+ {'Life is like a box of chocolates. You never know what you are gonna get.'}
```
diff --git a/packages/grafana-ui/src/components/Text/Text.story.internal.tsx b/packages/grafana-ui/src/components/Text/Text.story.internal.tsx
deleted file mode 100644
index e3ea5b5eea9..00000000000
--- a/packages/grafana-ui/src/components/Text/Text.story.internal.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { Meta, StoryFn } from '@storybook/react';
-import React from 'react';
-
-import { StoryExample } from '../../utils/storybook/StoryExample';
-import { VerticalGroup } from '../Layout/Layout';
-
-import { Text } from './Text';
-import mdx from './Text.mdx';
-
-const meta: Meta = {
- title: 'General/Text',
- component: Text,
- parameters: {
- docs: {
- page: mdx,
- },
- },
- argTypes: {
- variant: { control: 'select', options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body', 'bodySmall', undefined] },
- weight: {
- control: 'select',
- options: ['bold', 'medium', 'light', 'regular', undefined],
- },
- color: {
- control: 'select',
- options: [
- 'error',
- 'success',
- 'warning',
- 'info',
- 'primary',
- 'secondary',
- 'disabled',
- 'link',
- 'maxContrast',
- undefined,
- ],
- },
- truncate: { control: 'boolean' },
- italic: { control: 'boolean' },
- textAlignment: {
- control: 'select',
- options: ['inherit', 'initial', 'left', 'right', 'center', 'justify', undefined],
- },
- },
- args: {
- element: 'h1',
- variant: undefined,
- weight: 'light',
- textAlignment: 'left',
- truncate: false,
- italic: false,
- color: 'primary',
- children: `This is an example of a Text component`,
- },
-};
-
-export const Example: StoryFn = (args) => {
- return (
-
-
-
- This is a header
-
-
- This is a paragraph that contains
-
- {' '}
- a span element with different color and style{' '}
-
- but is comprised within the same block text
-
-
-
-
- This is a paragraph that contains
-
- {' '}
- a span element{' '}
-
- but has truncate set to true
-
-
-
- );
-};
-Example.parameters = {
- controls: {
- exclude: ['element', 'variant', 'weight', 'textAlignment', 'truncate', 'italic', 'color', 'children'],
- },
-};
-
-export const Basic: StoryFn = (args) => {
- return (
-
-
- {args.children}
-
-
- );
-};
-
-export default meta;
diff --git a/packages/grafana-ui/src/components/Text/Text.tsx b/packages/grafana-ui/src/components/Text/Text.tsx
index ffcc98fc090..dda343c2222 100644
--- a/packages/grafana-ui/src/components/Text/Text.tsx
+++ b/packages/grafana-ui/src/components/Text/Text.tsx
@@ -1,9 +1,20 @@
import { css } from '@emotion/css';
-import React, { createElement, CSSProperties, useCallback } from 'react';
+import React, {
+ createElement,
+ CSSProperties,
+ useCallback,
+ useEffect,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import ReactDomServer from 'react-dom/server';
import { GrafanaTheme2, ThemeTypographyVariantTypes } from '@grafana/data';
import { useStyles2 } from '../../themes';
+import { Tooltip } from '../Tooltip/Tooltip';
import { customWeight, customColor, customVariant } from './utils';
@@ -22,7 +33,7 @@ export interface TextProps {
italic?: boolean;
/** Whether to align the text to left, center or right */
textAlignment?: CSSProperties['textAlign'];
- children: React.ReactNode;
+ children: NonNullable;
}
export const Text = React.forwardRef(
@@ -33,15 +44,68 @@ export const Text = React.forwardRef(
[color, textAlignment, truncate, italic, weight, variant, element]
)
);
+ const [isOverflowing, setIsOverflowing] = useState(false);
+ const internalRef = useRef(null);
- return createElement(
+ // wire up the forwarded ref to the internal ref
+ useImperativeHandle(ref, () => internalRef.current);
+
+ const childElement = createElement(
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
);
+
+ 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}
+
+ );
+ } else {
+ return childElement;
+ }
}
);
diff --git a/public/app/features/scenes/dashboard/DashboardScenePage.test.tsx b/public/app/features/scenes/dashboard/DashboardScenePage.test.tsx
index b73674bc8be..f1bb1af432a 100644
--- a/public/app/features/scenes/dashboard/DashboardScenePage.test.tsx
+++ b/public/app/features/scenes/dashboard/DashboardScenePage.test.tsx
@@ -10,7 +10,7 @@ import { config, locationService, setPluginImportUtils } from '@grafana/runtime'
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { DashboardScenePage, Props } from './DashboardScenePage';
-import { mockResizeObserver, setupLoadDashboardMock } from './test-utils';
+import { setupLoadDashboardMock } from './test-utils';
function setup() {
const context = getGrafanaContextMock();
@@ -79,8 +79,6 @@ setPluginImportUtils({
getPanelPluginFromCache: (id: string) => undefined,
});
-mockResizeObserver();
-
describe('DashboardScenePage', () => {
beforeEach(() => {
locationService.push('/');
diff --git a/public/test/jest-setup.ts b/public/test/jest-setup.ts
index cfcaf1e40bf..b9d85c2bbcf 100644
--- a/public/test/jest-setup.ts
+++ b/public/test/jest-setup.ts
@@ -3,9 +3,11 @@
import './global-jquery-shim';
import angular from 'angular';
+import { TextEncoder, TextDecoder } from 'util';
import { EventBusSrv } from '@grafana/data';
import { GrafanaBootConfig } from '@grafana/runtime';
+
import 'blob-polyfill';
import 'mutationobserver-shim';
import './mocks/workers';
@@ -62,6 +64,9 @@ const mockIntersectionObserver = jest
}));
global.IntersectionObserver = mockIntersectionObserver;
+global.TextEncoder = TextEncoder;
+global.TextDecoder = TextDecoder;
+
jest.mock('../app/core/core', () => ({
...jest.requireActual('../app/core/core'),
appEvents: testAppEvents,
@@ -96,6 +101,7 @@ global.ResizeObserver = class ResizeObserver {
left: 100,
right: 0,
},
+ target: {},
} as ResizeObserverEntry,
],
this