Grafana UI: Text component show all text on hover when it is truncated (#68578)

This commit is contained in:
Laura Fernández
2023-07-31 18:04:48 +02:00
committed by GitHub
parent 3353b1a8aa
commit b632eb33b9
5 changed files with 77 additions and 118 deletions

View File

@@ -105,14 +105,14 @@ The Text component can be truncated. However, the Text component element rendere
<Preview>
<Text color="primary" element="p" truncate>
{'And Forrest Gump said: '}
<Text italic>{"Life is like a box of chocolates. You never know what you're gonna get."}</Text>
<Text italic>{'Life is like a box of chocolates. You never know what you are gonna get.'}</Text>
</Text>
</Preview>
```jsx
<Text color="primary" element="p" truncate>
And Forrest Gump said:
<Text italic>Life is like a box of chocolates. You never know what you're gonna get.</Text>
<Text italic>{'Life is like a box of chocolates. You never know what you are gonna get.'}</Text>
</Text>
```

View File

@@ -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 (
<VerticalGroup>
<StoryExample name="Header, paragraph and span">
<Text {...args} element="h1">
This is a header
</Text>
<Text {...args} element="p">
This is a paragraph that contains
<Text color="success" italic>
{' '}
a span element with different color and style{' '}
</Text>
but is comprised within the same block text
</Text>
</StoryExample>
<StoryExample name="Paragraph with truncate set to true and wrapping up a span element">
<Text {...args} element="p" truncate>
This is a paragraph that contains
<Text color="warning" italic>
{' '}
a span element{' '}
</Text>
but has truncate set to true
</Text>
</StoryExample>
</VerticalGroup>
);
};
Example.parameters = {
controls: {
exclude: ['element', 'variant', 'weight', 'textAlignment', 'truncate', 'italic', 'color', 'children'],
},
};
export const Basic: StoryFn = (args) => {
return (
<div style={{ width: '300px' }}>
<Text
element={args.element}
variant={args.variant}
weight={args.weight}
textAlignment={args.textAlignment}
{...args}
>
{args.children}
</Text>
</div>
);
};
export default meta;

View File

@@ -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<React.ReactNode>;
}
export const Text = React.forwardRef<HTMLElement, TextProps>(
@@ -33,15 +44,68 @@ export const Text = React.forwardRef<HTMLElement, TextProps>(
[color, textAlignment, truncate, italic, weight, variant, element]
)
);
const [isOverflowing, setIsOverflowing] = useState(false);
const internalRef = useRef<HTMLElement>(null);
return createElement(
// wire up the forwarded ref to the internal ref
useImperativeHandle<HTMLElement | null, HTMLElement | null>(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<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;
}
}
);

View File

@@ -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('/');

View File

@@ -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