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