Grafana UI: Create Text component (#66932)

This commit is contained in:
Laura Fernández 2023-04-28 15:31:40 +02:00 committed by GitHub
parent 692bb9ed1a
commit fe59b65f9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 370 additions and 63 deletions

View File

@ -6170,9 +6170,6 @@ exports[`no undocumented stories`] = {
"packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.story.tsx:5381": [
[0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"]
],
"packages/grafana-ui/src/components/Typography/Typography.story.tsx:5381": [
[0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"]
],
"packages/grafana-ui/src/components/VizLayout/VizLayout.story.tsx:5381": [
[0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"]
],

View File

@ -5,7 +5,7 @@
import { ThemeColors } from './createColors';
/** @beta */
export interface ThemeTypography {
export interface ThemeTypography extends ThemeTypographyVariantTypes {
fontFamily: string;
fontFamilyMonospace: string;
fontSize: number;
@ -17,16 +17,6 @@ export interface ThemeTypography {
// The font-size on the html element.
htmlFontSize?: number;
h1: ThemeTypographyVariant;
h2: ThemeTypographyVariant;
h3: ThemeTypographyVariant;
h4: ThemeTypographyVariant;
h5: ThemeTypographyVariant;
h6: ThemeTypographyVariant;
body: ThemeTypographyVariant;
bodySmall: ThemeTypographyVariant;
/**
* @deprecated
* from legacy old theme
@ -152,3 +142,14 @@ export function createTypography(colors: ThemeColors, typographyInput: ThemeTypo
function round(value: number) {
return Math.round(value * 1e5) / 1e5;
}
export interface ThemeTypographyVariantTypes {
h1: ThemeTypographyVariant;
h2: ThemeTypographyVariant;
h3: ThemeTypographyVariant;
h4: ThemeTypographyVariant;
h5: ThemeTypographyVariant;
h6: ThemeTypographyVariant;
body: ThemeTypographyVariant;
bodySmall: ThemeTypographyVariant;
}

View File

@ -5,7 +5,7 @@ export type { ThemeColors } from './createColors';
export type { ThemeBreakpoints, ThemeBreakpointsKey } from './breakpoints';
export type { ThemeShadows } from './createShadows';
export type { ThemeShape } from './createShape';
export type { ThemeTypography, ThemeTypographyVariant } from './createTypography';
export type { ThemeTypography, ThemeTypographyVariant, ThemeTypographyVariantTypes } from './createTypography';
export type { ThemeTransitions } from './createTransitions';
export type { ThemeSpacing } from './createSpacing';
export type { ThemeZIndices } from './zIndex';

View File

@ -0,0 +1,8 @@
import { Props, ArgsTable } from '@storybook/addon-docs/blocks';
import { Text } from './Text';
# Text
Use for showing text.
<ArgsTable of={Text} />

View File

@ -0,0 +1,131 @@
import { Meta, Story } 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';
import { H1, H2, H3, H4, H5, H6, Span, P, Legend, TextModifier } from './TextElements';
const meta: Meta = {
title: 'General/Text',
component: Text,
parameters: {
docs: {
page: mdx,
},
controls: { exclude: ['as'] },
},
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' },
textAlignment: {
control: 'select',
options: ['inherit', 'initial', 'left', 'right', 'center', 'justify', undefined],
},
},
};
export const Example: Story = () => {
return (
<VerticalGroup>
<StoryExample name="Header, paragraph, span and legend elements">
<H1>h1. Heading</H1>
<H2>h2. Heading</H2>
<H3>h3. Heading</H3>
<H4>h4. Heading</H4>
<H5>h5. Heading</H5>
<H6>h6. Heading</H6>
<P>This is a paragraph</P>
<Legend>This is a legend</Legend>
<Span>This is a span</Span>
</StoryExample>
</VerticalGroup>
);
};
Example.parameters = {
controls: {
exclude: ['variant', 'weight', 'textAlignment', 'truncate', 'color', 'children'],
},
};
export const HeadingComponent: Story = (args) => {
return (
<div style={{ width: '300px' }}>
<H1 variant={args.variant} weight={args.weight} textAlignment={args.textAlignment} {...args}>
{args.children}
</H1>
</div>
);
};
HeadingComponent.args = {
variant: undefined,
weight: 'light',
textAlignment: 'center',
truncate: false,
color: 'primary',
children: 'This is a H1 component',
};
export const LegendComponent: Story = (args) => {
return (
<div style={{ width: '300px' }}>
<Legend variant={args.variant} weight={args.weight} textAlignment={args.textAlignment} {...args}>
{args.children}
</Legend>
</div>
);
};
LegendComponent.args = {
variant: undefined,
weight: 'bold',
textAlignment: 'center',
truncate: false,
color: 'error',
children: 'This is a lengend component',
};
export const TextModifierComponent: Story = (args) => {
return (
<div style={{ width: '300px' }}>
<H6 variant={args.variant} weight={args.weight} textAlignment={args.textAlignment} {...args}>
{args.children}{' '}
<TextModifier weight="bold" color="error">
{' '}
with a part of its text modified{' '}
</TextModifier>
</H6>
</div>
);
};
TextModifierComponent.args = {
variant: undefined,
weight: 'light',
textAlignment: 'center',
truncate: false,
color: 'maxContrast',
children: 'This is a H6 component',
};
export default meta;

View File

@ -0,0 +1,36 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { createTheme, ThemeTypographyVariantTypes } from '@grafana/data';
import { Text } from './Text';
describe('Text', () => {
it('renders correctly', () => {
render(<Text as={'h1'}>This is a text component</Text>);
expect(screen.getByText('This is a text component')).toBeInTheDocument();
});
it('keeps the element type but changes its styles', () => {
const customVariant: keyof ThemeTypographyVariantTypes = 'body';
render(
<Text as={'h1'} variant={customVariant}>
This is a text component
</Text>
);
const theme = createTheme();
const textComponent = screen.getByRole('heading');
expect(textComponent).toBeInTheDocument();
expect(textComponent).toHaveStyle(`fontSize: ${theme.typography.body.fontSize}`);
});
it('has the selected colour', () => {
const customColor = 'info';
const theme = createTheme();
render(
<Text as={'h1'} color={customColor}>
This is a text component
</Text>
);
const textComponent = screen.getByRole('heading');
expect(textComponent).toHaveStyle(`color:${theme.colors.info.text}`);
});
});

View File

@ -0,0 +1,106 @@
import { css } from '@emotion/css';
import React, { createElement, CSSProperties, useCallback } from 'react';
import { GrafanaTheme2, ThemeTypographyVariantTypes } from '@grafana/data';
import { useStyles2 } from '../../themes';
export interface TextProps {
/** Defines what HTML element is defined underneath */
as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p' | 'legend';
/** What typograpy variant should be used for the component. Only use if default variant for the defined element is not what is needed */
variant?: keyof ThemeTypographyVariantTypes;
/** Override the default weight for the used variant */
weight?: 'light' | 'regular' | 'medium' | 'bold';
/** Color to use for text */
color?: keyof GrafanaTheme2['colors']['text'] | 'error' | 'success' | 'warning' | 'info';
/** Use to cut the text off with ellipsis if there isn't space to show all of it. On hover shows the rest of the text */
truncate?: boolean;
/** Whether to align the text to left, center or right */
textAlignment?: CSSProperties['textAlign'];
children: React.ReactNode;
}
export const Text = React.forwardRef<HTMLElement, TextProps>(
({ as, variant, weight, color, truncate, textAlignment, children }, ref) => {
const styles = useStyles2(
useCallback(
(theme) => getTextStyles(theme, variant, color, weight, truncate, textAlignment),
[color, textAlignment, truncate, weight, variant]
)
);
return createElement(
as,
{
className: styles,
ref,
},
children
);
}
);
Text.displayName = 'Text';
const getTextStyles = (
theme: GrafanaTheme2,
variant?: keyof ThemeTypographyVariantTypes,
color?: TextProps['color'],
weight?: TextProps['weight'],
truncate?: TextProps['truncate'],
textAlignment?: TextProps['textAlignment']
) => {
return css([
variant && {
...theme.typography[variant],
},
{
margin: 0,
padding: 0,
},
color && {
color: customColor(color, theme),
},
weight && {
fontWeight: customWeight(weight, theme),
},
truncate && {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
textAlignment && {
textAlign: textAlignment,
},
]);
};
const customWeight = (weight: TextProps['weight'], theme: GrafanaTheme2): number => {
switch (weight) {
case 'bold':
return theme.typography.fontWeightBold;
case 'medium':
return theme.typography.fontWeightMedium;
case 'light':
return theme.typography.fontWeightLight;
case 'regular':
case undefined:
return theme.typography.fontWeightRegular;
}
};
const customColor = (color: TextProps['color'], theme: GrafanaTheme2): string | undefined => {
switch (color) {
case 'error':
return theme.colors.error.text;
case 'success':
return theme.colors.success.text;
case 'info':
return theme.colors.info.text;
case 'warning':
return theme.colors.warning.text;
default:
return color ? theme.colors.text[color] : undefined;
}
};

View File

@ -0,0 +1,75 @@
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Text, TextProps } from './Text';
interface TextElementsProps extends Omit<TextProps, 'as'> {}
interface TextModifierProps {
/** Override the default weight for the used variant */
weight?: 'light' | 'regular' | 'medium' | 'bold';
/** Color to use for text */
color?: keyof GrafanaTheme2['colors']['text'] | 'error' | 'success' | 'warning' | 'info';
children: React.ReactNode;
}
export const H1 = React.forwardRef<HTMLHeadingElement, TextElementsProps>((props, ref) => {
return <Text as="h1" {...props} variant={props.variant || 'h1'} ref={ref} />;
});
H1.displayName = 'H1';
export const H2 = React.forwardRef<HTMLHeadingElement, TextElementsProps>((props, ref) => {
return <Text as="h2" {...props} variant={props.variant || 'h2'} ref={ref} />;
});
H2.displayName = 'H2';
export const H3 = React.forwardRef<HTMLHeadingElement, TextElementsProps>((props, ref) => {
return <Text as="h3" {...props} variant={props.variant || 'h3'} ref={ref} />;
});
H3.displayName = 'H3';
export const H4 = React.forwardRef<HTMLHeadingElement, TextElementsProps>((props, ref) => {
return <Text as="h4" {...props} variant={props.variant || 'h4'} ref={ref} />;
});
H4.displayName = 'H4';
export const H5 = React.forwardRef<HTMLHeadingElement, TextElementsProps>((props, ref) => {
return <Text as="h5" {...props} variant={props.variant || 'h5'} ref={ref} />;
});
H5.displayName = 'H5';
export const H6 = React.forwardRef<HTMLHeadingElement, TextElementsProps>((props, ref) => {
return <Text as="h6" {...props} variant={props.variant || 'h6'} ref={ref} />;
});
H6.displayName = 'H6';
export const P = React.forwardRef<HTMLParagraphElement, TextElementsProps>((props, ref) => {
return <Text as="p" {...props} variant={props.variant || 'body'} ref={ref} />;
});
P.displayName = 'P';
export const Span = React.forwardRef<HTMLSpanElement, TextElementsProps>((props, ref) => {
return <Text as="span" {...props} variant={props.variant || 'bodySmall'} ref={ref} />;
});
Span.displayName = 'Span';
export const Legend = React.forwardRef<HTMLLegendElement, TextElementsProps>((props, ref) => {
return <Text as="legend" {...props} variant={props.variant || 'bodySmall'} ref={ref} />;
});
Legend.displayName = 'Legend';
export const TextModifier = React.forwardRef<HTMLSpanElement, TextModifierProps>((props, ref) => {
return <Text as="span" {...props} ref={ref} />;
});
TextModifier.displayName = 'TextModifier';

View File

@ -1,32 +0,0 @@
import { Meta, Story } from '@storybook/react';
import React from 'react';
import { StoryExample } from '../../utils/storybook/StoryExample';
import { VerticalGroup } from '../Layout/Layout';
import { Typography } from './Typography';
const meta: Meta = {
title: 'General/Typography',
component: Typography,
parameters: {
docs: {},
},
};
export const Typopgraphy: Story = () => {
return (
<VerticalGroup>
<StoryExample name="Native header elements (global styles)">
<h1>h1. Heading</h1>
<h2>h2. Heading</h2>
<h3>h3. Heading</h3>
<h4>h4. Heading</h4>
<h5>h5. Heading</h5>
<h6>h6. Heading</h6>
</StoryExample>
</VerticalGroup>
);
};
export default meta;

View File

@ -1,16 +0,0 @@
import React from 'react';
/** @internal */
export interface Props {
children: React.ReactNode;
}
/**
* @internal
* TODO implementation coming
**/
export const Typography = ({ children }: Props) => {
return <h1>{children}</h1>;
};
Typography.displayName = 'Typography';

View File

@ -0,0 +1 @@
export * from './components/Text/TextElements';