From b632eb33b9a28214f4691f60ed3a797028219f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Mon, 31 Jul 2023 18:04:48 +0200 Subject: [PATCH] Grafana UI: Text component show all text on hover when it is truncated (#68578) --- .../grafana-ui/src/components/Text/Text.mdx | 4 +- .../components/Text/Text.story.internal.tsx | 109 ------------------ .../grafana-ui/src/components/Text/Text.tsx | 72 +++++++++++- .../dashboard/DashboardScenePage.test.tsx | 4 +- public/test/jest-setup.ts | 6 + 5 files changed, 77 insertions(+), 118 deletions(-) delete mode 100644 packages/grafana-ui/src/components/Text/Text.story.internal.tsx 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