mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Forms: Introduce new Primary, Secondary and Destructive buttons (#19973)
* Implement Label component * Expose next-gen form components from grafana-ui under Forms namespace * adding knobs to story, setting new variants and sizes * handle next gen button in their own component * removing duplication * fix Thresholds test * removing blank lines * moving noUnusedLocals * new button should not need theme * remove not used export * pseudo classes for focus state * remove not used things * use correct border radius * extracting focus styles to commonStyles for reuse * tidying up getButtonStyles * Adding a few examples to the doc * Adding props table
This commit is contained in:
parent
fc1ded5026
commit
24183ba390
@ -161,7 +161,7 @@
|
|||||||
"jest": "jest --notify --watch",
|
"jest": "jest --notify --watch",
|
||||||
"e2e-tests": "jest --runInBand --config=jest.config.e2e.js",
|
"e2e-tests": "jest --runInBand --config=jest.config.e2e.js",
|
||||||
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
|
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
|
||||||
"storybook": "cd packages/grafana-ui && yarn storybook",
|
"storybook": "cd packages/grafana-ui && yarn storybook --ci",
|
||||||
"storybook:build": "cd packages/grafana-ui && yarn storybook:build",
|
"storybook:build": "cd packages/grafana-ui && yarn storybook:build",
|
||||||
"prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"",
|
"prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"",
|
||||||
"prettier:write": "prettier --list-different \"**/*.{ts,tsx,scss}\" --write",
|
"prettier:write": "prettier --list-different \"**/*.{ts,tsx,scss}\" --write",
|
||||||
|
11
packages/grafana-ui/.storybook/tsconfig.json
Normal file
11
packages/grafana-ui/.storybook/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"include": ["../src/**/*.ts", "../src/**/*.tsx"],
|
||||||
|
"exclude": ["../dist", "../node_modules"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
|
||||||
|
"declarationDir": "dist",
|
||||||
|
"outDir": "compiled"
|
||||||
|
}
|
||||||
|
}
|
@ -9,13 +9,13 @@ module.exports = ({ config, mode }) => {
|
|||||||
loader: require.resolve('ts-loader'),
|
loader: require.resolve('ts-loader'),
|
||||||
options: {
|
options: {
|
||||||
// transpileOnly: true,
|
// transpileOnly: true,
|
||||||
configFile: path.resolve(__dirname, '../tsconfig.json'),
|
configFile: path.resolve(__dirname, 'tsconfig.json'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loader: require.resolve('react-docgen-typescript-loader'),
|
loader: require.resolve('react-docgen-typescript-loader'),
|
||||||
options: {
|
options: {
|
||||||
tsconfigPath: path.resolve(__dirname, '../tsconfig.json'),
|
tsconfigPath: path.resolve(__dirname, 'tsconfig.json'),
|
||||||
// https://github.com/styleguidist/react-docgen-typescript#parseroptions
|
// https://github.com/styleguidist/react-docgen-typescript#parseroptions
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
propFilter: prop => {
|
propFilter: prop => {
|
||||||
|
@ -1,33 +1,9 @@
|
|||||||
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
|
import React, { ComponentType, ReactNode } from 'react';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import { Themeable, GrafanaTheme } from '../../types';
|
import { selectThemeVariant, stylesFactory } from '../../themes';
|
||||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
import { AbstractButtonProps, ButtonSize, ButtonStyles, ButtonVariant, CommonButtonProps, StyleDeps } from './types';
|
||||||
import { stylesFactory } from '../../themes/stylesFactory';
|
import { GrafanaTheme } from '../../types';
|
||||||
|
|
||||||
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'inverse' | 'transparent';
|
|
||||||
|
|
||||||
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
||||||
|
|
||||||
export interface CommonButtonProps {
|
|
||||||
size?: ButtonSize;
|
|
||||||
variant?: ButtonVariant;
|
|
||||||
/**
|
|
||||||
* icon prop is a temporary solution. It accepts lefacy icon class names for the icon to be rendered.
|
|
||||||
* TODO: migrate to a component when we are going to migrate icons to @grafana/ui
|
|
||||||
*/
|
|
||||||
icon?: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LinkButtonProps extends CommonButtonProps, AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
export interface ButtonProps extends CommonButtonProps, ButtonHTMLAttributes<HTMLButtonElement> {}
|
|
||||||
|
|
||||||
interface AbstractButtonProps extends CommonButtonProps, Themeable {
|
|
||||||
renderAs: React.ComponentType<CommonButtonProps> | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonVariantStyles = (
|
const buttonVariantStyles = (
|
||||||
from: string,
|
from: string,
|
||||||
@ -50,12 +26,6 @@ const buttonVariantStyles = (
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface StyleDeps {
|
|
||||||
theme: GrafanaTheme;
|
|
||||||
size: ButtonSize;
|
|
||||||
variant: ButtonVariant;
|
|
||||||
withIcon: boolean;
|
|
||||||
}
|
|
||||||
const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: StyleDeps) => {
|
const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: StyleDeps) => {
|
||||||
const borderRadius = theme.border.radius.sm;
|
const borderRadius = theme.border.radius.sm;
|
||||||
let padding,
|
let padding,
|
||||||
@ -72,12 +42,14 @@ const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: Style
|
|||||||
iconDistance = theme.spacing.xs;
|
iconDistance = theme.spacing.xs;
|
||||||
height = theme.height.sm;
|
height = theme.height.sm;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'md':
|
case 'md':
|
||||||
padding = `${theme.spacing.sm} ${theme.spacing.md}`;
|
padding = `${theme.spacing.sm} ${theme.spacing.md}`;
|
||||||
fontSize = theme.typography.size.md;
|
fontSize = theme.typography.size.md;
|
||||||
iconDistance = theme.spacing.sm;
|
iconDistance = theme.spacing.sm;
|
||||||
height = theme.height.md;
|
height = theme.height.md;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'lg':
|
case 'lg':
|
||||||
padding = `${theme.spacing.md} ${theme.spacing.lg}`;
|
padding = `${theme.spacing.md} ${theme.spacing.lg}`;
|
||||||
fontSize = theme.typography.size.lg;
|
fontSize = theme.typography.size.lg;
|
||||||
@ -85,6 +57,7 @@ const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: Style
|
|||||||
iconDistance = theme.spacing.sm;
|
iconDistance = theme.spacing.sm;
|
||||||
height = theme.height.lg;
|
height = theme.height.lg;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
padding = `${theme.spacing.sm} ${theme.spacing.md}`;
|
padding = `${theme.spacing.sm} ${theme.spacing.md}`;
|
||||||
iconDistance = theme.spacing.sm;
|
iconDistance = theme.spacing.sm;
|
||||||
@ -96,12 +69,15 @@ const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: Style
|
|||||||
case 'primary':
|
case 'primary':
|
||||||
background = buttonVariantStyles(theme.colors.greenBase, theme.colors.greenShade, theme.colors.white);
|
background = buttonVariantStyles(theme.colors.greenBase, theme.colors.greenShade, theme.colors.white);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'secondary':
|
case 'secondary':
|
||||||
background = buttonVariantStyles(theme.colors.blueBase, theme.colors.blueShade, theme.colors.white);
|
background = buttonVariantStyles(theme.colors.blueBase, theme.colors.blueShade, theme.colors.white);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'danger':
|
case 'danger':
|
||||||
background = buttonVariantStyles(theme.colors.redBase, theme.colors.redShade, theme.colors.white);
|
background = buttonVariantStyles(theme.colors.redBase, theme.colors.redShade, theme.colors.white);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'inverse':
|
case 'inverse':
|
||||||
const from = selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.dark6 }, theme.type) as string;
|
const from = selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.dark6 }, theme.type) as string;
|
||||||
const to = selectThemeVariant(
|
const to = selectThemeVariant(
|
||||||
@ -118,6 +94,7 @@ const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: Style
|
|||||||
|
|
||||||
background = buttonVariantStyles(from, to, theme.colors.link, 'rgba(0, 0, 0, 0.1)', true);
|
background = buttonVariantStyles(from, to, theme.colors.link, 'rgba(0, 0, 0, 0.1)', true);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'transparent':
|
case 'transparent':
|
||||||
background = css`
|
background = css`
|
||||||
${buttonVariantStyles('', '', theme.colors.link, 'rgba(0, 0, 0, 0.1)', true)};
|
${buttonVariantStyles('', '', theme.colors.link, 'rgba(0, 0, 0, 0.1)', true)};
|
||||||
@ -164,23 +141,22 @@ const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: Style
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AbstractButton: React.FunctionComponent<AbstractButtonProps> = ({
|
export const renderButton = (
|
||||||
renderAs,
|
theme: GrafanaTheme,
|
||||||
theme,
|
buttonStyles: ButtonStyles,
|
||||||
size = 'md',
|
renderAs: ComponentType<CommonButtonProps> | string,
|
||||||
variant = 'primary',
|
children: ReactNode,
|
||||||
className,
|
size: ButtonSize,
|
||||||
icon,
|
variant: ButtonVariant,
|
||||||
children,
|
icon?: string,
|
||||||
...otherProps
|
className?: string,
|
||||||
}) => {
|
otherProps?: Partial<AbstractButtonProps>
|
||||||
const buttonStyles = getButtonStyles({ theme, size, variant, withIcon: !!icon });
|
) => {
|
||||||
const nonHtmlProps = {
|
const nonHtmlProps = {
|
||||||
theme,
|
theme,
|
||||||
size,
|
size,
|
||||||
variant,
|
variant,
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalClassName = cx(buttonStyles.button, className);
|
const finalClassName = cx(buttonStyles.button, className);
|
||||||
const finalChildren = icon ? (
|
const finalChildren = icon ? (
|
||||||
<span className={buttonStyles.iconWrap}>
|
<span className={buttonStyles.iconWrap}>
|
||||||
@ -208,4 +184,19 @@ export const AbstractButton: React.FunctionComponent<AbstractButtonProps> = ({
|
|||||||
return React.createElement(renderAs, finalProps);
|
return React.createElement(renderAs, finalProps);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AbstractButton: React.FunctionComponent<AbstractButtonProps> = ({
|
||||||
|
renderAs,
|
||||||
|
theme,
|
||||||
|
size = 'md',
|
||||||
|
variant = 'primary',
|
||||||
|
className,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
...otherProps
|
||||||
|
}) => {
|
||||||
|
const buttonStyles = getButtonStyles({ theme, size, variant, withIcon: !!icon });
|
||||||
|
|
||||||
|
return renderButton(theme, buttonStyles, renderAs, children, size, variant, icon, className, otherProps);
|
||||||
|
};
|
||||||
|
|
||||||
AbstractButton.displayName = 'AbstractButton';
|
AbstractButton.displayName = 'AbstractButton';
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { Button, LinkButton } from './Button';
|
import { Button, LinkButton } from './Button';
|
||||||
import { CommonButtonProps } from './AbstractButton';
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import withPropsCombinations from 'react-storybook-addon-props-combinations';
|
import withPropsCombinations from 'react-storybook-addon-props-combinations';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { ThemeableCombinationsRowRenderer } from '../../utils/storybook/CombinationsRowRenderer';
|
import { ThemeableCombinationsRowRenderer } from '../../utils/storybook/CombinationsRowRenderer';
|
||||||
import { select, boolean } from '@storybook/addon-knobs';
|
import { select, boolean } from '@storybook/addon-knobs';
|
||||||
|
import { CommonButtonProps } from './types';
|
||||||
|
|
||||||
const ButtonStories = storiesOf('UI/Button', module);
|
const ButtonStories = storiesOf('UI/Button', module);
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ const defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
size: ['xs', 'sm', 'md', 'lg', 'xl'],
|
size: ['xs', 'sm', 'md', 'lg'],
|
||||||
variant: ['primary', 'secondary', 'danger', 'inverse', 'transparent'],
|
variant: ['primary', 'secondary', 'danger', 'inverse', 'transparent'],
|
||||||
};
|
};
|
||||||
const combinationOptions = {
|
const combinationOptions = {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { AbstractButton, ButtonProps, LinkButtonProps } from './AbstractButton';
|
import { AbstractButton } from './AbstractButton';
|
||||||
import { ThemeContext } from '../../themes';
|
import { ThemeContext } from '../../themes';
|
||||||
|
import { ButtonProps, LinkButtonProps } from './types';
|
||||||
|
|
||||||
export const Button: React.FunctionComponent<ButtonProps> = props => {
|
export const Button: React.FunctionComponent<ButtonProps> = props => {
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useContext(ThemeContext);
|
||||||
|
39
packages/grafana-ui/src/components/Button/types.ts
Normal file
39
packages/grafana-ui/src/components/Button/types.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { AnchorHTMLAttributes, ButtonHTMLAttributes, ComponentType } from 'react';
|
||||||
|
import { GrafanaTheme, Themeable } from '../../types';
|
||||||
|
|
||||||
|
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'inverse' | 'transparent' | 'destructive';
|
||||||
|
|
||||||
|
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
|
export interface StyleDeps {
|
||||||
|
theme: GrafanaTheme;
|
||||||
|
size: ButtonSize;
|
||||||
|
variant: ButtonVariant;
|
||||||
|
withIcon: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonStyles {
|
||||||
|
button: string;
|
||||||
|
iconWrap: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommonButtonProps {
|
||||||
|
size?: ButtonSize;
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
/**
|
||||||
|
* icon prop is a temporary solution. It accepts legacy icon class names for the icon to be rendered.
|
||||||
|
* TODO: migrate to a component when we are going to migrate icons to @grafana/ui
|
||||||
|
*/
|
||||||
|
icon?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkButtonProps extends CommonButtonProps, AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
export interface ButtonProps extends CommonButtonProps, ButtonHTMLAttributes<HTMLButtonElement> {}
|
||||||
|
|
||||||
|
export interface AbstractButtonProps extends CommonButtonProps, Themeable {
|
||||||
|
renderAs: ComponentType<CommonButtonProps> | string;
|
||||||
|
}
|
63
packages/grafana-ui/src/components/Forms/Button.mdx
Normal file
63
packages/grafana-ui/src/components/Forms/Button.mdx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
|
||||||
|
import { Button } from './Button';
|
||||||
|
|
||||||
|
<Meta title="MDX|Button" component={Button} />
|
||||||
|
|
||||||
|
# Button
|
||||||
|
|
||||||
|
## Primary
|
||||||
|
|
||||||
|
Used for "call to action".
|
||||||
|
|
||||||
|
<Preview>
|
||||||
|
<div>
|
||||||
|
<Button variant="primary" size="sm" renderAs="button" style={{ margin: '5px' }}>
|
||||||
|
Small
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" size="md" renderAs="button" style={{ margin: '5px' }}>
|
||||||
|
Medium
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" size="lg" renderAs="button" style={{ margin: '5px' }}>
|
||||||
|
Large
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Preview>
|
||||||
|
|
||||||
|
## Secondary
|
||||||
|
|
||||||
|
The secondary button, used for "cancel" or aborting.
|
||||||
|
|
||||||
|
<Preview>
|
||||||
|
<div>
|
||||||
|
<Button variant="secondary" size="sm" renderAs="button" style={{ margin: '5px' }}>
|
||||||
|
Small
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" size="md" renderAs="button" style={{ margin: '5px' }}>
|
||||||
|
Medium
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" size="lg" renderAs="button" style={{ margin: '5px' }}>
|
||||||
|
Large
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Preview>
|
||||||
|
|
||||||
|
## Destructive
|
||||||
|
|
||||||
|
Used for removing or deleting entities.
|
||||||
|
|
||||||
|
<Preview>
|
||||||
|
<div>
|
||||||
|
<Button variant="destructive" size="sm" renderAs="button" style={{ margin: '5px' }}>
|
||||||
|
Small
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" size="md" renderAs="button" style={{ margin: '5px' }}>
|
||||||
|
Medium
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" size="lg" renderAs="button" style={{ margin: '5px' }}>
|
||||||
|
Large
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Preview>
|
||||||
|
|
||||||
|
|
||||||
|
<Props of={Button} />
|
33
packages/grafana-ui/src/components/Forms/Button.story.tsx
Normal file
33
packages/grafana-ui/src/components/Forms/Button.story.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
|
import { select, text } from '@storybook/addon-knobs';
|
||||||
|
import { ButtonSize, ButtonVariant } from '../Button/types';
|
||||||
|
import mdx from './Button.mdx';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'UI/Forms/Button',
|
||||||
|
component: Button,
|
||||||
|
decorators: [withCenteredStory, withHorizontallyCenteredStory],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
page: mdx,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const variants = ['primary', 'secondary', 'destructive'];
|
||||||
|
|
||||||
|
const sizes = ['sm', 'md', 'lg'];
|
||||||
|
|
||||||
|
export const simple = () => {
|
||||||
|
const variant = select('Variant', variants, 'primary');
|
||||||
|
const size = select('Size', sizes, 'md');
|
||||||
|
const buttonText = text('text', 'Button');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button variant={variant as ButtonVariant} size={size as ButtonSize} renderAs="button">
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
158
packages/grafana-ui/src/components/Forms/Button.tsx
Normal file
158
packages/grafana-ui/src/components/Forms/Button.tsx
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { css, cx } from 'emotion';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
import { selectThemeVariant, stylesFactory, useTheme } from '../../themes';
|
||||||
|
import { renderButton } from '../Button/AbstractButton';
|
||||||
|
import { getFocusStyle } from './commonStyles';
|
||||||
|
import { AbstractButtonProps, ButtonSize, ButtonVariant, StyleDeps } from '../Button/types';
|
||||||
|
import { GrafanaTheme } from '../../types';
|
||||||
|
|
||||||
|
const buttonVariantStyles = (from: string, to: string, textColor: string) => css`
|
||||||
|
background: linear-gradient(180deg, ${from} 0%, ${to} 100%);
|
||||||
|
color: ${textColor};
|
||||||
|
&:hover {
|
||||||
|
background: ${from};
|
||||||
|
color: ${textColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: ${from};
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const getPropertiesForSize = (theme: GrafanaTheme, size: ButtonSize) => {
|
||||||
|
switch (size) {
|
||||||
|
case 'sm':
|
||||||
|
return {
|
||||||
|
padding: `0 ${theme.spacing.sm}`,
|
||||||
|
fontSize: theme.typography.size.sm,
|
||||||
|
iconDistance: theme.spacing.xs,
|
||||||
|
height: theme.height.sm,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'md':
|
||||||
|
return {
|
||||||
|
padding: `0 ${theme.spacing.md}`,
|
||||||
|
fontSize: theme.typography.size.md,
|
||||||
|
iconDistance: theme.spacing.sm,
|
||||||
|
height: `${theme.spacing.formButtonHeight}px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'lg':
|
||||||
|
return {
|
||||||
|
padding: `0 ${theme.spacing.lg}`,
|
||||||
|
fontSize: theme.typography.size.lg,
|
||||||
|
iconDistance: theme.spacing.sm,
|
||||||
|
height: theme.height.lg,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
padding: `0 ${theme.spacing.md}`,
|
||||||
|
iconDistance: theme.spacing.sm,
|
||||||
|
fontSize: theme.typography.size.base,
|
||||||
|
height: theme.height.md,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'secondary':
|
||||||
|
const from = selectThemeVariant({ light: theme.colors.gray7, dark: theme.colors.gray15 }, theme.type) as string;
|
||||||
|
const to = selectThemeVariant(
|
||||||
|
{
|
||||||
|
light: tinycolor(from)
|
||||||
|
.darken(5)
|
||||||
|
.toString(),
|
||||||
|
dark: tinycolor(from)
|
||||||
|
.lighten(4)
|
||||||
|
.toString(),
|
||||||
|
},
|
||||||
|
theme.type
|
||||||
|
) as string;
|
||||||
|
|
||||||
|
return {
|
||||||
|
borderColor: selectThemeVariant({ light: theme.colors.gray70, dark: theme.colors.gray33 }, theme.type),
|
||||||
|
background: buttonVariantStyles(from, to, selectThemeVariant(
|
||||||
|
{ light: theme.colors.gray25, dark: theme.colors.gray4 },
|
||||||
|
theme.type
|
||||||
|
) as string),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'destructive':
|
||||||
|
return {
|
||||||
|
borderColor: theme.colors.redShade,
|
||||||
|
background: buttonVariantStyles(theme.colors.redBase, theme.colors.redShade, theme.colors.white),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'primary':
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
borderColor: theme.colors.blueShade,
|
||||||
|
background: buttonVariantStyles(theme.colors.blueBase, theme.colors.blueShade, theme.colors.white),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: StyleDeps) => {
|
||||||
|
const { padding, fontSize, iconDistance, height } = getPropertiesForSize(theme, size);
|
||||||
|
const { background, borderColor } = getPropertiesForVariant(theme, variant);
|
||||||
|
|
||||||
|
return {
|
||||||
|
button: cx(
|
||||||
|
css`
|
||||||
|
position: relative;
|
||||||
|
label: button;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: ${theme.typography.weight.semibold};
|
||||||
|
font-size: ${fontSize};
|
||||||
|
font-family: ${theme.typography.fontFamily.sansSerif};
|
||||||
|
line-height: ${theme.typography.lineHeight.sm};
|
||||||
|
padding: ${padding};
|
||||||
|
text-align: ${withIcon ? 'left' : 'center'};
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid ${borderColor};
|
||||||
|
height: ${height};
|
||||||
|
border-radius: ${theme.border.radius.sm};
|
||||||
|
${background};
|
||||||
|
|
||||||
|
&[disabled],
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.65;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
getFocusStyle(theme)
|
||||||
|
),
|
||||||
|
iconWrap: css`
|
||||||
|
label: button-icon-wrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`,
|
||||||
|
icon: css`
|
||||||
|
label: button-icon;
|
||||||
|
margin-right: ${iconDistance};
|
||||||
|
filter: brightness(100);
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Button: FC<Omit<AbstractButtonProps, 'theme'>> = ({
|
||||||
|
renderAs,
|
||||||
|
size = 'md',
|
||||||
|
variant = 'primary',
|
||||||
|
className,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
...otherProps
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const buttonStyles = getButtonStyles({ theme, size, variant, withIcon: !!icon });
|
||||||
|
|
||||||
|
return renderButton(theme, buttonStyles, renderAs, children, size, variant, icon, className, otherProps);
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { text } from '@storybook/addon-knobs';
|
import { text } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
import { Label } from './Label';
|
import { Label } from './Label';
|
||||||
|
20
packages/grafana-ui/src/components/Forms/commonStyles.ts
Normal file
20
packages/grafana-ui/src/components/Forms/commonStyles.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { css } from 'emotion';
|
||||||
|
import { GrafanaTheme } from '../../types';
|
||||||
|
|
||||||
|
export const getFocusStyle = (theme: GrafanaTheme) => css`
|
||||||
|
&[focus],
|
||||||
|
&:focus {
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid ${theme.colors.blueLight};
|
||||||
|
border-radius: ${theme.border.radius.lg};
|
||||||
|
background-color: ${theme.colors.bodyBg};
|
||||||
|
height: calc(100% + 8px);
|
||||||
|
width: calc(100% + 8px);
|
||||||
|
top: -4px;
|
||||||
|
left: -4px;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@ -3,11 +3,13 @@ import { GrafanaTheme } from '../../types';
|
|||||||
import { getLabelStyles } from './Label';
|
import { getLabelStyles } from './Label';
|
||||||
import { getLegendStyles } from './Legend';
|
import { getLegendStyles } from './Legend';
|
||||||
import { getFieldValidationMessageStyles } from './FieldValidationMessage';
|
import { getFieldValidationMessageStyles } from './FieldValidationMessage';
|
||||||
|
import { getButtonStyles } from './Button';
|
||||||
|
|
||||||
export const getFormStyles = stylesFactory((theme: GrafanaTheme) => {
|
export const getFormStyles = stylesFactory((theme: GrafanaTheme, options?: any) => {
|
||||||
return {
|
return {
|
||||||
...getLabelStyles(theme),
|
...getLabelStyles(theme),
|
||||||
...getLegendStyles(theme),
|
...getLegendStyles(theme),
|
||||||
...getFieldValidationMessageStyles(theme),
|
...getFieldValidationMessageStyles(theme),
|
||||||
|
...getButtonStyles({ theme, variant: options.variant, size: options.size, withIcon: options.withIcon }),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -202,6 +202,7 @@ exports[`Render should render with base threshold 1`] = `
|
|||||||
},
|
},
|
||||||
"spacing": Object {
|
"spacing": Object {
|
||||||
"d": "14px",
|
"d": "14px",
|
||||||
|
"formButtonHeight": 32,
|
||||||
"formFieldsetMargin": "16px",
|
"formFieldsetMargin": "16px",
|
||||||
"formInputAffixPaddingHorizontal": "4px",
|
"formInputAffixPaddingHorizontal": "4px",
|
||||||
"formInputHeight": "32px",
|
"formInputHeight": "32px",
|
||||||
@ -255,6 +256,7 @@ exports[`Render should render with base threshold 1`] = `
|
|||||||
"xs": "10px",
|
"xs": "10px",
|
||||||
},
|
},
|
||||||
"weight": Object {
|
"weight": Object {
|
||||||
|
"bold": 600,
|
||||||
"light": 300,
|
"light": 300,
|
||||||
"regular": 400,
|
"regular": 400,
|
||||||
"semibold": 500,
|
"semibold": 500,
|
||||||
@ -410,6 +412,7 @@ exports[`Render should render with base threshold 1`] = `
|
|||||||
},
|
},
|
||||||
"spacing": Object {
|
"spacing": Object {
|
||||||
"d": "14px",
|
"d": "14px",
|
||||||
|
"formButtonHeight": 32,
|
||||||
"formFieldsetMargin": "16px",
|
"formFieldsetMargin": "16px",
|
||||||
"formInputAffixPaddingHorizontal": "4px",
|
"formInputAffixPaddingHorizontal": "4px",
|
||||||
"formInputHeight": "32px",
|
"formInputHeight": "32px",
|
||||||
@ -463,6 +466,7 @@ exports[`Render should render with base threshold 1`] = `
|
|||||||
"xs": "10px",
|
"xs": "10px",
|
||||||
},
|
},
|
||||||
"weight": Object {
|
"weight": Object {
|
||||||
|
"bold": 600,
|
||||||
"light": 300,
|
"light": 300,
|
||||||
"regular": 400,
|
"regular": 400,
|
||||||
"semibold": 500,
|
"semibold": 500,
|
||||||
|
@ -6,7 +6,6 @@ export { Portal } from './Portal/Portal';
|
|||||||
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
||||||
|
|
||||||
export * from './Button/Button';
|
export * from './Button/Button';
|
||||||
export { ButtonVariant } from './Button/AbstractButton';
|
|
||||||
|
|
||||||
// Select
|
// Select
|
||||||
export { Select, AsyncSelect } from './Select/Select';
|
export { Select, AsyncSelect } from './Select/Select';
|
||||||
|
@ -50,6 +50,7 @@ const theme: GrafanaThemeCommons = {
|
|||||||
light: 300,
|
light: 300,
|
||||||
regular: 400,
|
regular: 400,
|
||||||
semibold: 500,
|
semibold: 500,
|
||||||
|
bold: 600,
|
||||||
},
|
},
|
||||||
lineHeight: {
|
lineHeight: {
|
||||||
xs: 1,
|
xs: 1,
|
||||||
@ -87,6 +88,7 @@ const theme: GrafanaThemeCommons = {
|
|||||||
formFieldsetMargin: `${SPACING_BASE * 2}px`,
|
formFieldsetMargin: `${SPACING_BASE * 2}px`,
|
||||||
formLegendMargin: `0 0 ${SPACING_BASE * 2}px 0`,
|
formLegendMargin: `0 0 ${SPACING_BASE * 2}px 0`,
|
||||||
formInputHeight: `${SPACING_BASE * 4}px`,
|
formInputHeight: `${SPACING_BASE * 4}px`,
|
||||||
|
formButtonHeight: SPACING_BASE * 4,
|
||||||
formInputPaddingHorizontal: `${SPACING_BASE}px`,
|
formInputPaddingHorizontal: `${SPACING_BASE}px`,
|
||||||
|
|
||||||
// Used for icons do define spacing between icon and input field
|
// Used for icons do define spacing between icon and input field
|
||||||
|
@ -30,6 +30,7 @@ export interface GrafanaThemeCommons {
|
|||||||
light: number;
|
light: number;
|
||||||
regular: number;
|
regular: number;
|
||||||
semibold: number;
|
semibold: number;
|
||||||
|
bold: number;
|
||||||
};
|
};
|
||||||
lineHeight: {
|
lineHeight: {
|
||||||
xs: number; //1
|
xs: number; //1
|
||||||
@ -69,6 +70,7 @@ export interface GrafanaThemeCommons {
|
|||||||
formFieldsetMargin: string;
|
formFieldsetMargin: string;
|
||||||
formLegendMargin: string;
|
formLegendMargin: string;
|
||||||
formInputHeight: string;
|
formInputHeight: string;
|
||||||
|
formButtonHeight: number;
|
||||||
formInputPaddingHorizontal: string;
|
formInputPaddingHorizontal: string;
|
||||||
// Used for icons do define spacing between icon and input field
|
// Used for icons do define spacing between icon and input field
|
||||||
// Applied on the right(prefix) or left(suffix)
|
// Applied on the right(prefix) or left(suffix)
|
||||||
|
Loading…
Reference in New Issue
Block a user