mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
8232659012
commit
64e609e19e
@ -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 <FieldValidationMessage>{message}</FieldValidationMessage>;
|
||||||
|
};
|
@ -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<FieldValidationMessageProps> = ({ children, className }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getFieldValidationMessageStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx(styles.fieldValidationMessage, className)}>
|
||||||
|
<i className={cx(styles.fieldValidationMessageIcon, 'fa', 'fa-warning')} />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
20
packages/grafana-ui/src/components/Forms/Form.story.tsx
Normal file
20
packages/grafana-ui/src/components/Forms/Form.story.tsx
Normal file
@ -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 (
|
||||||
|
<div>
|
||||||
|
<fieldset>
|
||||||
|
<Legend>Organization profile</Legend>
|
||||||
|
<Label description="Provide a name of your organisation that will be used across Grafana installation">
|
||||||
|
Organization name
|
||||||
|
</Label>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
22
packages/grafana-ui/src/components/Forms/Label.story.tsx
Normal file
22
packages/grafana-ui/src/components/Forms/Label.story.tsx
Normal file
@ -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 <Label description={description}>{label}</Label>;
|
||||||
|
};
|
36
packages/grafana-ui/src/components/Forms/Label.tsx
Normal file
36
packages/grafana-ui/src/components/Forms/Label.tsx
Normal file
@ -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<HTMLLabelElement> {
|
||||||
|
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<LabelProps> = ({ children, description, className, ...labelProps }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getLabelStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx(styles.label, className)}>
|
||||||
|
<label {...labelProps}>{children}</label>
|
||||||
|
{description && <div className={styles.description}>{description}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
19
packages/grafana-ui/src/components/Forms/Legend.story.tsx
Normal file
19
packages/grafana-ui/src/components/Forms/Legend.story.tsx
Normal file
@ -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 <Legend>{label}</Legend>;
|
||||||
|
});
|
31
packages/grafana-ui/src/components/Forms/Legend.tsx
Normal file
31
packages/grafana-ui/src/components/Forms/Legend.tsx
Normal file
@ -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<HTMLLegendElement> {
|
||||||
|
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<LabelProps> = ({ children, className, ...legendProps }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getLegendStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<legend className={cx(styles.legend, className)} {...legendProps}>
|
||||||
|
{children}
|
||||||
|
</legend>
|
||||||
|
);
|
||||||
|
};
|
13
packages/grafana-ui/src/components/Forms/getFormStyles.ts
Normal file
13
packages/grafana-ui/src/components/Forms/getFormStyles.ts
Normal file
@ -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),
|
||||||
|
};
|
||||||
|
});
|
9
packages/grafana-ui/src/components/Forms/index.ts
Normal file
9
packages/grafana-ui/src/components/Forms/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { getFormStyles } from './getFormStyles';
|
||||||
|
import { Label } from './Label';
|
||||||
|
|
||||||
|
const Forms = {
|
||||||
|
getFormStyles,
|
||||||
|
Label: Label,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Forms;
|
@ -209,8 +209,10 @@ exports[`Render should render with base threshold 1`] = `
|
|||||||
"formInputPaddingHorizontal": "8px",
|
"formInputPaddingHorizontal": "8px",
|
||||||
"formLabelMargin": "0 0 4px 0",
|
"formLabelMargin": "0 0 4px 0",
|
||||||
"formLabelPadding": "0 0 0 2px",
|
"formLabelPadding": "0 0 0 2px",
|
||||||
"formLegendMargin": "16px",
|
"formLegendMargin": "0 0 16px 0",
|
||||||
"formMargin": "32px",
|
"formMargin": "32px",
|
||||||
|
"formSpacingBase": 8,
|
||||||
|
"formValidationMessagePadding": "4px 8px",
|
||||||
"gutter": "30px",
|
"gutter": "30px",
|
||||||
"insetSquishMd": "4px 8px",
|
"insetSquishMd": "4px 8px",
|
||||||
"lg": "24px",
|
"lg": "24px",
|
||||||
@ -415,8 +417,10 @@ exports[`Render should render with base threshold 1`] = `
|
|||||||
"formInputPaddingHorizontal": "8px",
|
"formInputPaddingHorizontal": "8px",
|
||||||
"formLabelMargin": "0 0 4px 0",
|
"formLabelMargin": "0 0 4px 0",
|
||||||
"formLabelPadding": "0 0 0 2px",
|
"formLabelPadding": "0 0 0 2px",
|
||||||
"formLegendMargin": "16px",
|
"formLegendMargin": "0 0 16px 0",
|
||||||
"formMargin": "32px",
|
"formMargin": "32px",
|
||||||
|
"formSpacingBase": 8,
|
||||||
|
"formValidationMessagePadding": "4px 8px",
|
||||||
"gutter": "30px",
|
"gutter": "30px",
|
||||||
"insetSquishMd": "4px 8px",
|
"insetSquishMd": "4px 8px",
|
||||||
"lg": "24px",
|
"lg": "24px",
|
||||||
|
@ -93,3 +93,6 @@ export { Spinner } from './Spinner/Spinner';
|
|||||||
export { FadeTransition } from './transitions/FadeTransition';
|
export { FadeTransition } from './transitions/FadeTransition';
|
||||||
export { SlideOutTransition } from './transitions/SlideOutTransition';
|
export { SlideOutTransition } from './transitions/SlideOutTransition';
|
||||||
export { Segment, SegmentAsync, SegmentSelect } from './Segment/';
|
export { Segment, SegmentAsync, SegmentSelect } from './Segment/';
|
||||||
|
|
||||||
|
// Next-gen forms
|
||||||
|
export { default as Forms } from './Forms';
|
||||||
|
@ -82,9 +82,10 @@ const theme: GrafanaThemeCommons = {
|
|||||||
|
|
||||||
// Next-gen forms spacing variables
|
// Next-gen forms spacing variables
|
||||||
// TODO: Move variables definition to respective components when implementing
|
// TODO: Move variables definition to respective components when implementing
|
||||||
|
formSpacingBase: SPACING_BASE,
|
||||||
formMargin: `${SPACING_BASE * 4}px`,
|
formMargin: `${SPACING_BASE * 4}px`,
|
||||||
formFieldsetMargin: `${SPACING_BASE * 2}px`,
|
formFieldsetMargin: `${SPACING_BASE * 2}px`,
|
||||||
formLegendMargin: `${SPACING_BASE * 2}px`,
|
formLegendMargin: `0 0 ${SPACING_BASE * 2}px 0`,
|
||||||
formInputHeight: `${SPACING_BASE * 4}px`,
|
formInputHeight: `${SPACING_BASE * 4}px`,
|
||||||
formInputPaddingHorizontal: `${SPACING_BASE}px`,
|
formInputPaddingHorizontal: `${SPACING_BASE}px`,
|
||||||
|
|
||||||
@ -95,6 +96,7 @@ const theme: GrafanaThemeCommons = {
|
|||||||
formInputMargin: `${SPACING_BASE * 2}px`,
|
formInputMargin: `${SPACING_BASE * 2}px`,
|
||||||
formLabelPadding: '0 0 0 2px',
|
formLabelPadding: '0 0 0 2px',
|
||||||
formLabelMargin: '0 0 4px 0',
|
formLabelMargin: '0 0 4px 0',
|
||||||
|
formValidationMessagePadding: '4px 8px',
|
||||||
},
|
},
|
||||||
border: {
|
border: {
|
||||||
radius: {
|
radius: {
|
||||||
|
@ -64,6 +64,7 @@ export interface GrafanaThemeCommons {
|
|||||||
|
|
||||||
// Next-gen forms spacing variables
|
// Next-gen forms spacing variables
|
||||||
// TODO: Move variables definition to respective components when implementing
|
// TODO: Move variables definition to respective components when implementing
|
||||||
|
formSpacingBase: number;
|
||||||
formMargin: string;
|
formMargin: string;
|
||||||
formFieldsetMargin: string;
|
formFieldsetMargin: string;
|
||||||
formLegendMargin: string;
|
formLegendMargin: string;
|
||||||
@ -75,6 +76,7 @@ export interface GrafanaThemeCommons {
|
|||||||
formInputMargin: string;
|
formInputMargin: string;
|
||||||
formLabelPadding: string;
|
formLabelPadding: string;
|
||||||
formLabelMargin: string;
|
formLabelMargin: string;
|
||||||
|
formValidationMessagePadding: string;
|
||||||
};
|
};
|
||||||
border: {
|
border: {
|
||||||
radius: {
|
radius: {
|
||||||
|
@ -5,33 +5,14 @@
|
|||||||
// GENERAL STYLES
|
// 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
|
// Reset height since textareas have rows
|
||||||
// Set font for forms
|
// Set font for forms
|
||||||
label,
|
|
||||||
input,
|
input,
|
||||||
button,
|
button,
|
||||||
select,
|
select,
|
||||||
textarea {
|
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,
|
input,
|
||||||
button,
|
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)
|
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,
|
input,
|
||||||
select {
|
select {
|
||||||
background-color: $input-bg;
|
background-color: $input-bg;
|
||||||
|
Loading…
Reference in New Issue
Block a user