From 64e609e19e295f69154912703ebae10687c2edc3 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 22 Oct 2019 17:36:07 +0200 Subject: [PATCH] Forms: Introduce typographic form elements (#19879) * Implement Label component * Expose next-gen form components from grafana-ui under Forms namespace * Minor Label update * Add Legend component * Test form story * Expose Legend class name via getFormStyles * Test * FieldValidationMessage spacing * Expose FieldValidationMessage styles via getFormStyles * Update snapshot --- .../Forms/FieldValidationMessage.story.tsx | 20 ++++++++ .../Forms/FieldValidationMessage.tsx | 51 +++++++++++++++++++ .../src/components/Forms/Form.story.tsx | 20 ++++++++ .../src/components/Forms/Label.story.tsx | 22 ++++++++ .../grafana-ui/src/components/Forms/Label.tsx | 36 +++++++++++++ .../src/components/Forms/Legend.story.tsx | 19 +++++++ .../src/components/Forms/Legend.tsx | 31 +++++++++++ .../src/components/Forms/getFormStyles.ts | 13 +++++ .../grafana-ui/src/components/Forms/index.ts | 9 ++++ .../ThresholdsEditor.test.tsx.snap | 8 ++- packages/grafana-ui/src/components/index.ts | 3 ++ packages/grafana-ui/src/themes/default.ts | 4 +- packages/grafana-ui/src/types/theme.ts | 2 + public/sass/base/_forms.scss | 28 +--------- 14 files changed, 237 insertions(+), 29 deletions(-) create mode 100644 packages/grafana-ui/src/components/Forms/FieldValidationMessage.story.tsx create mode 100644 packages/grafana-ui/src/components/Forms/FieldValidationMessage.tsx create mode 100644 packages/grafana-ui/src/components/Forms/Form.story.tsx create mode 100644 packages/grafana-ui/src/components/Forms/Label.story.tsx create mode 100644 packages/grafana-ui/src/components/Forms/Label.tsx create mode 100644 packages/grafana-ui/src/components/Forms/Legend.story.tsx create mode 100644 packages/grafana-ui/src/components/Forms/Legend.tsx create mode 100644 packages/grafana-ui/src/components/Forms/getFormStyles.ts create mode 100644 packages/grafana-ui/src/components/Forms/index.ts diff --git a/packages/grafana-ui/src/components/Forms/FieldValidationMessage.story.tsx b/packages/grafana-ui/src/components/Forms/FieldValidationMessage.story.tsx new file mode 100644 index 00000000000..fcd93b2a058 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/FieldValidationMessage.story.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { FieldValidationMessage } from './FieldValidationMessage'; +import { text } from '@storybook/addon-knobs'; + +const getKnobs = () => { + return { + message: text('message', 'Invalid input message'), + }; +}; + +export default { + title: 'UI/Forms/FieldValidationMessage', + component: FieldValidationMessage, +}; + +export const simple = () => { + const { message } = getKnobs(); + + return {message}; +}; diff --git a/packages/grafana-ui/src/components/Forms/FieldValidationMessage.tsx b/packages/grafana-ui/src/components/Forms/FieldValidationMessage.tsx new file mode 100644 index 00000000000..ca6341dd900 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/FieldValidationMessage.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { useTheme, stylesFactory } from '../../themes'; +import { GrafanaTheme } from '../../types'; +import { css, cx } from 'emotion'; + +export interface FieldValidationMessageProps { + children: string; + className?: string; +} + +export const getFieldValidationMessageStyles = stylesFactory((theme: GrafanaTheme) => { + return { + fieldValidationMessage: css` + font-size: ${theme.typography.size.sm}; + font-weight: ${theme.typography.weight.semibold}; + margin: ${theme.spacing.formLabelMargin}; + padding: ${theme.spacing.formValidationMessagePadding}; + color: ${theme.colors.formValidationMessageText}; + background: ${theme.colors.formValidationMessageBg}; + border-radius: ${theme.border.radius.sm}; + position: relative; + + &:before { + content: ''; + position: absolute; + left: 9px; + top: -5px; + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid ${theme.colors.formValidationMessageBg}; + } + `, + fieldValidationMessageIcon: css` + margin-right: ${theme.spacing.formSpacingBase}px; + `, + }; +}); + +export const FieldValidationMessage: React.FC = ({ children, className }) => { + const theme = useTheme(); + const styles = getFieldValidationMessageStyles(theme); + + return ( +
+ + {children} +
+ ); +}; diff --git a/packages/grafana-ui/src/components/Forms/Form.story.tsx b/packages/grafana-ui/src/components/Forms/Form.story.tsx new file mode 100644 index 00000000000..a9e374055cd --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/Form.story.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import { Legend } from './Legend'; +import { Label } from './Label'; + +const story = storiesOf('UI/Forms/Test', module); + +story.add('Configuration/Preferences', () => { + return ( +
+
+ Organization profile + +
+
+ ); +}); diff --git a/packages/grafana-ui/src/components/Forms/Label.story.tsx b/packages/grafana-ui/src/components/Forms/Label.story.tsx new file mode 100644 index 00000000000..1386c24cd55 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/Label.story.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { text } from '@storybook/addon-knobs'; + +import { Label } from './Label'; + +const getKnobs = () => { + return { + label: text('text', 'Form element label'), + description: text('description', 'Description of the form field'), + }; +}; + +export default { + title: 'UI|Forms', + component: Label, +}; + +export const simple = () => { + const { label, description } = getKnobs(); + + return ; +}; diff --git a/packages/grafana-ui/src/components/Forms/Label.tsx b/packages/grafana-ui/src/components/Forms/Label.tsx new file mode 100644 index 00000000000..1b648f0ad49 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/Label.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useTheme, stylesFactory } from '../../themes'; +import { GrafanaTheme } from '../../types'; +import { css, cx } from 'emotion'; + +export interface LabelProps extends React.HTMLAttributes { + children: string; + description?: string; +} + +export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => { + return { + label: css` + font-size: ${theme.typography.size.sm}; + font-weight: ${theme.typography.weight.semibold}; + margin: ${theme.spacing.formLabelMargin}; + padding: ${theme.spacing.formLabelPadding}; + color: ${theme.colors.formLabel}; + `, + description: css` + font-weight: ${theme.typography.weight.regular}; + `, + }; +}); + +export const Label: React.FC = ({ children, description, className, ...labelProps }) => { + const theme = useTheme(); + const styles = getLabelStyles(theme); + + return ( +
+ + {description &&
{description}
} +
+ ); +}; diff --git a/packages/grafana-ui/src/components/Forms/Legend.story.tsx b/packages/grafana-ui/src/components/Forms/Legend.story.tsx new file mode 100644 index 00000000000..6820c6ff363 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/Legend.story.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { text } from '@storybook/addon-knobs'; + +import { Legend } from './Legend'; + +const getKnobs = () => { + return { + label: text('text', 'Form section'), + }; +}; + +const story = storiesOf('UI/Forms', module); + +story.add('Legend', () => { + const { label } = getKnobs(); + + return {label}; +}); diff --git a/packages/grafana-ui/src/components/Forms/Legend.tsx b/packages/grafana-ui/src/components/Forms/Legend.tsx new file mode 100644 index 00000000000..17260714320 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/Legend.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { useTheme, stylesFactory } from '../../themes'; +import { GrafanaTheme } from '../../types'; +import { css, cx } from 'emotion'; + +export interface LabelProps extends React.HTMLAttributes { + children: string; + description?: string; +} + +export const getLegendStyles = stylesFactory((theme: GrafanaTheme) => { + return { + legend: css` + font-size: ${theme.typography.heading.h3}; + font-weight: ${theme.typography.weight.regular}; + margin: ${theme.spacing.formLegendMargin}; + color: ${theme.colors.formLegend}; + `, + }; +}); + +export const Legend: React.FC = ({ children, className, ...legendProps }) => { + const theme = useTheme(); + const styles = getLegendStyles(theme); + + return ( + + {children} + + ); +}; diff --git a/packages/grafana-ui/src/components/Forms/getFormStyles.ts b/packages/grafana-ui/src/components/Forms/getFormStyles.ts new file mode 100644 index 00000000000..41e9baa9da5 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/getFormStyles.ts @@ -0,0 +1,13 @@ +import { stylesFactory } from '../../themes'; +import { GrafanaTheme } from '../../types'; +import { getLabelStyles } from './Label'; +import { getLegendStyles } from './Legend'; +import { getFieldValidationMessageStyles } from './FieldValidationMessage'; + +export const getFormStyles = stylesFactory((theme: GrafanaTheme) => { + return { + ...getLabelStyles(theme), + ...getLegendStyles(theme), + ...getFieldValidationMessageStyles(theme), + }; +}); diff --git a/packages/grafana-ui/src/components/Forms/index.ts b/packages/grafana-ui/src/components/Forms/index.ts new file mode 100644 index 00000000000..446a2a1d026 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/index.ts @@ -0,0 +1,9 @@ +import { getFormStyles } from './getFormStyles'; +import { Label } from './Label'; + +const Forms = { + getFormStyles, + Label: Label, +}; + +export default Forms; diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap index cf2726ebd46..284e5e33910 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap +++ b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap @@ -209,8 +209,10 @@ exports[`Render should render with base threshold 1`] = ` "formInputPaddingHorizontal": "8px", "formLabelMargin": "0 0 4px 0", "formLabelPadding": "0 0 0 2px", - "formLegendMargin": "16px", + "formLegendMargin": "0 0 16px 0", "formMargin": "32px", + "formSpacingBase": 8, + "formValidationMessagePadding": "4px 8px", "gutter": "30px", "insetSquishMd": "4px 8px", "lg": "24px", @@ -415,8 +417,10 @@ exports[`Render should render with base threshold 1`] = ` "formInputPaddingHorizontal": "8px", "formLabelMargin": "0 0 4px 0", "formLabelPadding": "0 0 0 2px", - "formLegendMargin": "16px", + "formLegendMargin": "0 0 16px 0", "formMargin": "32px", + "formSpacingBase": 8, + "formValidationMessagePadding": "4px 8px", "gutter": "30px", "insetSquishMd": "4px 8px", "lg": "24px", diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 42c528210f0..97d3f80668a 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -93,3 +93,6 @@ export { Spinner } from './Spinner/Spinner'; export { FadeTransition } from './transitions/FadeTransition'; export { SlideOutTransition } from './transitions/SlideOutTransition'; export { Segment, SegmentAsync, SegmentSelect } from './Segment/'; + +// Next-gen forms +export { default as Forms } from './Forms'; diff --git a/packages/grafana-ui/src/themes/default.ts b/packages/grafana-ui/src/themes/default.ts index a2ef04da58f..22578ff0ac9 100644 --- a/packages/grafana-ui/src/themes/default.ts +++ b/packages/grafana-ui/src/themes/default.ts @@ -82,9 +82,10 @@ const theme: GrafanaThemeCommons = { // Next-gen forms spacing variables // TODO: Move variables definition to respective components when implementing + formSpacingBase: SPACING_BASE, formMargin: `${SPACING_BASE * 4}px`, formFieldsetMargin: `${SPACING_BASE * 2}px`, - formLegendMargin: `${SPACING_BASE * 2}px`, + formLegendMargin: `0 0 ${SPACING_BASE * 2}px 0`, formInputHeight: `${SPACING_BASE * 4}px`, formInputPaddingHorizontal: `${SPACING_BASE}px`, @@ -95,6 +96,7 @@ const theme: GrafanaThemeCommons = { formInputMargin: `${SPACING_BASE * 2}px`, formLabelPadding: '0 0 0 2px', formLabelMargin: '0 0 4px 0', + formValidationMessagePadding: '4px 8px', }, border: { radius: { diff --git a/packages/grafana-ui/src/types/theme.ts b/packages/grafana-ui/src/types/theme.ts index d7bd051987d..03c0c24ac3e 100644 --- a/packages/grafana-ui/src/types/theme.ts +++ b/packages/grafana-ui/src/types/theme.ts @@ -64,6 +64,7 @@ export interface GrafanaThemeCommons { // Next-gen forms spacing variables // TODO: Move variables definition to respective components when implementing + formSpacingBase: number; formMargin: string; formFieldsetMargin: string; formLegendMargin: string; @@ -75,6 +76,7 @@ export interface GrafanaThemeCommons { formInputMargin: string; formLabelPadding: string; formLabelMargin: string; + formValidationMessagePadding: string; }; border: { radius: { diff --git a/public/sass/base/_forms.scss b/public/sass/base/_forms.scss index bfd59b113b9..7cf9f7eb1a0 100644 --- a/public/sass/base/_forms.scss +++ b/public/sass/base/_forms.scss @@ -5,33 +5,14 @@ // GENERAL STYLES // -------------- -// Groups of fields with labels on top (legends) -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: $line-height-base; - font-size: $font-size-base * 1.5; - line-height: $line-height-base * 2; - color: $gray-3; - border: 0; - border-bottom: 1px solid #e5e5e5; - - // Small - small { - font-size: $line-height-base * 0.75; - color: $gray-2; - } -} - // Reset height since textareas have rows // Set font for forms -label, + input, button, select, textarea { - @include font-shorthand($font-size-base, normal, $line-height-base); // Set size, weight, line-height here + @include font-shorthand($font-size-base, normal, $line-height-base); } input, button, @@ -40,11 +21,6 @@ textarea { font-family: $font-family-sans-serif; // And only set font-family here for those that need it (note the missing label element) } -// Identify controls by their labels -label { - display: block; -} - input, select { background-color: $input-bg;