mirror of https://github.com/grafana/grafana
Grafana UI: Create Text component (#66932)
parent
692bb9ed1a
commit
fe59b65f9e
@ -0,0 +1,8 @@ |
|||||||
|
import { Props, ArgsTable } from '@storybook/addon-docs/blocks'; |
||||||
|
import { Text } from './Text'; |
||||||
|
|
||||||
|
# Text |
||||||
|
|
||||||
|
Use for showing text. |
||||||
|
|
||||||
|
<ArgsTable of={Text} /> |
||||||
@ -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; |
||||||
@ -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}`); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -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; |
||||||
|
} |
||||||
|
}; |
||||||
@ -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'; |
||||||
@ -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; |
|
||||||
@ -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'; |
|
||||||
@ -0,0 +1 @@ |
|||||||
|
export * from './components/Text/TextElements'; |
||||||
Loading…
Reference in new issue