From fe59b65f9ef4f33b22fb5f8e8c47ba3aeb8f89a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Fri, 28 Apr 2023 15:31:40 +0200 Subject: [PATCH] Grafana UI: Create Text component (#66932) --- .betterer.results | 3 - .../src/themes/createTypography.ts | 23 +-- packages/grafana-data/src/themes/index.ts | 2 +- .../grafana-ui/src/components/Text/Text.mdx | 8 ++ .../components/Text/Text.story.internal.tsx | 131 ++++++++++++++++++ .../src/components/Text/Text.test.tsx | 36 +++++ .../grafana-ui/src/components/Text/Text.tsx | 106 ++++++++++++++ .../src/components/Text/TextElements.tsx | 75 ++++++++++ .../Typography/Typography.story.tsx | 32 ----- .../src/components/Typography/Typography.tsx | 16 --- packages/grafana-ui/src/unstable.ts | 1 + 11 files changed, 370 insertions(+), 63 deletions(-) create mode 100644 packages/grafana-ui/src/components/Text/Text.mdx create mode 100644 packages/grafana-ui/src/components/Text/Text.story.internal.tsx create mode 100644 packages/grafana-ui/src/components/Text/Text.test.tsx create mode 100644 packages/grafana-ui/src/components/Text/Text.tsx create mode 100644 packages/grafana-ui/src/components/Text/TextElements.tsx delete mode 100644 packages/grafana-ui/src/components/Typography/Typography.story.tsx delete mode 100644 packages/grafana-ui/src/components/Typography/Typography.tsx create mode 100644 packages/grafana-ui/src/unstable.ts diff --git a/.betterer.results b/.betterer.results index 125dde4bc3a..4dbb503be09 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] ], diff --git a/packages/grafana-data/src/themes/createTypography.ts b/packages/grafana-data/src/themes/createTypography.ts index 5dfe3ebaf47..1473adc2bb8 100644 --- a/packages/grafana-data/src/themes/createTypography.ts +++ b/packages/grafana-data/src/themes/createTypography.ts @@ -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; +} diff --git a/packages/grafana-data/src/themes/index.ts b/packages/grafana-data/src/themes/index.ts index a0f2f9fcc9d..9fac0e68a3c 100644 --- a/packages/grafana-data/src/themes/index.ts +++ b/packages/grafana-data/src/themes/index.ts @@ -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'; diff --git a/packages/grafana-ui/src/components/Text/Text.mdx b/packages/grafana-ui/src/components/Text/Text.mdx new file mode 100644 index 00000000000..bbbf8abc2e8 --- /dev/null +++ b/packages/grafana-ui/src/components/Text/Text.mdx @@ -0,0 +1,8 @@ +import { Props, ArgsTable } from '@storybook/addon-docs/blocks'; +import { Text } from './Text'; + +# Text + +Use for showing text. + + diff --git a/packages/grafana-ui/src/components/Text/Text.story.internal.tsx b/packages/grafana-ui/src/components/Text/Text.story.internal.tsx new file mode 100644 index 00000000000..54394aa47d9 --- /dev/null +++ b/packages/grafana-ui/src/components/Text/Text.story.internal.tsx @@ -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 ( + + +

h1. Heading

+

h2. Heading

+

h3. Heading

+

h4. Heading

+
h5. Heading
+
h6. Heading
+

This is a paragraph

+ This is a legend + This is a span +
+
+ ); +}; +Example.parameters = { + controls: { + exclude: ['variant', 'weight', 'textAlignment', 'truncate', 'color', 'children'], + }, +}; + +export const HeadingComponent: Story = (args) => { + return ( +
+

+ {args.children} +

+
+ ); +}; +HeadingComponent.args = { + variant: undefined, + weight: 'light', + textAlignment: 'center', + truncate: false, + color: 'primary', + children: 'This is a H1 component', +}; + +export const LegendComponent: Story = (args) => { + return ( +
+ + {args.children} + +
+ ); +}; + +LegendComponent.args = { + variant: undefined, + weight: 'bold', + textAlignment: 'center', + truncate: false, + color: 'error', + children: 'This is a lengend component', +}; + +export const TextModifierComponent: Story = (args) => { + return ( +
+
+ {args.children}{' '} + + {' '} + with a part of its text modified{' '} + +
+
+ ); +}; +TextModifierComponent.args = { + variant: undefined, + weight: 'light', + textAlignment: 'center', + truncate: false, + color: 'maxContrast', + children: 'This is a H6 component', +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/Text/Text.test.tsx b/packages/grafana-ui/src/components/Text/Text.test.tsx new file mode 100644 index 00000000000..591211db265 --- /dev/null +++ b/packages/grafana-ui/src/components/Text/Text.test.tsx @@ -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(This is a text component); + expect(screen.getByText('This is a text component')).toBeInTheDocument(); + }); + it('keeps the element type but changes its styles', () => { + const customVariant: keyof ThemeTypographyVariantTypes = 'body'; + render( + + This is a text component + + ); + 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( + + This is a text component + + ); + const textComponent = screen.getByRole('heading'); + expect(textComponent).toHaveStyle(`color:${theme.colors.info.text}`); + }); +}); diff --git a/packages/grafana-ui/src/components/Text/Text.tsx b/packages/grafana-ui/src/components/Text/Text.tsx new file mode 100644 index 00000000000..fa191317f90 --- /dev/null +++ b/packages/grafana-ui/src/components/Text/Text.tsx @@ -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( + ({ 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; + } +}; diff --git a/packages/grafana-ui/src/components/Text/TextElements.tsx b/packages/grafana-ui/src/components/Text/TextElements.tsx new file mode 100644 index 00000000000..f0569f9b376 --- /dev/null +++ b/packages/grafana-ui/src/components/Text/TextElements.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { Text, TextProps } from './Text'; + +interface TextElementsProps extends Omit {} + +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((props, ref) => { + return ; +}); + +H1.displayName = 'H1'; + +export const H2 = React.forwardRef((props, ref) => { + return ; +}); + +H2.displayName = 'H2'; + +export const H3 = React.forwardRef((props, ref) => { + return ; +}); + +H3.displayName = 'H3'; + +export const H4 = React.forwardRef((props, ref) => { + return ; +}); + +H4.displayName = 'H4'; + +export const H5 = React.forwardRef((props, ref) => { + return ; +}); + +H5.displayName = 'H5'; + +export const H6 = React.forwardRef((props, ref) => { + return ; +}); + +H6.displayName = 'H6'; + +export const P = React.forwardRef((props, ref) => { + return ; +}); + +P.displayName = 'P'; + +export const Span = React.forwardRef((props, ref) => { + return ; +}); + +Span.displayName = 'Span'; + +export const Legend = React.forwardRef((props, ref) => { + return ; +}); + +Legend.displayName = 'Legend'; + +export const TextModifier = React.forwardRef((props, ref) => { + return ; +}); + +TextModifier.displayName = 'TextModifier'; diff --git a/packages/grafana-ui/src/components/Typography/Typography.story.tsx b/packages/grafana-ui/src/components/Typography/Typography.story.tsx deleted file mode 100644 index e9f9ca6eacc..00000000000 --- a/packages/grafana-ui/src/components/Typography/Typography.story.tsx +++ /dev/null @@ -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 ( - - -

h1. Heading

-

h2. Heading

-

h3. Heading

-

h4. Heading

-
h5. Heading
-
h6. Heading
-
-
- ); -}; - -export default meta; diff --git a/packages/grafana-ui/src/components/Typography/Typography.tsx b/packages/grafana-ui/src/components/Typography/Typography.tsx deleted file mode 100644 index 90aa7d11379..00000000000 --- a/packages/grafana-ui/src/components/Typography/Typography.tsx +++ /dev/null @@ -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

{children}

; -}; - -Typography.displayName = 'Typography'; diff --git a/packages/grafana-ui/src/unstable.ts b/packages/grafana-ui/src/unstable.ts new file mode 100644 index 00000000000..8b61345b899 --- /dev/null +++ b/packages/grafana-ui/src/unstable.ts @@ -0,0 +1 @@ +export * from './components/Text/TextElements';