mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Grafana UI: Text component show all text on hover when it is truncated (#68578)
This commit is contained in:
@@ -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>
|
||||
```
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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('/');
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user